From 717b988a7fe0efafb64a91c3caa8ffc10cc30594 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 16 Jun 2026 14:12:00 +0800 Subject: [PATCH 01/39] chore: bump version to 1.0.0.dev16 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dee3111b..77ae651d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rhosocial-activerecord-postgres" -version = "1.0.0.dev15" +version = "1.0.0.dev16" description = "postgres backend implementation for rhosocial-activerecord, providing a robust and optimized postgres database support." readme = "README.md" license = { file = "LICENSE" } From bd72d65131a537b6f407b49d207f0479a30be8e0 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 19 Jun 2026 12:53:36 +0800 Subject: [PATCH 02/39] ci: update activerecord dependency to release/v1.0.0.dev29 --- .github/workflows/test.yml | 8 +- tests/providers/composite_pk.py | 122 ++++++++++++++++++ tests/providers/registry.py | 3 + .../feature/composite_pk/conftest.py | 2 + .../composite_pk/schema/order_items.sql | 7 + .../feature/composite_pk/schema/orders.sql | 6 + .../composite_pk/schema/store_inventory.sql | 7 + .../test_composite_pk_active_query.py | 2 + .../test_composite_pk_activerecord.py | 2 + .../test_composite_pk_cte_query.py | 2 + .../test_composite_pk_set_operation.py | 2 + 11 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 tests/providers/composite_pk.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 102ecdc0..621d5075 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,7 +68,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord + pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord pip install rhosocial-activerecord-testsuite==1.0.0.dev15 - name: Create postgres scenario config @@ -279,7 +279,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord + pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord pip install rhosocial-activerecord-testsuite==1.0.0.dev15 - name: Create postgres scenario config @@ -391,7 +391,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord + pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord pip install rhosocial-activerecord-testsuite==1.0.0.dev15 - name: Create postgres scenario config @@ -490,7 +490,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord + pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord pip install rhosocial-activerecord-testsuite==1.0.0.dev15 - name: Create postgres scenario config diff --git a/tests/providers/composite_pk.py b/tests/providers/composite_pk.py new file mode 100644 index 00000000..93eed043 --- /dev/null +++ b/tests/providers/composite_pk.py @@ -0,0 +1,122 @@ +# tests/providers/composite_pk.py +import os +from typing import Type, List + +from rhosocial.activerecord.model import ActiveRecord + +from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( + OrderItem as OrderItemBase, + StoreInventory as StoreInventoryBase, + Order as OrderBase, +) +from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( + AsyncOrderItem as AsyncOrderItemBase, + AsyncStoreInventory as AsyncStoreInventoryBase, + AsyncOrder as AsyncOrderBase, +) + +from .scenarios import get_enabled_scenarios, get_scenario + + +class CompositePKProvider: + def __init__(self): + self._active_backends = [] + self._active_async_backends = [] + + def get_test_scenarios(self) -> List[str]: + return list(get_enabled_scenarios().keys()) + + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: + backend_class, config = get_scenario(scenario_name) + model_class.configure(config, backend_class) + backend_instance = model_class.__backend__ + if backend_instance not in self._active_backends: + self._active_backends.append(backend_instance) + try: + backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + schema_sql = self._load_postgres_schema(f"{table_name}.sql") + backend_instance.execute(schema_sql) + return model_class + + 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 + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + if backend_instance not in self._active_async_backends: + self._active_async_backends.append(backend_instance) + try: + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + schema_sql = self._load_postgres_schema(f"{table_name}.sql") + await backend_instance.execute(schema_sql) + return model_class + + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), + "..", + "rhosocial", + "activerecord_postgres_test", + "feature", + "composite_pk", + "schema", + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, "r", encoding="utf-8") as f: + return f.read() + + def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + return self._setup_model(OrderItemBase, scenario_name, "order_items") + + def setup_store_inventory_model(self, scenario_name: str) -> Type[ActiveRecord]: + return self._setup_model(StoreInventoryBase, scenario_name, "store_inventory") + + def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: + return self._setup_model(OrderBase, scenario_name, "orders") + + async def setup_async_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncOrderItemBase, scenario_name, "order_items") + + async def setup_async_store_inventory_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncStoreInventoryBase, scenario_name, "store_inventory") + + async def setup_async_order_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncOrderBase, scenario_name, "orders") + + def cleanup_after_test(self, scenario_name: str): + tables = ["order_items", "store_inventory", "orders"] + for backend in self._active_backends: + try: + for t in tables: + try: + backend.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE') + except Exception: + pass + except Exception: + pass + try: + backend.disconnect() + except Exception: + pass + self._active_backends.clear() + + async def cleanup_after_test_async(self, scenario_name: str): + tables = ["order_items", "store_inventory", "orders"] + for backend in self._active_async_backends: + try: + for t in tables: + try: + await backend.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE') + except Exception: + pass + except Exception: + pass + try: + await backend.disconnect() + except Exception: + pass + self._active_async_backends.clear() diff --git a/tests/providers/registry.py b/tests/providers/registry.py index 152d0088..34984fff 100644 --- a/tests/providers/registry.py +++ b/tests/providers/registry.py @@ -13,6 +13,7 @@ from .relation import RelationProvider from .basic_connection import BasicConnectionProvider from .query_connection import QueryConnectionProvider +from .composite_pk import CompositePKProvider from .crud_benchmark import CrudBenchmarkProvider from .fastapi_benchmark import FastAPIBenchmarkProvider from .mixin_benchmark import MixinBenchmarkProvider @@ -40,6 +41,8 @@ QueryConnectionProvider, ) +provider_registry.register("feature.composite_pk.ICompositePKProvider", CompositePKProvider) + # Register benchmark providers. provider_registry.register("benchmark.crud.ICrudBenchmarkProvider", CrudBenchmarkProvider) provider_registry.register("benchmark.query.IQueryBenchmarkProvider", QueryBenchmarkProvider) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py new file mode 100644 index 00000000..4d6a5f53 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py @@ -0,0 +1,2 @@ +# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py +from rhosocial.activerecord.testsuite.feature.composite_pk.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql new file mode 100644 index 00000000..9ea4998a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql @@ -0,0 +1,7 @@ +CREATE TABLE "order_items" ( + "order_id" INTEGER NOT NULL, + "product_id" INTEGER NOT NULL, + "quantity" INTEGER NOT NULL DEFAULT 1, + "unit_price" DECIMAL(10,2) NOT NULL, + PRIMARY KEY ("order_id", "product_id") +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql new file mode 100644 index 00000000..b96c78fa --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql @@ -0,0 +1,6 @@ +CREATE TABLE "orders" ( + "id" SERIAL PRIMARY KEY, + "total" DECIMAL(10,2) NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE, + "updated_at" TIMESTAMP WITH TIME ZONE +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql new file mode 100644 index 00000000..e4bfbab9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql @@ -0,0 +1,7 @@ +CREATE TABLE "store_inventory" ( + "store_id" INTEGER NOT NULL, + "product_id" INTEGER NOT NULL, + "batch_id" VARCHAR(64) NOT NULL, + "stock" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY ("store_id", "product_id", "batch_id") +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py new file mode 100644 index 00000000..66e9faae --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py @@ -0,0 +1,2 @@ +# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py +from rhosocial.activerecord.testsuite.feature.composite_pk.test_active_query import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py new file mode 100644 index 00000000..a4b222c9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py @@ -0,0 +1,2 @@ +# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py +from rhosocial.activerecord.testsuite.feature.composite_pk.test_activerecord import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py new file mode 100644 index 00000000..6790f16b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py @@ -0,0 +1,2 @@ +# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py +from rhosocial.activerecord.testsuite.feature.composite_pk.test_cte_query import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py new file mode 100644 index 00000000..148c1b97 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py @@ -0,0 +1,2 @@ +# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py +from rhosocial.activerecord.testsuite.feature.composite_pk.test_set_operation import * # noqa: F403 From 63ed8d1abba43022a99f1c34bf54c0c295da8afa Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 19 Jun 2026 13:04:31 +0800 Subject: [PATCH 03/39] fix: install testsuite from git release/v1.0.0.dev16 instead of PyPI dev15 PyPI rhosocial-activerecord-testsuite==1.0.0.dev15 does not include the composite_pk module added in testsuite v1.0.0.dev16. Install from the release branch directly to resolve ModuleNotFoundError. --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 621d5075..15186a95 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: - name: Install activerecord and testsuite run: | pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord - pip install rhosocial-activerecord-testsuite==1.0.0.dev15 + pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev16#egg=rhosocial-activerecord-testsuite - name: Create postgres scenario config run: | @@ -280,7 +280,7 @@ jobs: - name: Install activerecord and testsuite run: | pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord - pip install rhosocial-activerecord-testsuite==1.0.0.dev15 + pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev16#egg=rhosocial-activerecord-testsuite - name: Create postgres scenario config run: | @@ -392,7 +392,7 @@ jobs: - name: Install activerecord and testsuite run: | pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord - pip install rhosocial-activerecord-testsuite==1.0.0.dev15 + pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev16#egg=rhosocial-activerecord-testsuite - name: Create postgres scenario config run: | @@ -491,7 +491,7 @@ jobs: - name: Install activerecord and testsuite run: | pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev29#egg=rhosocial-activerecord - pip install rhosocial-activerecord-testsuite==1.0.0.dev15 + pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev16#egg=rhosocial-activerecord-testsuite - name: Create postgres scenario config run: | From fb975d53a5eae870f5ec535aef893f42bd1bac78 Mon Sep 17 00:00:00 2001 From: zhao vistart <864657+vistart@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:11:11 +0800 Subject: [PATCH 04/39] feat(schema): implement schema-diff support and DataType refactoring for PostgreSQL backend (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(types): implement PostgreSQL DataType subclasses and type support mixin - Add ~45 PostgreSQL-specific DataType subclasses in expression/types.py - Add PostgresTypeFormatSupportMixin with TypeFormattingSupport + TypeParsingSupport - Integrate type support into PostgresDialect - Update exports in __init__.py, expression/__init__.py, mixins/__init__.py, mixins/types/__init__.py * refactor(types): add __eq__/__hash__, remove synonyms, add CharacterVaryingType - Replace _type_params() with __eq__/__hash__ in PostgresBitType, PostgresVarBitType, PostgresVectorType - Remove synonyms() from PostgresSmallSerialType, PostgresSerialType, PostgresBigSerialType - Add PostgresCharacterVaryingType(VarCharType) for CHARACTER VARYING(n) - Register new type in data_type_formatting mixin - Clean up unused imports (ClassVar, Set, Tuple) * feat(schema): add parsed_data_type to PostgreSQL _parse_columns Call DataType.parse_data_type_str(dialect, data_type) when constructing ColumnInfo so that introspection results carry structured DataType instances for schema comparison. * feat(types): add synonyms to PostgreSQL types - PostgresSerialType ↔ IntegerType/SerialType - PostgresSmallSerialType ↔ SmallIntType/SmallSerialType - PostgresBigSerialType ↔ BigIntType/BigSerialType - PostgresCharacterVaryingType ↔ VarCharType * feat(types): add synonyms to PostgresByteaType PostgresByteaType ↔ BlobType for cross-backend schema comparison. * refactor(types): inherit DDLTypeMixin in Postgres type formatting * refactor(types): update to DDLTypeSupport protocol * refactor(types): use @DDLTypeMixin.handles, remove format_data_type override - Add @DDLTypeMixin.handles decorators to all 56 formatter methods - Remove custom format_data_type() dispatch and supports_data_types() list - Move Postgres type imports from TYPE_CHECKING to runtime * fix(types): rewrite Postgres core type formatters, fix missing PostgresTypeFormatSupportMixin base - Replace _default_sql() calls with explicit PostgreSQL SQL strings for all 18 core type handlers (SmallIntType→SMALLINT, etc.) - Add PostgresTypeFormatSupportMixin as base of PostgresDialect (was imported but never used as base, causing format_data_type to resolve to protocol stub and return None) - Create test_core_type_mappings.py with 15 tests for core type→SQL rendering via PostgreSQL dialect * feat(postgres): implement array type support - Add PostgresArrayType parameterised container type (element_type, dimensions) - Add format_data_type_array handler that recursively formats element types - Update parse_type to detect ARRAY keyword and [] suffix before standard dispatch - Add 25 tests covering core type mappings and array rendering/parsing * refactor(postgres): use core ArrayType, keep PostgresArrayType as alias - PostgresArrayType is now just 'PostgresArrayType = ArrayType' - Update handler registration to use core ArrayType - Update parse_type to return ArrayType directly - Add test for is_element_type_equivalent * feat(postgres): add element type validation and PostgresArrayType subclass - format_data_type_array validates element_type is in _type_formatters - PostgresArrayType is now a proper subclass with is_equivalent ignoring dimensions (PG normalises multi-dim arrays to 1-D internally) - parse_type always creates PostgresArrayType with dimensions=1 - Update tests for new PostgresArrayType subclass and PG array semantics * feat(schema): add PostgresSchemaDiffer Column order has no semantic meaning in PostgreSQL; array dimension equivalence is handled by PostgresArrayType.is_equivalent(). * feat(schema): add schema-diff examples and refactor for PostgreSQL backend - Add 6 schema-diff example scripts: table_changes, column_order_irrelevant, postgres10_identity, array_type_diff, index_type_diff, serialization_roundtrip - All DDL uses expression objects, call introspect_and_adapt() after connect - Refactor format_column_definition to use col_def.data_type.to_sql() and add identity dialect_options support - Update existing DDL examples to use expression API (table param, QueryExpression for verification) * refactor(types): remove _default_sql(), make format_data_type return Tuple[str, tuple] - Delete all _default_sql() methods from Postgres expression types - Change all PostgresTypeFormatSupportMixin formatters to return Tuple[str, tuple] - Inline _default_sql() logic directly into each format_data_type_* method - Unpack format_data_type() call in array formatter * style(types): add complete type annotations to all format_data_type_* methods * feat(examples): strengthen serialization roundtrip with table data assertions * fix(tests): replace string data_type with DataType instances across 22 test files ColumnDefinition now validates that data_type must be a DataType instance. Replace all string data_type values (SERIAL, TEXT, INTEGER, etc.) with proper instance references. Also add PostgresCitextType, PostgresCubeType, PostgresLtreeType, PostgresRasterType missing extension DataType subclasses. Update security injection tests to verify TypeError at construction time instead of ValueError at format time. * docs: add changelog fragment for PR #46 * fix(tests): replace positional string data_type with DataType instances in 6 more files * fix(types): register PostgresCitextType, PostgresCubeType, PostgresLtreeType, PostgresRasterType formatters * fix: restore PostgresCidrType import accidentally removed in type formatter registration --- changelog.d/46.added.md | 1 + .../backend/impl/postgres/__init__.py | 97 +++ .../backend/impl/postgres/dialect.py | 5 + .../impl/postgres/examples/conftest.py | 42 + .../impl/postgres/examples/ddl/alter_table.py | 26 +- .../postgres/examples/ddl/create_index.py | 18 +- .../examples/ddl/materialized_view.py | 282 ++++-- .../postgres/examples/schema_diff/__init__.py | 0 .../examples/schema_diff/array_type_diff.py | 141 +++ .../schema_diff/column_order_irrelevant.py | 103 +++ .../examples/schema_diff/index_type_diff.py | 103 +++ .../schema_diff/postgres10_identity.py | 116 +++ .../schema_diff/serialization_roundtrip.py | 127 +++ .../examples/schema_diff/table_changes.py | 109 +++ .../impl/postgres/expression/__init__.py | 101 +++ .../backend/impl/postgres/expression/types.py | 391 +++++++++ .../postgres/introspection/introspector.py | 3 + .../backend/impl/postgres/mixins/__init__.py | 2 + .../backend/impl/postgres/mixins/table.py | 15 +- .../impl/postgres/mixins/types/__init__.py | 2 + .../mixins/types/data_type_formatting.py | 821 ++++++++++++++++++ .../backend/impl/postgres/schema/__init__.py | 6 + .../backend/impl/postgres/schema/differ.py | 15 + tests/providers/partition.py | 17 +- .../extensions/test_bloom_integration.py | 27 +- .../extensions/test_btree_gin_integration.py | 25 +- .../extensions/test_btree_gist_integration.py | 25 +- .../extensions/test_citext_integration.py | 52 +- .../extensions/test_cube_integration.py | 29 +- .../test_earthdistance_integration.py | 13 +- .../test_fuzzystrmatch_integration.py | 22 +- .../extensions/test_hstore_integration.py | 8 +- .../extensions/test_hypopg_integration.py | 18 +- .../extensions/test_intarray_integration.py | 47 +- .../extensions/test_ltree_integration.py | 51 +- .../extensions/test_orafce_integration.py | 32 +- .../extensions/test_pg_trgm_integration.py | 38 +- .../extensions/test_pgcrypto_integration.py | 27 +- .../extensions/test_pgrouting_integration.py | 43 +- .../extensions/test_pgvector_integration.py | 8 +- .../extensions/test_postgis_integration.py | 45 +- .../test_postgis_raster_integration.py | 19 +- .../extensions/test_tablefunc_integration.py | 34 +- .../extensions/test_uuid_ossp_integration.py | 22 +- .../postgres/test_core_type_mappings.py | 212 +++++ .../backend/postgres/test_ddl_expressions.py | 26 +- .../test_dialect_security_integration.py | 20 +- .../test_property_graph_query_scenarios.py | 51 +- .../feature/backend/test_create_table_like.py | 11 +- .../test_partition_advanced_operations.py | 34 +- .../backend/test_partition_operations.py | 20 +- .../backend/test_postgres_dialect_security.py | 21 +- 52 files changed, 3137 insertions(+), 386 deletions(-) create mode 100644 changelog.d/46.added.md create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/__init__.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/expression/types.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py diff --git a/changelog.d/46.added.md b/changelog.d/46.added.md new file mode 100644 index 00000000..cf118163 --- /dev/null +++ b/changelog.d/46.added.md @@ -0,0 +1 @@ +Added PostgreSQL-specific DataType subclasses and schema-diff support with `PostgresSchemaDiffer` for accurate schema introspection and column comparison. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py index 83f988d5..9c6f720e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py @@ -23,6 +23,55 @@ from .dialect import PostgresDialect from .transaction import PostgresTransactionManager, AsyncPostgresTransactionManager from .types import PostgresEnumType +from .expression.types import ( + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, +) from .explain import PostgresExplainResult, PostgresExplainPlanLine from .expression.ddl import ( PostgresCreateEnumTypeExpression, @@ -505,4 +554,52 @@ # hstore functions - Subscript "hstore_subscript_get", "hstore_subscript_set", + # PostgreSQL DataType subclasses for DDL + "PostgresBigSerialType", + "PostgresBitType", + "PostgresBoxType", + "PostgresByteaType", + "PostgresCIDType", + "PostgresCircleType", + "PostgresDateMultirangeType", + "PostgresDateRangeType", + "PostgresGeographyType", + "PostgresGeometryType", + "PostgresHstoreType", + "PostgresInetType", + "PostgresInt4MultirangeType", + "PostgresInt4RangeType", + "PostgresInt8MultirangeType", + "PostgresInt8RangeType", + "PostgresJsonPathType", + "PostgresLineSegmentType", + "PostgresLineType", + "PostgresMacAddr8Type", + "PostgresMacAddrType", + "PostgresMoneyType", + "PostgresNumMultirangeType", + "PostgresNumRangeType", + "PostgresOIDType", + "PostgresPathType", + "PostgresPgLSNType", + "PostgresPointType", + "PostgresPolygonType", + "PostgresRegClassType", + "PostgresRegTypeType", + "PostgresSerialType", + "PostgresSmallSerialType", + "PostgresTIDType", + "PostgresTSQueryType", + "PostgresTSVectorType", + "PostgresTsMultirangeType", + "PostgresTsRangeType", + "PostgresTsTzMultirangeType", + "PostgresTsTzRangeType", + "PostgresUUIDType", + "PostgresVarBitType", + "PostgresVectorType", + "PostgresXID8Type", + "PostgresXIDType", + "PostgresXMLType", + "PostgresCidrType", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index c59bb218..8a681cb0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -90,6 +90,7 @@ IntrospectionSupport, TransactionControlSupport, SQLFunctionSupport, + DDLTypeSupport, ) from .mixins import ( PostgresExtensionMixin, @@ -169,6 +170,7 @@ PostgresRangeTypeMixin, PostgresJSONBEnhancedMixin, PostgresArrayEnhancedMixin, + PostgresTypeFormatSupportMixin, # DDL/DML operation mixins (new) PostgresExtendedStatisticsMixin, PostgresStoredProcedureMixin, @@ -400,6 +402,7 @@ class PostgresDialect( PostgresFullTextSearchMixin, PostgresRangeTypeMixin, PostgresArrayEnhancedMixin, + PostgresTypeFormatSupportMixin, # DDL/DML operation mixins (new) PostgresExtendedStatisticsMixin, PostgresStoredProcedureMixin, @@ -524,6 +527,8 @@ class PostgresDialect( PostgresRangeTypeSupport, PostgresJSONBEnhancedSupport, PostgresArrayEnhancedSupport, + # DataType Support Protocol + DDLTypeSupport, # New feature protocols PostgresParallelQuerySupport, PostgresStoredProcedureSupport, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py index 44cc9fd2..a3c1a4db 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py @@ -416,4 +416,46 @@ 'max_version': '18', 'note': 'Requires superuser or pg_read_server_files role; only available in PG15+ docker images', }, + 'schema_diff/table_changes.py': { + 'title': 'Schema Diff — Table Add/Remove', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/column_order_irrelevant.py': { + 'title': 'Schema Diff — Column Order Is Ignored', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/postgres10_identity.py': { + 'title': 'Schema Diff — PG9 SERIAL vs PG10+ IDENTITY', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/array_type_diff.py': { + 'title': 'Schema Diff — Array Type Dimension Changes', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/index_type_diff.py': { + 'title': 'Schema Diff — PostgreSQL Index Type Switches', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/serialization_roundtrip.py': { + 'title': 'Schema Diff — Snapshot Serialization Roundtrip', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, } diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py index 9005f9a7..ce68ffae 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py @@ -38,7 +38,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table='users', if_exists=True, cascade=True, ) @@ -47,7 +47,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table='users', columns=[ ColumnDefinition( 'id', @@ -148,13 +148,23 @@ print("Column renamed successfully") # Verify table structure -options = ExecutionOptions(stmt_type=StatementType.DQL) -result = backend.execute( - "SELECT column_name, data_type, is_nullable " - "FROM information_schema.columns " - "WHERE table_name = 'users' ORDER BY ordinal_position", - options=options +from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression.core import WildcardExpression, Column +from rhosocial.activerecord.backend.expression.query_parts import OrderByClause + +verify_expr = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'column_name'), + Column(dialect, 'data_type'), + Column(dialect, 'is_nullable'), + ], + from_=TableExpression(dialect, 'columns', schema_name='information_schema'), + where=Column(dialect, 'table_name') == Literal(dialect, 'users'), + order_by=OrderByClause(dialect, [Column(dialect, 'ordinal_position')]), ) +options = ExecutionOptions(stmt_type=StatementType.DQL) +result = backend.execute(*verify_expr.to_sql(), options=options) print("Table structure after alterations:") for row in result.data or []: print(f" {row}") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py index 9e097b89..5e0c1ea3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py @@ -33,7 +33,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='products', + table='products', if_exists=True, cascade=True, ) @@ -42,7 +42,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='products', + table='products', columns=[ ColumnDefinition( 'id', @@ -83,11 +83,17 @@ print("Index created: idx_category_price") # Verify index creation -options = ExecutionOptions(stmt_type=StatementType.DQL) -verify_result = backend.execute( - "SELECT indexname FROM pg_indexes WHERE indexname = 'idx_category_price'", - options=options +from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression.core import Column, Literal + +verify_expr = QueryExpression( + dialect=dialect, + select=[Column(dialect, 'indexname')], + from_=TableExpression(dialect, 'pg_indexes'), + where=Column(dialect, 'indexname') == Literal(dialect, 'idx_category_price'), ) +options = ExecutionOptions(stmt_type=StatementType.DQL) +verify_result = backend.execute(*verify_expr.to_sql(), options=options) print(f"Index info: {verify_result.data}") # ============================================================ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py index 557e2858..1ef87074 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py @@ -2,10 +2,16 @@ Materialized Views - PostgreSQL 9.3+. This example demonstrates: -1. Creating a materialized view -2. Refreshing materialized views -3. Using WITH NO DATA for fast definition -4. Materialized view with aggregations +1. Creating a materialized view with aggregations +2. Column aliases for the materialized view +3. Storage options (WITH clause) for physical storage tuning +4. TABLESPACE assignment +5. WITH DATA vs WITH NO DATA population modes +6. REFRESH MATERIALIZED VIEW (normal) +7. REFRESH MATERIALIZED VIEW CONCURRENTLY (PostgreSQL 9.4+) +8. REFRESH MATERIALIZED VIEW WITH DATA (after NO DATA) +9. Schema-qualified refresh (PostgresRefreshMaterializedViewExpression) +10. DROP MATERIALIZED VIEW with CASCADE and IF EXISTS """ # ============================================================ @@ -24,6 +30,7 @@ CreateMaterializedViewExpression, DropMaterializedViewExpression, RefreshMaterializedViewExpression, + CreateIndexExpression, GroupByHavingClause, FunctionCall, ) @@ -46,22 +53,35 @@ ) backend = PostgresBackend(connection_config=config) backend.connect() +backend.introspect_and_adapt() dialect = backend.dialect dql_options = ExecutionOptions(stmt_type=StatementType.DQL) +ddl_options = ExecutionOptions(stmt_type=StatementType.DDL) +_drop_mv = DropMaterializedViewExpression( + dialect=dialect, view_name='sales_summary', if_exists=True, cascade=True, +) +backend.execute(*_drop_mv.to_sql()) +_drop_mv = DropMaterializedViewExpression( + dialect=dialect, view_name='sales_daily', if_exists=True, cascade=True, +) +backend.execute(*_drop_mv.to_sql()) +_drop_mv = DropMaterializedViewExpression( + dialect=dialect, view_name='sales_product_summary', if_exists=True, cascade=True, +) +backend.execute(*_drop_mv.to_sql()) drop_table = DropTableExpression( dialect=dialect, - table_name='sales', + table='sales', if_exists=True, cascade=True, ) -sql, params = drop_table.to_sql() -backend.execute(sql, params) +backend.execute(*drop_table.to_sql()) create_table = CreateTableExpression( dialect=dialect, - table_name='sales', + table='sales', columns=[ ColumnDefinition( 'id', @@ -105,18 +125,18 @@ backend.execute(sql, params) # ============================================================ -# SECTION: Create Materialized View +# SECTION: 1. Create Materialized View with Column Aliases and Storage Options # ============================================================ -# Materialized views store the query result physically +# Column aliases rename the output columns of the materialized view. +# Storage options control physical storage behavior via WITH (param = value). -# Build the summary query with aggregations summary_query = QueryExpression( dialect=dialect, select=[ Column(dialect, 'product_id'), - FunctionCall(dialect, 'COUNT', WildcardExpression(dialect)).as_('total_sales'), - FunctionCall(dialect, 'SUM', Column(dialect, 'amount')).as_('total_amount'), - FunctionCall(dialect, 'AVG', Column(dialect, 'amount')).as_('avg_amount'), + FunctionCall(dialect, 'COUNT', WildcardExpression(dialect)), + FunctionCall(dialect, 'SUM', Column(dialect, 'amount')), + FunctionCall(dialect, 'AVG', Column(dialect, 'amount')), ], from_=TableExpression(dialect, 'sales'), group_by_having=GroupByHavingClause( @@ -129,12 +149,18 @@ dialect=dialect, view_name='sales_summary', query=summary_query, + column_aliases=['product_id', 'total_sales', 'total_amount', 'avg_amount'], + storage_options={ + 'fillfactor': 70, + 'autovacuum_enabled': 'true', + }, ) sql, params = create_mv.to_sql() -print(f"Create MV SQL: {sql}") +print(f"Create MV with column aliases and storage options:") +print(f" SQL: {sql}") backend.execute(sql, params) -# Verify the materialized view content +# Verify verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], @@ -143,53 +169,47 @@ ) sql, params = verify_query.to_sql() result = backend.execute(sql, params, options=dql_options) -print("Materialized view result:") +print("MV result:") for row in result.data or []: print(f" {row}") # ============================================================ -# SECTION: Refresh Materialized View +# SECTION: 2. TABLESPACE (SQL Generation Demo) # ============================================================ -# Refresh to update the data +# Tablespace requires an existing tablespace on the server. +# This section generates the SQL to show the syntax; execution is optional. -# Insert new data -insert_new = InsertExpression( +print() +sales_query = QueryExpression( dialect=dialect, - into='sales', - columns=['product_id', 'amount', 'sale_date'], - source=ValuesSource( - dialect, - [[Literal(dialect, '1'), Literal(dialect, '300.00'), Literal(dialect, "'2024-01-05'")]], - ), + select=[ + Column(dialect, 'product_id'), + Column(dialect, 'amount'), + Column(dialect, 'sale_date'), + ], + from_=TableExpression(dialect, 'sales'), ) -sql, params = insert_new.to_sql() -backend.execute(sql, params) -# Data in view is stale - refresh to see new data -refresh_mv = RefreshMaterializedViewExpression( +mv_tablespace = CreateMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view_name='sales_on_fast_storage', + query=sales_query, + tablespace='fast_ssd', ) -sql, params = refresh_mv.to_sql() -backend.execute(sql, params) - -# Verify after refresh -sql, params = verify_query.to_sql() -result = backend.execute(sql, params, options=dql_options) -print("After refresh:") -for row in result.data or []: - print(f" {row}") +sql, params = mv_tablespace.to_sql() +print(f"TABLESPACE example SQL (not executed): {sql}") # ============================================================ -# SECTION: Create with NO DATA +# SECTION: 3. WITH NO DATA — Fast Definition Without Population # ============================================================ -# Use WITH NO DATA to create without populating +# Create a materialized view without initial data. The MV is registered +# but cannot be queried until a REFRESH populates it. daily_query = QueryExpression( dialect=dialect, select=[ Column(dialect, 'sale_date'), - FunctionCall(dialect, 'SUM', Column(dialect, 'amount')).as_('daily_total'), + FunctionCall(dialect, 'SUM', Column(dialect, 'amount')), ], from_=TableExpression(dialect, 'sales'), group_by_having=GroupByHavingClause( @@ -202,12 +222,14 @@ dialect=dialect, view_name='sales_daily', query=daily_query, + column_aliases=['sale_date', 'daily_total'], with_data=False, ) sql, params = create_mv_nodata.to_sql() +print(f"\nWITH NO DATA SQL: {sql}") backend.execute(sql, params) -# Verify WITH NO DATA - query should fail or return empty +# Confirm it's unqueriable nodata_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], @@ -218,17 +240,150 @@ result = backend.execute(sql, params, options=dql_options) print(f"WITH NO DATA result: {result.data}") except Exception as e: - print(f"WITH NO DATA query error (expected): {e}") + print(f"WITH NO DATA query error (expected): {type(e).__name__}: {e}") # ============================================================ -# SECTION: Drop Materialized View +# SECTION: 4. REFRESH MATERIALIZED VIEW (Normal) # ============================================================ +# Refresh recalculates the query and atomically replaces the MV content. + +# Insert new data so refresh has an effect +insert_new = InsertExpression( + dialect=dialect, + into='sales', + columns=['product_id', 'amount', 'sale_date'], + source=ValuesSource( + dialect, + [[Literal(dialect, '1'), Literal(dialect, '300.00'), Literal(dialect, "'2024-01-05'")]], + ), +) +sql, params = insert_new.to_sql() +backend.execute(sql, params) + +# Refresh sales_summary +refresh_mv = RefreshMaterializedViewExpression( + dialect=dialect, + view_name='sales_summary', +) +sql, params = refresh_mv.to_sql() +print(f"\nREFRESH SQL: {sql}") +backend.execute(sql, params) + +sql, params = verify_query.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("After normal refresh:") +for row in result.data or []: + print(f" {row}") + +# ============================================================ +# SECTION: 5. REFRESH MATERIALIZED VIEW CONCURRENTLY (PG 9.4+) +# ============================================================ +# CONCURRENTLY allows the MV to be read during the refresh but +# requires a UNIQUE index on every row of the materialized view. + +# Create a unique index prerequisite +create_unique_idx = CreateIndexExpression( + dialect=dialect, + index_name='sales_summary_product_id_idx', + table_name='sales_summary', + columns=['product_id'], + unique=True, +) +sql, params = create_unique_idx.to_sql() +backend.execute(sql, params, options=ddl_options) + +# Insert more data +insert_more = InsertExpression( + dialect=dialect, + into='sales', + columns=['product_id', 'amount', 'sale_date'], + source=ValuesSource( + dialect, + [[Literal(dialect, '2'), Literal(dialect, '400.00'), Literal(dialect, "'2024-01-06'")]], + ), +) +sql, params = insert_more.to_sql() +backend.execute(sql, params) + +refresh_concurrent = RefreshMaterializedViewExpression( + dialect=dialect, + view_name='sales_summary', + concurrent=True, +) +sql, params = refresh_concurrent.to_sql() +print(f"\nREFRESH CONCURRENTLY SQL: {sql}") +backend.execute(sql, params) + +sql, params = verify_query.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("After CONCURRENTLY refresh:") +for row in result.data or []: + print(f" {row}") + +# ============================================================ +# SECTION: 6. REFRESH WITH DATA — Populate a NO DATA View +# ============================================================ +# An MV created with WITH NO DATA can be populated later via +# REFRESH ... WITH DATA. + +refresh_with_data = RefreshMaterializedViewExpression( + dialect=dialect, + view_name='sales_daily', + with_data=True, +) +sql, params = refresh_with_data.to_sql() +print(f"\nREFRESH WITH DATA SQL: {sql}") +backend.execute(sql, params) + +# Now the NO-DATA view is queryable +sql, params = nodata_query.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("After REFRESH WITH DATA (sales_daily):") +for row in result.data or []: + print(f" {row}") + +# ============================================================ +# SECTION: 7. Schema-Qualified Refresh (PostgresRefreshMaterializedViewExpression) +# ============================================================ +# The PostgreSQL backend provides an extended refresh expression that +# supports specifying the schema for the materialized view. + +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.mv import ( + PostgresRefreshMaterializedViewExpression, +) + +# Refresh with schema (public is PG's default schema) +pg_refresh = PostgresRefreshMaterializedViewExpression( + dialect=dialect, + name='sales_summary', + schema='public', +) +sql, params = pg_refresh.to_sql() +print(f"\nSchema-qualified REFRESH SQL: {sql}") +backend.execute(sql, params) + +# Also supports backward-compatible aliases: name=, concurrently= +pg_refresh_c = PostgresRefreshMaterializedViewExpression( + dialect=dialect, + name='sales_summary', + concurrently=True, +) +sql, params = pg_refresh_c.to_sql() +print(f"Backward-compat CONCURRENTLY SQL: {sql}") + +# ============================================================ +# SECTION: 8. DROP MATERIALIZED VIEW with CASCADE and IF EXISTS +# ============================================================ +# CASCADE drops any objects that depend on the MV. + drop_mv1 = DropMaterializedViewExpression( dialect=dialect, view_name='sales_summary', if_exists=True, + cascade=True, ) sql, params = drop_mv1.to_sql() +print(f"\nDROP CASCADE SQL: {sql}") backend.execute(sql, params) drop_mv2 = DropMaterializedViewExpression( @@ -239,12 +394,26 @@ sql, params = drop_mv2.to_sql() backend.execute(sql, params) +# ============================================================ +# SECTION: 9. DROP MATERIALIZED VIEW Without IF EXISTS (SQL Generation Demo) +# ============================================================ +# Dropping a non-existent view without IF EXISTS will raise an error +# at runtime. This section shows the SQL generation to avoid that. + +drop_noexist = DropMaterializedViewExpression( + dialect=dialect, + view_name='nonexistent_mv', +) +sql, params = drop_noexist.to_sql() +print(f"DROP without IF EXISTS SQL (not executed): {sql}") +print(" Note: This would fail at runtime if the MV does not exist.") + # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='sales', + table='sales', if_exists=True, cascade=True, ) @@ -257,8 +426,15 @@ # ============================================================ # Key points: # 1. Use CreateMaterializedViewExpression to create MVs -# 2. Use RefreshMaterializedViewExpression to update MV data -# 3. Use with_data=False for WITH NO DATA (lazy population) -# 4. Use DropMaterializedViewExpression to drop MVs -# 5. Requires PostgreSQL 9.3+ -# 6. Use concurrent=True for CONCURRENTLY refresh (PG 9.4+) +# 2. Use column_aliases to name the MV output columns +# 3. Use storage_options for physical storage tuning (fillfactor, autovacuum, etc.) +# 4. Use tablespace to assign the MV to a specific storage location +# 5. Use with_data=False for WITH NO DATA (lazy population, MV unqueriable) +# 6. Use RefreshMaterializedViewExpression for standard refresh +# 7. Use RefreshMaterializedViewExpression with concurrent=True for CONCURRENTLY refresh (PG 9.4+, requires UNIQUE index) +# 8. Use RefreshMaterializedViewExpression with with_data=True to populate a NO-DATA MV +# 9. Use PostgresRefreshMaterializedViewExpression for schema-qualified refresh +# 10. Use DropMaterializedViewExpression with cascade=True to drop dependent objects +# 11. Use DropMaterializedViewExpression with if_exists=True for safe drop +# 12. Requires PostgreSQL 9.3+ for basic MV support +# 13. Requires PostgreSQL 9.4+ for CONCURRENTLY refresh \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py new file mode 100644 index 00000000..5b0583c7 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py @@ -0,0 +1,141 @@ +""" +Schema diff: PostgreSQL array type dimension changes. + +PostgresArrayType.is_equivalent() handles dimension comparison +internally. Column type changes like INTEGER[] → INTEGER[][] +are detected via parsed_data_type equivalence checks. + +Supported versions: PostgreSQL 8.4+ (arrays available since PG 8.4) +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, ArrayType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_v1 = DropTableExpression(dialect, "scores_v1", if_exists=True, cascade=True) +drop_v2 = DropTableExpression(dialect, "scores_v2", if_exists=True, cascade=True) +sql, params = drop_v1.to_sql() +backend.execute(sql, params) +sql, params = drop_v2.to_sql() +backend.execute(sql, params) + +# One-dimensional array column +scores_v1_table = CreateTableExpression( + dialect=dialect, table="scores_v1", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("values", ArrayType(IntegerType(), dimensions=1)), + ] +) +sql, params = scores_v1_table.to_sql() +backend.execute(sql, params) + +# Two-dimensional array column +scores_v2_table = CreateTableExpression( + dialect=dialect, table="scores_v2", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("values", ArrayType(IntegerType(), dimensions=2)), + ] +) +sql, params = scores_v2_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot = builder.build(schema="public") + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +for tbl_name in ("scores_v1", "scores_v2"): + if tbl_name in snapshot.tables: + for col in snapshot.tables[tbl_name].columns: + if col.name == "values": + pt = col.parsed_data_type + dims = pt.dimensions if hasattr(pt, "dimensions") else "N/A" + print(f"{tbl_name}.values: data_type='{col.data_type}', dimensions={dims}") + +# Manually construct two snapshots that differ only in array dimensions +from rhosocial.activerecord.backend.introspection.types import ( # noqa: E402 + TableInfo, ColumnInfo, ColumnNullable, TableType, DatabaseInfo, +) +from rhosocial.activerecord.backend.expression.types import IntegerType, ArrayType # noqa: E402 +from datetime import datetime, timezone # noqa: E402 + +db_info = DatabaseInfo(name="test", version="16", version_tuple=(16, 0, 0), vendor="postgresql") + +# V1: 1-d array +col1d = ColumnInfo( + name="values", table_name="t", ordinal_position=1, + data_type="INTEGER[]", parsed_data_type=ArrayType(IntegerType(), dimensions=1), + nullable=ColumnNullable.NULLABLE, +) +snap1d = snapshot.__class__( + dialect_class=snapshot.dialect_class, + captured_at=datetime.now(tz=timezone.utc), + database_info=db_info, + tables={"t": TableInfo(name="t", columns=[col1d], table_type=TableType.BASE_TABLE)}, +) + +# V2: 2-d array +col2d = ColumnInfo( + name="values", table_name="t", ordinal_position=1, + data_type="INTEGER[][]", parsed_data_type=ArrayType(IntegerType(), dimensions=2), + nullable=ColumnNullable.NULLABLE, +) +snap2d = snapshot.__class__( + dialect_class=snapshot.dialect_class, + captured_at=datetime.now(tz=timezone.utc), + database_info=db_info, + tables={"t": TableInfo(name="t", columns=[col2d], table_type=TableType.BASE_TABLE)}, +) + +differ = PostgresSchemaDiffer() +diff = differ.compare(snap1d, snap2d) +print("\nDiff 1-d vs 2-d array:") +print(f" Modified tables: {diff.modified_tables}") +print(f" Diff is empty: {diff.is_empty}") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_v1.to_sql() +backend.execute(sql, params) +sql, params = drop_v2.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py new file mode 100644 index 00000000..1015e988 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py @@ -0,0 +1,103 @@ +""" +Schema diff: PostgreSQL column order has no semantic meaning. + +Unlike MySQL, PostgreSQL does not assign semantic significance to +column ordinal position. Reordering columns produces no diff because +PostgresSchemaDiffer only compares type, nullability, and default value. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( + AlterTableExpression, AddColumn, DropColumn, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_demo = DropTableExpression(dialect, "demo", if_exists=True, cascade=True) +sql, params = drop_demo.to_sql() +backend.execute(sql, params) + +demo_table = CreateTableExpression( + dialect=dialect, table="demo", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ColumnDefinition("email", VarCharType(200)), + ] +) +sql, params = demo_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot_before = builder.build(schema="public") + +# Add a column between name and email — shifts ordinal positions +# but PostgresSchemaDiffer ignores ordinal_position. +add_age = AlterTableExpression(dialect, "demo", [ + AddColumn(dialect, ColumnDefinition("age", IntegerType())) +]) +sql, params = add_age.to_sql() +backend.execute(sql, params) + +# Now drop it — column set is identical +drop_age = AlterTableExpression(dialect, "demo", [ + DropColumn(dialect, "age") +]) +sql, params = drop_age.to_sql() +backend.execute(sql, params) + +snapshot_after = builder.build(schema="public") + +differ = PostgresSchemaDiffer() +diff = differ.compare(snapshot_before, snapshot_after) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Modified tables: {diff.modified_tables}") +print(f"Diff is empty: {diff.is_empty}") +print("(No diff expected — column set unchanged regardless of position)") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_demo.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py new file mode 100644 index 00000000..ca9c7a29 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py @@ -0,0 +1,103 @@ +""" +Schema diff: PostgreSQL-specific index types (BTREE, GIN, GiST, HASH). + +PostgreSQL supports multiple index access methods. The differ checks +index_type for equivalence, so switching from BTREE to GIN or GiST +will be reported as a removed+added index pair. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + VarCharType, ArrayType, IntegerType, TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_index import ( + CreateIndexExpression, DropIndexExpression, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_demo = DropTableExpression(dialect, "demo", if_exists=True, cascade=True) +sql, params = drop_demo.to_sql() +backend.execute(sql, params) + +demo_table = CreateTableExpression( + dialect=dialect, table="demo", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(200)), + ColumnDefinition("tags", ArrayType(TextType(), dimensions=1)), + ] +) +sql, params = demo_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) + +# Snapshot with BTREE index +idx_btree = CreateIndexExpression(dialect, "idx_name", "demo", ["name"], index_type="BTREE") +sql, params = idx_btree.to_sql() +backend.execute(sql, params) +snap_btree = builder.build(schema="public") + +# Drop BTREE, create GIN index on array column — different index type +drop_idx = DropIndexExpression(dialect, "idx_name", "demo", if_exists=True) +sql, params = drop_idx.to_sql() +backend.execute(sql, params) +idx_gin = CreateIndexExpression(dialect, "idx_tags", "demo", ["tags"], index_type="GIN") +sql, params = idx_gin.to_sql() +backend.execute(sql, params) +snap_gin = builder.build(schema="public") + +differ = PostgresSchemaDiffer() +diff = differ.compare(snap_btree, snap_gin) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Modified tables: {diff.modified_tables}") + +if "demo" in diff.table_diffs: + td = diff.table_diffs["demo"] + print(f" Removed indexes: {[(idx.name, idx.index_type.value) for idx in td.removed_indexes]}") + print(f" Added indexes: {[(idx.name, idx.index_type.value) for idx in td.added_indexes]}") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_demo.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py new file mode 100644 index 00000000..7075b33e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py @@ -0,0 +1,116 @@ +""" +Schema diff: PostgreSQL 9 vs 10+ — SERIAL vs GENERATED AS IDENTITY. + +PostgreSQL 9: SERIAL creates a sequence + DEFAULT nextval(). +PostgreSQL 10+: GENERATED AS IDENTITY is the SQL-standard way. + +When comparing snapshots, a SERIAL column and an IDENTITY column +will appear differently in introspected metadata: +- SERIAL: default_value = 'nextval(''tab_col_seq''::regclass)' +- IDENTITY: is_auto_increment = True, special metadata + +Supported versions: PostgreSQL 9 — SERIAL only. + PostgreSQL 10+ — GENERATED AS IDENTITY available. +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_pg9 = DropTableExpression(dialect, "pg9_style", if_exists=True, cascade=True) +drop_pg10 = DropTableExpression(dialect, "pg10_style", if_exists=True, cascade=True) +sql, params = drop_pg9.to_sql() +backend.execute(sql, params) +sql, params = drop_pg10.to_sql() +backend.execute(sql, params) + +# PostgreSQL 9-compatible: SERIAL shorthand +pg9_table = CreateTableExpression( + dialect=dialect, table="pg9_style", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ] +) +sql, params = pg9_table.to_sql() +backend.execute(sql, params) + +# PostgreSQL 10+: GENERATED AS IDENTITY +# Note: dialect_options={'identity': 'ALWAYS'} triggers GENERATED ALWAYS AS IDENTITY +# in the PostgreSQL dialect's format_column_definition +try: + pg10_table = CreateTableExpression( + dialect=dialect, table="pg10_style", columns=[ + ColumnDefinition("id", IntegerType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)], + dialect_options={"identity": "ALWAYS"}), + ColumnDefinition("name", VarCharType(100)), + ] + ) + sql, params = pg10_table.to_sql() + backend.execute(sql, params) +except Exception as e: + print(f"(GENERATED AS IDENTITY not available on this server: {e})") + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot = builder.build(schema="public") + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +for tbl_name in ("pg9_style", "pg10_style"): + if tbl_name in snapshot.tables: + tbl = snapshot.tables[tbl_name] + for col in tbl.columns: + if col.is_auto_increment or col.is_primary_key: + print(f"{tbl_name}.{col.name}:") + print(f" data_type: {col.data_type}") + print(f" is_auto_increment:{col.is_auto_increment}") + print(f" is_generated: {col.is_generated}") + print(f" default_value: {col.default_value}") + print(f" generated_expr: {col.generated_expression}") + +print() +print("Note: SERIAL vs IDENTITY columns produce different metadata.") +print("When diff-ing PG9 vs PG10+ snapshots, these differences will appear.") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_pg9.to_sql() +backend.execute(sql, params) +sql, params = drop_pg10.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py new file mode 100644 index 00000000..b630b4f0 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py @@ -0,0 +1,127 @@ +""" +Schema diff: snapshot serialization roundtrip across sessions. + +Serialized snapshots can be stored as JSON files and compared +later — useful for CI/CD pipeline schema validation. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import json +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( + AlterTableExpression, AddColumn, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_users = DropTableExpression(dialect, "users", if_exists=True, cascade=True) +sql, params = drop_users.to_sql() +backend.execute(sql, params) + +users_table = CreateTableExpression( + dialect=dialect, table="users", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ColumnDefinition("email", VarCharType(200)), + ] +) +sql, params = users_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SchemaSnapshot, + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot_before = builder.build(schema="public") + +# Serialize to JSON and deserialize back +snapshot_json = json.dumps(snapshot_before.to_dict(), default=str) +snapshot_loaded = SchemaSnapshot.from_dict(json.loads(snapshot_json)) + +# Modify the database +add_phone = AlterTableExpression(dialect, "users", [ + AddColumn(dialect, ColumnDefinition("phone", VarCharType(20))) +]) +sql, params = add_phone.to_sql() +backend.execute(sql, params) + +snapshot_after = builder.build(schema="public") + +# Diff: loaded snapshot (before) vs current (after) +differ = PostgresSchemaDiffer() +diff = differ.compare(snapshot_loaded, snapshot_after) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Snapshot size (JSON): {len(snapshot_json)} chars") +print(f"Roundtrip preserves dialect: {snapshot_loaded.dialect_class == snapshot_before.dialect_class}") +# Verify table data survived the roundtrip +assert snapshot_loaded.dialect_class == snapshot_before.dialect_class +assert list(snapshot_loaded.tables.keys()) == list(snapshot_before.tables.keys()), ( + f"table names mismatch: {list(snapshot_loaded.tables.keys())} " + f"!= {list(snapshot_before.tables.keys())}" +) +for name in snapshot_before.tables: + before_t = snapshot_before.tables[name] + after_t = snapshot_loaded.tables[name] + assert [c.name for c in before_t.columns] == [c.name for c in after_t.columns], ( + f"columns mismatch in table '{name}'" + ) + for col_b, col_a in zip(before_t.columns, after_t.columns): + assert col_b.data_type.__class__.__name__ == col_a.data_type.__class__.__name__, ( + f"type mismatch in {name}.{col_b.name}" + ) +print(f"Table data roundtrip: all columns and types match") +print(f"Added tables: {diff.added_tables}") +print(f"Removed tables: {diff.removed_tables}") +print(f"Modified tables: {diff.modified_tables}") + +if "users" in diff.table_diffs: + td = diff.table_diffs["users"] + db_info = snapshot_after.database_info + print(f"Database version: {db_info.version} (vendor={db_info.vendor})") + for cd in td.column_diffs: + print(f" Column '{cd.column_name}': added") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_users.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py new file mode 100644 index 00000000..e9665e0b --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py @@ -0,0 +1,109 @@ +""" +Schema diff: basic table add/remove detection in PostgreSQL. + +PostgreSQL uses multi-level namespaces (schema.table), and the +PostgresSchemaDiffer treats column order as semantically irrelevant. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, TableConstraint, TableConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, DecimalType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_users = DropTableExpression(dialect, "users", if_exists=True, cascade=True) +drop_orders = DropTableExpression(dialect, "orders", if_exists=True, cascade=True) +sql, params = drop_users.to_sql() +backend.execute(sql, params) +sql, params = drop_orders.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot_before = builder.build(schema="public") + +users_table = CreateTableExpression( + dialect=dialect, table="users", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.NOT_NULL)]), + ] +) +sql, params = users_table.to_sql() +backend.execute(sql, params) + +orders_table = CreateTableExpression( + dialect=dialect, table="orders", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType()), + ColumnDefinition("amount", DecimalType(10, 2)), + ], + table_constraints=[ + TableConstraint( + constraint_type=TableConstraintType.FOREIGN_KEY, + columns=["user_id"], + foreign_key_table="users", + foreign_key_columns=["id"], + ) + ] +) +sql, params = orders_table.to_sql() +backend.execute(sql, params) + +snapshot_after = builder.build(schema="public") + +differ = PostgresSchemaDiffer() +diff = differ.compare(snapshot_before, snapshot_after) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Added tables: {diff.added_tables}") +print(f"Removed tables: {diff.removed_tables}") +print(f"Modified tables:{diff.modified_tables}") +print(f"Diff is empty: {diff.is_empty}") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_users.to_sql() +backend.execute(sql, params) +sql, params = drop_orders.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py index 9de671dd..14200826 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py @@ -47,6 +47,58 @@ PostgresPgPartmanDeleteConfigExpression, ) +# DataType subclasses for DDL +from .types import ( + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCharacterVaryingType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, +) + __all__ = [ # advisory "PostgresAdvisoryLockExpression", @@ -82,4 +134,53 @@ "PostgresPgPartmanRunMaintenanceExpression", "PostgresPgPartmanUpdateConfigExpression", "PostgresPgPartmanDeleteConfigExpression", + # DataType subclasses for DDL + "PostgresBigSerialType", + "PostgresBitType", + "PostgresBoxType", + "PostgresByteaType", + "PostgresCharacterVaryingType", + "PostgresCIDType", + "PostgresCircleType", + "PostgresDateMultirangeType", + "PostgresDateRangeType", + "PostgresGeographyType", + "PostgresGeometryType", + "PostgresHstoreType", + "PostgresInetType", + "PostgresInt4MultirangeType", + "PostgresInt4RangeType", + "PostgresInt8MultirangeType", + "PostgresInt8RangeType", + "PostgresJsonPathType", + "PostgresLineSegmentType", + "PostgresLineType", + "PostgresMacAddr8Type", + "PostgresMacAddrType", + "PostgresMoneyType", + "PostgresNumMultirangeType", + "PostgresNumRangeType", + "PostgresOIDType", + "PostgresPathType", + "PostgresPgLSNType", + "PostgresPointType", + "PostgresPolygonType", + "PostgresRegClassType", + "PostgresRegTypeType", + "PostgresSerialType", + "PostgresSmallSerialType", + "PostgresTIDType", + "PostgresTSQueryType", + "PostgresTSVectorType", + "PostgresTsMultirangeType", + "PostgresTsRangeType", + "PostgresTsTzMultirangeType", + "PostgresTsTzRangeType", + "PostgresUUIDType", + "PostgresVarBitType", + "PostgresVectorType", + "PostgresXID8Type", + "PostgresXIDType", + "PostgresXMLType", + "PostgresCidrType", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py new file mode 100644 index 00000000..5a1508fd --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -0,0 +1,391 @@ +# src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +"""PostgreSQL-specific DataType subclasses. + +Naming convention +----------------- +PostgreSQL-specific types use the ``Postgres`` prefix to distinguish them +from the core types (which have no prefix). This avoids ambiguity when both +core and backend types are used together. + +Usage scope +----------- +These types are used **only** for PostgreSQL backend DDL column definitions, +introspection result parsing, and schema comparison. They should **not** +be used by application code directly — always use the core types for +DDL definition expressions (``ColumnDefinition.data_type``). +""" + +from __future__ import annotations + +from typing import Optional, Set + +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + BlobType, + DataType, + IntegerType, + SmallIntType, + BigIntType, + VarCharType, +) + + +# --------------------------------------------------------------------------- +# Character varying alias +# --------------------------------------------------------------------------- + +class PostgresCharacterVaryingType(VarCharType, backend="postgres"): + """PostgreSQL ``CHARACTER VARYING(n)`` — alias for ``VARCHAR(n)``.""" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'VarCharType'} + + +# --------------------------------------------------------------------------- +# Binary data: BYTEA +# --------------------------------------------------------------------------- + +class PostgresByteaType(BlobType, backend="postgres"): + """PostgreSQL ``BYTEA`` — variable-length binary string.""" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'BlobType'} + + +# --------------------------------------------------------------------------- +# Serial (auto-increment) types +# --------------------------------------------------------------------------- + +class PostgresSmallSerialType(DataType, backend="postgres"): + """PostgreSQL ``SMALLSERIAL`` — auto-incrementing SMALLINT (2 bytes).""" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'SmallSerialType', 'SmallIntType'} + + +class PostgresSerialType(DataType, backend="postgres"): + """PostgreSQL ``SERIAL`` — auto-incrementing INTEGER (4 bytes).""" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'SerialType', 'IntegerType'} + + +class PostgresBigSerialType(DataType, backend="postgres"): + """PostgreSQL ``BIGSERIAL`` — auto-incrementing BIGINT (8 bytes).""" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'BigSerialType', 'BigIntType'} + + +# --------------------------------------------------------------------------- +# UUID +# --------------------------------------------------------------------------- + +class PostgresUUIDType(DataType, backend="postgres"): + """PostgreSQL ``UUID`` — universally unique identifier.""" + + +# --------------------------------------------------------------------------- +# XML +# --------------------------------------------------------------------------- + +class PostgresXMLType(DataType, backend="postgres"): + """PostgreSQL ``XML`` — XML data type.""" + + +# --------------------------------------------------------------------------- +# Text search +# --------------------------------------------------------------------------- + +class PostgresTSVectorType(DataType, backend="postgres"): + """PostgreSQL ``TSVECTOR`` — text search document.""" + + +class PostgresTSQueryType(DataType, backend="postgres"): + """PostgreSQL ``TSQUERY`` — text search query.""" + + +# --------------------------------------------------------------------------- +# JSON path +# --------------------------------------------------------------------------- + +class PostgresJsonPathType(DataType, backend="postgres"): + """PostgreSQL ``JSONPATH`` — SQL/JSON path expression (PG 12+).""" + + +# --------------------------------------------------------------------------- +# Bit string types +# --------------------------------------------------------------------------- + +class PostgresBitType(DataType, backend="postgres"): + """PostgreSQL ``BIT(n)`` — fixed-length bit string.""" + + n: Optional[int] = None + + def __init__(self, n: Optional[int] = None): + super().__init__() + self.n = n + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.n == other.n + + def __hash__(self) -> int: + return hash((type(self), self.n)) + + +class PostgresVarBitType(DataType, backend="postgres"): + """PostgreSQL ``VARBIT(n)`` — variable-length bit string.""" + + n: Optional[int] = None + + def __init__(self, n: Optional[int] = None): + super().__init__() + self.n = n + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.n == other.n + + def __hash__(self) -> int: + return hash((type(self), self.n)) + + +# --------------------------------------------------------------------------- +# Network address types +# --------------------------------------------------------------------------- + +class PostgresInetType(DataType, backend="postgres"): + """PostgreSQL ``INET`` — IPv4 or IPv6 address.""" + + +class PostgresCidrType(DataType, backend="postgres"): + """PostgreSQL ``CIDR`` — IPv4 or IPv6 network.""" + + +class PostgresMacAddrType(DataType, backend="postgres"): + """PostgreSQL ``MACADDR`` — MAC address (EUI-48).""" + + +class PostgresMacAddr8Type(DataType, backend="postgres"): + """PostgreSQL ``MACADDR8`` — MAC address (EUI-64, PG 10+).""" + + +# --------------------------------------------------------------------------- +# Geometric types +# --------------------------------------------------------------------------- + +class PostgresPointType(DataType, backend="postgres"): + """PostgreSQL ``POINT`` — geometric point (x, y).""" + + +class PostgresLineType(DataType, backend="postgres"): + """PostgreSQL ``LINE`` — infinite line.""" + + +class PostgresLineSegmentType(DataType, backend="postgres"): + """PostgreSQL ``LSEG`` — line segment.""" + + +class PostgresBoxType(DataType, backend="postgres"): + """PostgreSQL ``BOX`` — rectangular box.""" + + +class PostgresPathType(DataType, backend="postgres"): + """PostgreSQL ``PATH`` — open or closed geometric path.""" + + +class PostgresPolygonType(DataType, backend="postgres"): + """PostgreSQL ``POLYGON`` — closed geometric polygon.""" + + +class PostgresCircleType(DataType, backend="postgres"): + """PostgreSQL ``CIRCLE`` — circle (center + radius).""" + + +# --------------------------------------------------------------------------- +# Monetary type +# --------------------------------------------------------------------------- + +class PostgresMoneyType(DataType, backend="postgres"): + """PostgreSQL ``MONEY`` — currency amount.""" + + +# --------------------------------------------------------------------------- +# Range types +# --------------------------------------------------------------------------- + +class PostgresInt4RangeType(DataType, backend="postgres"): + """PostgreSQL ``INT4RANGE`` — range of integer.""" + + +class PostgresInt8RangeType(DataType, backend="postgres"): + """PostgreSQL ``INT8RANGE`` — range of bigint.""" + + +class PostgresNumRangeType(DataType, backend="postgres"): + """PostgreSQL ``NUMRANGE`` — range of numeric.""" + + +class PostgresTsRangeType(DataType, backend="postgres"): + """PostgreSQL ``TSRANGE`` — range of timestamp without time zone.""" + + +class PostgresTsTzRangeType(DataType, backend="postgres"): + """PostgreSQL ``TSTZRANGE`` — range of timestamp with time zone.""" + + +class PostgresDateRangeType(DataType, backend="postgres"): + """PostgreSQL ``DATERANGE`` — range of date.""" + + +# --------------------------------------------------------------------------- +# Multirange types (PG 14+) +# --------------------------------------------------------------------------- + +class PostgresInt4MultirangeType(DataType, backend="postgres"): + """PostgreSQL ``INT4MULTIRANGE`` — multirange of integer (PG 14+).""" + + +class PostgresInt8MultirangeType(DataType, backend="postgres"): + """PostgreSQL ``INT8MULTIRANGE`` — multirange of bigint (PG 14+).""" + + +class PostgresNumMultirangeType(DataType, backend="postgres"): + """PostgreSQL ``NUMMULTIRANGE`` — multirange of numeric (PG 14+).""" + + +class PostgresTsMultirangeType(DataType, backend="postgres"): + """PostgreSQL ``TSMULTIRANGE`` — multirange of timestamp (PG 14+).""" + + +class PostgresTsTzMultirangeType(DataType, backend="postgres"): + """PostgreSQL ``TSTZMULTIRANGE`` — multirange of timestamptz (PG 14+).""" + + +class PostgresDateMultirangeType(DataType, backend="postgres"): + """PostgreSQL ``DATEMULTIRANGE`` — multirange of date (PG 14+).""" + + +# --------------------------------------------------------------------------- +# Object identifier types +# --------------------------------------------------------------------------- + +class PostgresOIDType(DataType, backend="postgres"): + """PostgreSQL ``OID`` — object identifier.""" + + +class PostgresRegClassType(DataType, backend="postgres"): + """PostgreSQL ``REGCLASS`` — relation name (OID alias).""" + + +class PostgresRegTypeType(DataType, backend="postgres"): + """PostgreSQL ``REGTYPE`` — type name (OID alias).""" + + +class PostgresXIDType(DataType, backend="postgres"): + """PostgreSQL ``XID`` — transaction ID.""" + + +class PostgresXID8Type(DataType, backend="postgres"): + """PostgreSQL ``XID8`` — 64-bit transaction ID (PG 13+).""" + + +class PostgresCIDType(DataType, backend="postgres"): + """PostgreSQL ``CID`` — command ID.""" + + +class PostgresTIDType(DataType, backend="postgres"): + """PostgreSQL ``TID`` — tuple ID (page, tuple).""" + + +# --------------------------------------------------------------------------- +# pg_lsn type +# --------------------------------------------------------------------------- + +class PostgresPgLSNType(DataType, backend="postgres"): + """PostgreSQL ``PG_LSN`` — WAL log sequence number.""" + + +# --------------------------------------------------------------------------- +# Extension-provided types (minimal DataType wrappers) +# --------------------------------------------------------------------------- + +class PostgresHstoreType(DataType, backend="postgres"): + """PostgreSQL ``HSTORE`` — key-value store (hstore extension).""" + + +class PostgresGeometryType(DataType, backend="postgres"): + """PostGIS ``GEOMETRY`` — generic spatial geometry (PostGIS extension). + + For production use, subclass with SRID support as needed. + """ + + +class PostgresGeographyType(DataType, backend="postgres"): + """PostGIS ``GEOGRAPHY`` — geodetic spatial type (PostGIS extension).""" + + +class PostgresCitextType(DataType, backend="postgres"): + """PostgreSQL ``CITEXT`` — case-insensitive text (citext extension).""" + + +class PostgresCubeType(DataType, backend="postgres"): + """PostgreSQL ``CUBE`` — multi-dimensional cube (cube extension).""" + + +class PostgresLtreeType(DataType, backend="postgres"): + """PostgreSQL ``LTREE`` — label tree (ltree extension).""" + + +class PostgresRasterType(DataType, backend="postgres"): + """PostgreSQL ``RASTER`` — raster (PostGIS raster extension).""" + + +class PostgresVectorType(DataType, backend="postgres"): + """pgvector ``VECTOR(n)`` — vector embedding (pgvector extension). + + Args: + dim: Number of dimensions. + """ + + dim: int + + def __init__(self, dim: int): + super().__init__() + self.dim = dim + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.dim == other.dim + + def __hash__(self) -> int: + return hash((type(self), self.dim)) + + +# --------------------------------------------------------------------------- +# Array container +# --------------------------------------------------------------------------- + +class PostgresArrayType(ArrayType): + """PostgreSQL array type. + + PostgreSQL normalises all multi-dimensional array declarations to a + single-dimensional internal representation at the storage level. + Therefore ``integer[][]`` and ``integer[]`` are considered equivalent + during schema comparison: ``is_equivalent`` intentionally ignores + ``dimensions``. + """ + + def is_equivalent(self, other: "DataType") -> bool: + if not isinstance(other, ArrayType): + return False + return self.element_type.is_equivalent(other.element_type) \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py b/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py index c4504f73..3ba187d1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py @@ -50,6 +50,7 @@ TriggerInfo, ViewInfo, ) +from rhosocial.activerecord.backend.expression.types import DataType from .status_introspector import ( SyncPostgreSQLStatusIntrospector, AsyncPostgreSQLStatusIntrospector, @@ -137,6 +138,7 @@ def _parse_columns(self, rows: List[Dict[str, Any]], table_name: str, schema: st data_type = row.get("data_type", "") # Extract base type (remove array brackets, size specifiers) base_type = data_type.split("[")[0].split("(")[0].strip() + parsed = DataType.parse_data_type_str(self._backend.dialect, data_type) columns.append( ColumnInfo( name=row["column_name"], @@ -145,6 +147,7 @@ def _parse_columns(self, rows: List[Dict[str, Any]], table_name: str, schema: st ordinal_position=row["ordinal_position"], data_type=base_type, data_type_full=data_type, + parsed_data_type=parsed, nullable=ColumnNullable.NULLABLE if row.get("is_nullable") else ColumnNullable.NOT_NULL, default_value=row.get("default_value"), is_primary_key=row.get("is_primary_key", False), diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py index 232e1805..77981ee9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py @@ -38,6 +38,7 @@ from .types.range_type import PostgresRangeTypeMixin from .types.jsonb_enhanced import PostgresJSONBEnhancedMixin from .types.array_enhanced import PostgresArrayEnhancedMixin +from .types.data_type_formatting import PostgresTypeFormatSupportMixin # Extension mixins from .extensions.pgvector import PostgresPgvectorMixin @@ -129,6 +130,7 @@ "PostgresRangeTypeMixin", "PostgresJSONBEnhancedMixin", "PostgresArrayEnhancedMixin", + "PostgresTypeFormatSupportMixin", # Extension mixins "PostgresPgvectorMixin", "PostgresPostGISMixin", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py index 77e7c10f..66290066 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py @@ -40,13 +40,20 @@ def format_create_table_like(self, expr) -> tuple: def format_column_definition(self, col_def) -> Tuple[str, tuple]: from rhosocial.activerecord.backend.dialect.base import SQLDialectBase all_params: List[Any] = [] - data_type = col_def.data_type - if not re.fullmatch(r"[A-Za-z0-9\s(),\[\]]+", data_type): + type_sql, _ = col_def.data_type.to_sql(self) + if not re.fullmatch(r"[A-Za-z0-9\s(),\[\]]+", type_sql): raise ValueError( - f"Invalid data type '{data_type}': " + f"Invalid data type '{type_sql}': " "must contain only alphanumeric characters, spaces, parentheses, commas, and brackets." ) - col_sql = f"{self.format_identifier(col_def.name)} {data_type}" + col_sql = f"{self.format_identifier(col_def.name)} {type_sql}" + dialect_opts = col_def.dialect_options or {} + identity = dialect_opts.get("identity") + if identity: + if identity.upper() in ("ALWAYS", "BY DEFAULT"): + col_sql += f" GENERATED {identity.upper()} AS IDENTITY" + else: + raise ValueError(f"Invalid identity option '{identity}': must be 'ALWAYS' or 'BY DEFAULT'") for constraint in col_def.constraints: suffix, params = self.format_column_constraint(constraint) col_sql += suffix diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py index a223f6b2..0d877803 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py @@ -8,6 +8,7 @@ from .range_type import PostgresRangeTypeMixin from .jsonb_enhanced import PostgresJSONBEnhancedMixin from .array_enhanced import PostgresArrayEnhancedMixin +from .data_type_formatting import PostgresTypeFormatSupportMixin __all__ = [ "EnumTypeMixin", @@ -17,4 +18,5 @@ "PostgresRangeTypeMixin", "PostgresJSONBEnhancedMixin", "PostgresArrayEnhancedMixin", + "PostgresTypeFormatSupportMixin", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py new file mode 100644 index 00000000..bb41a98e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -0,0 +1,821 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +"""PostgreSQL DataType formatting and parsing mixin.""" + +from __future__ import annotations + +import re +from typing import Tuple, Tuple, TYPE_CHECKING + +from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin +from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + BigIntType, + BlobType, + BooleanType, + CharType, + DataType, + DateType, + DateTimeType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + IntType, + IntervalType, + JsonBType, + JsonType, + RealType, + SmallIntType, + TextType, + TimeType, + TimeTzType, + TimestampType, + TimestampTzType, + TinyIntType, + VarCharType, +) +from ...expression.types import ( + PostgresArrayType, + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, + PostgresCharacterVaryingType, + PostgresCitextType, + PostgresCubeType, + PostgresLtreeType, + PostgresRasterType, +) + +if TYPE_CHECKING: + pass + + +class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): + """PostgreSQL DataType formatting and parsing. + + Implements ``DDLTypeSupport`` so the dialect can render ``DataType`` + expressions to SQL strings and parse raw SQL type strings back into + ``DataType`` instances. + """ + + # ------------------------------------------------------------------ + # DDLTypeSupport — formatting + # ------------------------------------------------------------------ + + # --- PostgreSQL-specific formatters --- + + @DDLTypeMixin.handles(PostgresByteaType) + def format_data_type_bytea(self, data_type) -> Tuple[str, tuple]: + return "BYTEA", () + + @DDLTypeMixin.handles(PostgresSmallSerialType) + def format_data_type_small_serial(self, data_type) -> Tuple[str, tuple]: + return "SMALLSERIAL", () + + @DDLTypeMixin.handles(PostgresSerialType) + def format_data_type_serial(self, data_type) -> Tuple[str, tuple]: + return "SERIAL", () + + @DDLTypeMixin.handles(PostgresBigSerialType) + def format_data_type_big_serial(self, data_type) -> Tuple[str, tuple]: + return "BIGSERIAL", () + + @DDLTypeMixin.handles(PostgresUUIDType) + def format_data_type_uuid(self, data_type) -> Tuple[str, tuple]: + return "UUID", () + + @DDLTypeMixin.handles(PostgresXMLType) + def format_data_type_xml(self, data_type) -> Tuple[str, tuple]: + return "XML", () + + @DDLTypeMixin.handles(PostgresCharacterVaryingType) + def format_data_type_character_varying(self, data_type: PostgresCharacterVaryingType) -> Tuple[str, tuple]: + if data_type.length is not None: + return f"CHARACTER VARYING({data_type.length})", () + return "CHARACTER VARYING", () + + @DDLTypeMixin.handles(PostgresTSVectorType) + def format_data_type_ts_vector(self, data_type) -> Tuple[str, tuple]: + return "TSVECTOR", () + + @DDLTypeMixin.handles(PostgresTSQueryType) + def format_data_type_ts_query(self, data_type) -> Tuple[str, tuple]: + return "TSQUERY", () + + @DDLTypeMixin.handles(PostgresJsonPathType) + def format_data_type_json_path(self, data_type) -> Tuple[str, tuple]: + return "JSONPATH", () + + @DDLTypeMixin.handles(PostgresBitType) + def format_data_type_bit(self, data_type) -> Tuple[str, tuple]: + if data_type.n is not None: + return f"BIT({data_type.n})", () + return "BIT", () + + @DDLTypeMixin.handles(PostgresVarBitType) + def format_data_type_var_bit(self, data_type) -> Tuple[str, tuple]: + if data_type.n is not None: + return f"VARBIT({data_type.n})", () + return "VARBIT", () + + @DDLTypeMixin.handles(PostgresInetType) + def format_data_type_inet(self, data_type) -> Tuple[str, tuple]: + return "INET", () + + @DDLTypeMixin.handles(PostgresCidrType) + def format_data_type_cidr(self, data_type) -> Tuple[str, tuple]: + return "CIDR", () + + @DDLTypeMixin.handles(PostgresMacAddrType) + def format_data_type_mac_addr(self, data_type) -> Tuple[str, tuple]: + return "MACADDR", () + + @DDLTypeMixin.handles(PostgresMacAddr8Type) + def format_data_type_mac_addr8(self, data_type) -> Tuple[str, tuple]: + return "MACADDR8", () + + @DDLTypeMixin.handles(PostgresPointType) + def format_data_type_point(self, data_type) -> Tuple[str, tuple]: + return "POINT", () + + @DDLTypeMixin.handles(PostgresLineType) + def format_data_type_line(self, data_type) -> Tuple[str, tuple]: + return "LINE", () + + @DDLTypeMixin.handles(PostgresLineSegmentType) + def format_data_type_line_segment(self, data_type) -> Tuple[str, tuple]: + return "LSEG", () + + @DDLTypeMixin.handles(PostgresBoxType) + def format_data_type_box(self, data_type) -> Tuple[str, tuple]: + return "BOX", () + + @DDLTypeMixin.handles(PostgresPathType) + def format_data_type_path(self, data_type) -> Tuple[str, tuple]: + return "PATH", () + + @DDLTypeMixin.handles(PostgresPolygonType) + def format_data_type_polygon(self, data_type) -> Tuple[str, tuple]: + return "POLYGON", () + + @DDLTypeMixin.handles(PostgresCircleType) + def format_data_type_circle(self, data_type) -> Tuple[str, tuple]: + return "CIRCLE", () + + @DDLTypeMixin.handles(PostgresMoneyType) + def format_data_type_money(self, data_type) -> Tuple[str, tuple]: + return "MONEY", () + + @DDLTypeMixin.handles(PostgresInt4RangeType) + def format_data_type_int4_range(self, data_type) -> Tuple[str, tuple]: + return "INT4RANGE", () + + @DDLTypeMixin.handles(PostgresInt8RangeType) + def format_data_type_int8_range(self, data_type) -> Tuple[str, tuple]: + return "INT8RANGE", () + + @DDLTypeMixin.handles(PostgresNumRangeType) + def format_data_type_num_range(self, data_type) -> Tuple[str, tuple]: + return "NUMRANGE", () + + @DDLTypeMixin.handles(PostgresTsRangeType) + def format_data_type_ts_range(self, data_type) -> Tuple[str, tuple]: + return "TSRANGE", () + + @DDLTypeMixin.handles(PostgresTsTzRangeType) + def format_data_type_ts_tz_range(self, data_type) -> Tuple[str, tuple]: + return "TSTZRANGE", () + + @DDLTypeMixin.handles(PostgresDateRangeType) + def format_data_type_date_range(self, data_type) -> Tuple[str, tuple]: + return "DATERANGE", () + + @DDLTypeMixin.handles(PostgresInt4MultirangeType) + def format_data_type_int4_multirange(self, data_type) -> Tuple[str, tuple]: + return "INT4MULTIRANGE", () + + @DDLTypeMixin.handles(PostgresInt8MultirangeType) + def format_data_type_int8_multirange(self, data_type) -> Tuple[str, tuple]: + return "INT8MULTIRANGE", () + + @DDLTypeMixin.handles(PostgresNumMultirangeType) + def format_data_type_num_multirange(self, data_type) -> Tuple[str, tuple]: + return "NUMMULTIRANGE", () + + @DDLTypeMixin.handles(PostgresTsMultirangeType) + def format_data_type_ts_multirange(self, data_type) -> Tuple[str, tuple]: + return "TSMULTIRANGE", () + + @DDLTypeMixin.handles(PostgresTsTzMultirangeType) + def format_data_type_ts_tz_multirange(self, data_type) -> Tuple[str, tuple]: + return "TSTZMULTIRANGE", () + + @DDLTypeMixin.handles(PostgresDateMultirangeType) + def format_data_type_date_multirange(self, data_type) -> Tuple[str, tuple]: + return "DATEMULTIRANGE", () + + @DDLTypeMixin.handles(PostgresOIDType) + def format_data_type_oid(self, data_type) -> Tuple[str, tuple]: + return "OID", () + + @DDLTypeMixin.handles(PostgresRegClassType) + def format_data_type_reg_class(self, data_type) -> Tuple[str, tuple]: + return "REGCLASS", () + + @DDLTypeMixin.handles(PostgresRegTypeType) + def format_data_type_reg_type(self, data_type) -> Tuple[str, tuple]: + return "REGTYPE", () + + @DDLTypeMixin.handles(PostgresXIDType) + def format_data_type_xid(self, data_type) -> Tuple[str, tuple]: + return "XID", () + + @DDLTypeMixin.handles(PostgresXID8Type) + def format_data_type_xid8(self, data_type) -> Tuple[str, tuple]: + return "XID8", () + + @DDLTypeMixin.handles(PostgresCIDType) + def format_data_type_cid(self, data_type) -> Tuple[str, tuple]: + return "CID", () + + @DDLTypeMixin.handles(PostgresTIDType) + def format_data_type_tid(self, data_type) -> Tuple[str, tuple]: + return "TID", () + + @DDLTypeMixin.handles(PostgresPgLSNType) + def format_data_type_pg_lsn(self, data_type) -> Tuple[str, tuple]: + return "PG_LSN", () + + @DDLTypeMixin.handles(PostgresHstoreType) + def format_data_type_hstore(self, data_type) -> Tuple[str, tuple]: + return "HSTORE", () + + @DDLTypeMixin.handles(PostgresGeometryType) + def format_data_type_geometry(self, data_type) -> Tuple[str, tuple]: + return "GEOMETRY", () + + @DDLTypeMixin.handles(PostgresGeographyType) + def format_data_type_geography(self, data_type) -> Tuple[str, tuple]: + return "GEOGRAPHY", () + + @DDLTypeMixin.handles(PostgresVectorType) + def format_data_type_vector(self, data_type) -> Tuple[str, tuple]: + return f"VECTOR({data_type.dim})", () + + @DDLTypeMixin.handles(PostgresCitextType) + def format_data_type_citext(self, data_type) -> Tuple[str, tuple]: + return "CITEXT", () + + @DDLTypeMixin.handles(PostgresCubeType) + def format_data_type_cube(self, data_type) -> Tuple[str, tuple]: + return "CUBE", () + + @DDLTypeMixin.handles(PostgresLtreeType) + def format_data_type_ltree(self, data_type) -> Tuple[str, tuple]: + return "LTREE", () + + @DDLTypeMixin.handles(PostgresRasterType) + def format_data_type_raster(self, data_type) -> Tuple[str, tuple]: + return "RASTER", () + + @DDLTypeMixin.handles(ArrayType) + def format_data_type_array(self, data_type: ArrayType) -> Tuple[str, tuple]: + elem = data_type.element_type + elem_cls = type(elem) + if elem_cls not in self._type_formatters: + raise TypeError( + f"ArrayType element type {elem_cls.__name__!r} is not supported " + f"by {type(self).__name__}. Use a PostgreSQL-specific DataType " + f"as the element type (e.g. PostgresIntegerType)." + ) + element_sql, _ = self.format_data_type(elem) + return element_sql + "[]" * data_type.dimensions, () + + # --- Core formatters (PostgreSQL specialized) --- + + @DDLTypeMixin.handles(TinyIntType) + def format_data_type_tiny_int(self, data_type: TinyIntType) -> Tuple[str, tuple]: + return "SMALLINT", () + + @DDLTypeMixin.handles(SmallIntType) + def format_data_type_small_int(self, data_type: SmallIntType) -> Tuple[str, tuple]: + return "SMALLINT", () + + @DDLTypeMixin.handles(IntType) + def format_data_type_int(self, data_type: IntType) -> Tuple[str, tuple]: + return "INTEGER", () + + @DDLTypeMixin.handles(IntegerType) + def format_data_type_integer(self, data_type: IntegerType) -> Tuple[str, tuple]: + return "INTEGER", () + + @DDLTypeMixin.handles(BigIntType) + def format_data_type_big_int(self, data_type: BigIntType) -> Tuple[str, tuple]: + return "BIGINT", () + + @DDLTypeMixin.handles(FloatType) + def format_data_type_float(self, data_type: FloatType) -> Tuple[str, tuple]: + if data_type.precision is not None: + return f"FLOAT({data_type.precision})", () + return "REAL", () + + @DDLTypeMixin.handles(RealType) + def format_data_type_real(self, data_type: RealType) -> Tuple[str, tuple]: + return "REAL", () + + @DDLTypeMixin.handles(DoubleType) + def format_data_type_double(self, data_type: DoubleType) -> Tuple[str, tuple]: + return "DOUBLE PRECISION", () + + @DDLTypeMixin.handles(DecimalType) + def format_data_type_decimal(self, data_type: DecimalType) -> Tuple[str, tuple]: + if data_type.precision is not None and data_type.scale is not None: + return f"DECIMAL({data_type.precision},{data_type.scale})", () + if data_type.precision is not None: + return f"DECIMAL({data_type.precision})", () + return "DECIMAL", () + + @DDLTypeMixin.handles(CharType) + def format_data_type_char(self, data_type: CharType) -> Tuple[str, tuple]: + if data_type.length is not None: + return f"CHAR({data_type.length})", () + return "CHAR", () + + @DDLTypeMixin.handles(VarCharType) + def format_data_type_var_char(self, data_type: VarCharType) -> Tuple[str, tuple]: + if data_type.length is not None: + return f"VARCHAR({data_type.length})", () + return "VARCHAR", () + + @DDLTypeMixin.handles(TextType) + def format_data_type_text(self, data_type: TextType) -> Tuple[str, tuple]: + return "TEXT", () + + @DDLTypeMixin.handles(BooleanType) + def format_data_type_boolean(self, data_type: BooleanType) -> Tuple[str, tuple]: + return "BOOLEAN", () + + @DDLTypeMixin.handles(BlobType) + def format_data_type_blob(self, data_type: BlobType) -> Tuple[str, tuple]: + return "BYTEA", () + + @DDLTypeMixin.handles(DateType) + def format_data_type_date(self, data_type: DateType) -> Tuple[str, tuple]: + return "DATE", () + + @DDLTypeMixin.handles(TimeType) + def format_data_type_time(self, data_type: TimeType) -> Tuple[str, tuple]: + if data_type.precision is not None: + return f"TIME({data_type.precision})", () + return "TIME", () + + @DDLTypeMixin.handles(TimeTzType) + def format_data_type_time_tz(self, data_type: TimeTzType) -> Tuple[str, tuple]: + if data_type.precision is not None: + return f"TIME({data_type.precision}) WITH TIME ZONE", () + return "TIME WITH TIME ZONE", () + + @DDLTypeMixin.handles(DateTimeType) + def format_data_type_date_time(self, data_type: DateTimeType) -> Tuple[str, tuple]: + return "TIMESTAMP", () + + @DDLTypeMixin.handles(TimestampType) + def format_data_type_timestamp(self, data_type: TimestampType) -> Tuple[str, tuple]: + if data_type.precision is not None: + return f"TIMESTAMP({data_type.precision})", () + return "TIMESTAMP", () + + @DDLTypeMixin.handles(TimestampTzType) + def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> Tuple[str, tuple]: + if data_type.precision is not None: + return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE", () + return "TIMESTAMP WITH TIME ZONE", () + + @DDLTypeMixin.handles(IntervalType) + def format_data_type_interval(self, data_type: IntervalType) -> Tuple[str, tuple]: + if data_type.fields is not None: + return f"INTERVAL {data_type.fields}", () + return "INTERVAL", () + + @DDLTypeMixin.handles(JsonType) + def format_data_type_json(self, data_type: JsonType) -> Tuple[str, tuple]: + return "JSON", () + + @DDLTypeMixin.handles(JsonBType) + def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: + return "JSONB", () + + # ------------------------------------------------------------------ + # DDLTypeSupport — parsing + # ------------------------------------------------------------------ + + _PG_INTEGER_TYPES = re.compile( + r"^(?:SMALLINT|INT2|INT|INTEGER|INT4|BIGINT|INT8)\b", + re.IGNORECASE, + ) + _PG_SERIAL_TYPES = re.compile( + r"^(?:SMALLSERIAL|SERIAL2|SERIAL|SERIAL4|BIGSERIAL|SERIAL8)\b", + re.IGNORECASE, + ) + _PG_FLOAT_TYPES = re.compile( + r"^(?:REAL|FLOAT4|FLOAT|DOUBLE)\b", + re.IGNORECASE, + ) + _PG_DECIMAL_TYPES = re.compile( + r"^(?:DECIMAL|NUMERIC)\b", + re.IGNORECASE, + ) + _PG_STRING_TYPES = re.compile( + r"^(?:CHAR|VARCHAR|TEXT)\b", + re.IGNORECASE, + ) + _PG_BINARY_TYPES = re.compile( + r"^(?:BYTEA|BLOB)\b", + re.IGNORECASE, + ) + _PG_DATE_TYPES = re.compile( + r"^(?:DATE|DATETIME|TIMESTAMP|TIME|INTERVAL)\b", + re.IGNORECASE, + ) + _PG_JSON_TYPES = re.compile( + r"^(?:JSON|JSONB|JSONPATH)\b", + re.IGNORECASE, + ) + _PG_UUID_TYPES = re.compile( + r"^(?:UUID)\b", + re.IGNORECASE, + ) + _PG_NET_TYPES = re.compile( + r"^(?:INET|CIDR|MACADDR|MACADDR8)\b", + re.IGNORECASE, + ) + _PG_GEOM_TYPES = re.compile( + r"^(?:POINT|LINE|LSEG|BOX|PATH|POLYGON|CIRCLE)\b", + re.IGNORECASE, + ) + _PG_BIT_TYPES = re.compile( + r"^(?:BIT|VARBIT)\b", + re.IGNORECASE, + ) + _PG_RANGE_TYPES = re.compile( + r"^(?:INT4RANGE|INT8RANGE|NUMRANGE|TSRANGE|TSTZRANGE|DATERANGE)\b", + re.IGNORECASE, + ) + _PG_MULTIRANGE_TYPES = re.compile( + r"^(?:INT4MULTIRANGE|INT8MULTIRANGE|NUMMULTIRANGE|" + r"TSMULTIRANGE|TSTZMULTIRANGE|DATEMULTIRANGE)\b", + re.IGNORECASE, + ) + _PG_OID_TYPES = re.compile( + r"^(?:OID|REGCLASS|REGTYPE|XID|XID8|CID|TID)\b", + re.IGNORECASE, + ) + _PG_TS_TYPES = re.compile( + r"^(?:TSVECTOR|TSQUERY)\b", + re.IGNORECASE, + ) + _PG_MISC_TYPES = re.compile( + r"^(?:MONEY|XML|PG_LSN|HSTORE|GEOMETRY|GEOGRAPHY)\b", + re.IGNORECASE, + ) + _PG_ARRAY_SUFFIX = re.compile(r"(\[(\d*)\])+\s*$") + _PG_ARRAY_KEYWORD = re.compile(r"\s+ARRAY(?:\s*\[\s*(\d+)\s*\])?\s*$", re.IGNORECASE) + + def parse_type(self, raw: str) -> DataType: + stripped = raw.strip() + upper = stripped.upper() + + # ---- Array detection (must run before any type-specific match) ---- + + # Check for ARRAY keyword: e.g. "INTEGER ARRAY" or "INTEGER ARRAY[3]" + kw_match = self._PG_ARRAY_KEYWORD.search(upper) + if kw_match: + base_raw = stripped[:kw_match.start()] + element_type = self.parse_type(base_raw) + # PG normalises all array declarations to 1-D internally + return PostgresArrayType(element_type, dimensions=1) + + # Check for array bracket suffix: e.g. "INTEGER[]", "INTEGER[][]" + remaining = stripped + while remaining.endswith("[]"): + remaining = remaining[:-2] + while remaining.endswith("]"): + m = self._PG_ARRAY_SUFFIX.search(remaining) + if m: + remaining = remaining[:m.start()] + else: + break + + if remaining != stripped: + element_type = self.parse_type(remaining) + # PG normalises all array declarations to 1-D internally + return PostgresArrayType(element_type, dimensions=1) + + # ---- Standard type dispatch ---- + + # Serial family + if self._PG_SERIAL_TYPES.match(upper): + from ..expression.types import ( + PostgresBigSerialType, + PostgresSerialType, + PostgresSmallSerialType, + ) + if upper.startswith("BIGSERIAL") or upper.startswith("SERIAL8"): + return PostgresBigSerialType() + if upper.startswith("SMALLSERIAL") or upper.startswith("SERIAL2"): + return PostgresSmallSerialType() + return PostgresSerialType() + + # Integer family + if self._PG_INTEGER_TYPES.match(upper): + if upper.startswith("SMALLINT") or upper.startswith("INT2"): + return SmallIntType() + if upper.startswith("BIGINT") or upper.startswith("INT8"): + return BigIntType() + return IntegerType() + + # Float family + if self._PG_FLOAT_TYPES.match(upper): + if upper.startswith("DOUBLE"): + return DoubleType() + if upper.startswith("REAL") or upper.startswith("FLOAT4"): + return RealType() + nums = re.findall(r"\d+", stripped) + precision = int(nums[0]) if nums else None + return FloatType(precision) + + # Decimal family + if self._PG_DECIMAL_TYPES.match(upper): + nums = re.findall(r"\d+", stripped) + if len(nums) >= 2: + return DecimalType(int(nums[0]), int(nums[1])) + if len(nums) == 1: + return DecimalType(int(nums[0])) + return DecimalType() + + # String family + if self._PG_STRING_TYPES.match(upper): + length_match = re.search(r"\((\d+)\)", stripped) + length = int(length_match.group(1)) if length_match else None + if upper.startswith("VARCHAR") or upper.startswith("CHARACTER VARYING"): + return VarCharType(length) + if upper.startswith("CHAR") or upper.startswith("CHARACTER"): + return CharType(length) + return TextType() + + # Binary + if self._PG_BINARY_TYPES.match(upper): + from ..expression.types import PostgresByteaType + return PostgresByteaType() + + # Date/time + if self._PG_DATE_TYPES.match(upper): + if upper.startswith("DATE"): + return DateType() + if upper.startswith("DATETIME"): + return DateTimeType() + if upper.startswith("TIMESTAMP"): + nums = re.findall(r"\d+", stripped) + precision = int(nums[0]) if nums else None + if "WITH TIME ZONE" in upper or upper.startswith("TIMESTAMPTZ"): + return TimestampTzType(precision) + return TimestampType(precision) + if upper.startswith("TIME"): + nums = re.findall(r"\d+", stripped) + precision = int(nums[0]) if nums else None + if "WITH TIME ZONE" in upper or upper.startswith("TIMETZ"): + return TimeTzType(precision) + return TimeType(precision) + if upper.startswith("INTERVAL"): + fields_match = re.search(r"INTERVAL\s+(.*)", upper) + fields = fields_match.group(1).strip() if fields_match else None + return IntervalType(fields) + + # JSON + if self._PG_JSON_TYPES.match(upper): + if upper.startswith("JSONB"): + return JsonBType() + if upper.startswith("JSONPATH"): + from ..expression.types import PostgresJsonPathType + return PostgresJsonPathType() + return JsonType() + + # UUID + if self._PG_UUID_TYPES.match(upper): + from ..expression.types import PostgresUUIDType + return PostgresUUIDType() + + # Boolean + if upper.startswith("BOOLEAN") or upper.startswith("BOOL"): + return BooleanType() + + # Bit string + if self._PG_BIT_TYPES.match(upper): + nums = re.findall(r"\d+", stripped) + n = int(nums[0]) if nums else None + if upper.startswith("BIT"): + from ..expression.types import PostgresBitType + return PostgresBitType(n) + from ..expression.types import PostgresVarBitType + return PostgresVarBitType(n) + + # Network address + if self._PG_NET_TYPES.match(upper): + from ..expression.types import ( + PostgresCidrType, + PostgresInetType, + PostgresMacAddr8Type, + PostgresMacAddrType, + ) + if upper.startswith("CIDR"): + return PostgresCidrType() + if upper.startswith("MACADDR8"): + return PostgresMacAddr8Type() + if upper.startswith("MACADDR"): + return PostgresMacAddrType() + return PostgresInetType() + + # Geometric + if self._PG_GEOM_TYPES.match(upper): + from ..expression.types import ( + PostgresBoxType, + PostgresCircleType, + PostgresLineSegmentType, + PostgresLineType, + PostgresPathType, + PostgresPointType, + PostgresPolygonType, + ) + geom_map = { + "POINT": PostgresPointType, + "LINE": PostgresLineType, + "LSEG": PostgresLineSegmentType, + "BOX": PostgresBoxType, + "PATH": PostgresPathType, + "POLYGON": PostgresPolygonType, + "CIRCLE": PostgresCircleType, + } + for name, cls in geom_map.items(): + if upper.startswith(name): + return cls() + return PostgresPointType() + + # Range types + if self._PG_RANGE_TYPES.match(upper): + from ..expression.types import ( + PostgresDateRangeType, + PostgresInt4RangeType, + PostgresInt8RangeType, + PostgresNumRangeType, + PostgresTsRangeType, + PostgresTsTzRangeType, + ) + range_map = { + "INT4RANGE": PostgresInt4RangeType, + "INT8RANGE": PostgresInt8RangeType, + "NUMRANGE": PostgresNumRangeType, + "TSRANGE": PostgresTsRangeType, + "TSTZRANGE": PostgresTsTzRangeType, + "DATERANGE": PostgresDateRangeType, + } + for name, cls in range_map.items(): + if upper.startswith(name): + return cls() + return PostgresInt4RangeType() + + # Multirange types + if self._PG_MULTIRANGE_TYPES.match(upper): + from ..expression.types import ( + PostgresDateMultirangeType, + PostgresInt4MultirangeType, + PostgresInt8MultirangeType, + PostgresNumMultirangeType, + PostgresTsMultirangeType, + PostgresTsTzMultirangeType, + ) + mr_map = { + "INT4MULTIRANGE": PostgresInt4MultirangeType, + "INT8MULTIRANGE": PostgresInt8MultirangeType, + "NUMMULTIRANGE": PostgresNumMultirangeType, + "TSMULTIRANGE": PostgresTsMultirangeType, + "TSTZMULTIRANGE": PostgresTsTzMultirangeType, + "DATEMULTIRANGE": PostgresDateMultirangeType, + } + for name, cls in mr_map.items(): + if upper.startswith(name): + return cls() + return PostgresInt4MultirangeType() + + # OID types + if self._PG_OID_TYPES.match(upper): + from ..expression.types import ( + PostgresCIDType, + PostgresOIDType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresTIDType, + PostgresXID8Type, + PostgresXIDType, + ) + oid_map = { + "OID": PostgresOIDType, + "REGCLASS": PostgresRegClassType, + "REGTYPE": PostgresRegTypeType, + "XID8": PostgresXID8Type, + "XID": PostgresXIDType, + "CID": PostgresCIDType, + "TID": PostgresTIDType, + } + for name, cls in oid_map.items(): + if upper.startswith(name): + return cls() + return PostgresOIDType() + + # Text search + if self._PG_TS_TYPES.match(upper): + from ..expression.types import ( + PostgresTSQueryType, + PostgresTSVectorType, + ) + if upper.startswith("TSVECTOR"): + return PostgresTSVectorType() + return PostgresTSQueryType() + + # Miscellaneous + if self._PG_MISC_TYPES.match(upper): + from ..expression.types import ( + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresMoneyType, + PostgresPgLSNType, + PostgresXMLType, + ) + if upper.startswith("MONEY"): + return PostgresMoneyType() + if upper.startswith("XML"): + return PostgresXMLType() + if upper.startswith("PG_LSN"): + return PostgresPgLSNType() + if upper.startswith("HSTORE"): + return PostgresHstoreType() + if upper.startswith("GEOGRAPHY"): + return PostgresGeographyType() + if upper.startswith("GEOMETRY"): + return PostgresGeometryType() + + # Vector type (pgvector) + if upper.startswith("VECTOR"): + nums = re.findall(r"\d+", stripped) + dim = int(nums[0]) if nums else 0 + from ..expression.types import PostgresVectorType + return PostgresVectorType(dim) + + # Fallback + from rhosocial.activerecord.backend.expression.types import CustomType + return CustomType(stripped) \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py new file mode 100644 index 00000000..5da28ebe --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py @@ -0,0 +1,6 @@ +# src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py +"""PostgreSQL schema differ.""" + +from .differ import PostgresSchemaDiffer + +__all__ = ["PostgresSchemaDiffer"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py b/src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py new file mode 100644 index 00000000..cc5f46c7 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py @@ -0,0 +1,15 @@ +# src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py +"""PostgreSQL schema differ — column order has no semantic meaning.""" + +from rhosocial.activerecord.backend.schema.differ import SchemaDiffer + + +class PostgresSchemaDiffer(SchemaDiffer): + """PostgreSQL schema differ. + + Column order has no semantic meaning in PostgreSQL, and array + dimension equivalence is already handled by + ``PostgresArrayType.is_equivalent()``, so the default + ``_columns_equivalent`` is sufficient. + """ + pass diff --git a/tests/providers/partition.py b/tests/providers/partition.py index 2f99935c..64a73d68 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -25,6 +25,13 @@ from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord from rhosocial.activerecord.testsuite.feature.partition.interfaces import IPartitionProvider +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, IntegerType, TextType, TimestampType, DecimalType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) + from .scenarios import get_enabled_scenarios, get_scenario @@ -245,11 +252,11 @@ def _create_parent_sql(self, dialect): dialect=dialect, table=self.TABLE_NAME, columns=[ - ColumnDefinition("id", "BIGINT GENERATED BY DEFAULT AS IDENTITY"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("tenant_id", "INTEGER NOT NULL"), - ColumnDefinition("payload", "TEXT"), - ColumnDefinition("amount", "NUMERIC(12, 2)"), + ColumnDefinition("id", BigIntType(), dialect_options={"identity": "BY DEFAULT"}), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tenant_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), + ColumnDefinition("amount", DecimalType(12, 2)), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py index 814d970b..b0509aff 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py @@ -33,6 +33,13 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + TABLE_NAME = "test_bloom" ASYNC_TABLE_NAME = "test_bloom_async" @@ -55,13 +62,13 @@ def bloom_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -159,12 +166,12 @@ def test_bloom_index_with_options(self, bloom_env): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="code", data_type="TEXT"), + ColumnDefinition(name="code", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -232,13 +239,13 @@ async def async_bloom_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -336,12 +343,12 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="code", data_type="TEXT"), + ColumnDefinition(name="code", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py index a94ded0a..a6fd3461 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py @@ -39,6 +39,15 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresArrayType, +) + @pytest.fixture def btree_gin_env(postgres_backend_single): @@ -51,14 +60,14 @@ def btree_gin_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="value", data_type="INTEGER"), - ColumnDefinition(name="tags", data_type="TEXT[]"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="value", data_type=IntegerType()), + ColumnDefinition(name="tags", data_type=PostgresArrayType(TextType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -206,14 +215,14 @@ async def async_btree_gin_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="value", data_type="INTEGER"), - ColumnDefinition(name="tags", data_type="TEXT[]"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="value", data_type=IntegerType()), + ColumnDefinition(name="tags", data_type=PostgresArrayType(TextType())), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py index 39636320..8e6fc040 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py @@ -41,6 +41,15 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + TextType, + TimestampType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + @pytest.fixture def btree_gist_env(postgres_backend_single): @@ -53,14 +62,14 @@ def btree_gist_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="created_at", data_type="TIMESTAMP"), - ColumnDefinition(name="value", data_type="INTEGER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="created_at", data_type=TimestampType()), + ColumnDefinition(name="value", data_type=IntegerType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -217,14 +226,14 @@ async def async_btree_gist_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="created_at", data_type="TIMESTAMP"), - ColumnDefinition(name="value", data_type="INTEGER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="created_at", data_type=TimestampType()), + ColumnDefinition(name="value", data_type=IntegerType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py index c87b27d2..19277faf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py @@ -38,6 +38,14 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresCitextType, +) + # --- Helper functions --- @@ -47,12 +55,12 @@ def _setup_citext_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -81,14 +89,14 @@ def _setup_citext_unique_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( name="email", - data_type="CITEXT", + data_type=PostgresCitextType(), constraints=[ ColumnConstraint(ColumnConstraintType.UNIQUE), ], @@ -121,12 +129,12 @@ def _setup_citext_like_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -158,12 +166,12 @@ def _setup_citext_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -192,13 +200,13 @@ def _setup_citext_roles_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), - ColumnDefinition(name="role", data_type="TEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), + ColumnDefinition(name="role", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -238,12 +246,12 @@ async def _async_setup_citext_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -272,14 +280,14 @@ async def _async_setup_citext_unique_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( name="email", - data_type="CITEXT", + data_type=PostgresCitextType(), constraints=[ ColumnConstraint(ColumnConstraintType.UNIQUE), ], @@ -312,12 +320,12 @@ async def _async_setup_citext_like_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -349,12 +357,12 @@ async def _async_setup_citext_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -383,13 +391,13 @@ async def _async_setup_citext_roles_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), - ColumnDefinition(name="role", data_type="TEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), + ColumnDefinition(name="role", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py index a7be5d5f..d2966b0d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py @@ -38,6 +38,11 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresCubeType, + PostgresSerialType, +) + # --- Sync fixture and tests --- @@ -62,12 +67,12 @@ def cube_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -97,12 +102,12 @@ def cube_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -132,12 +137,12 @@ def cube_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -348,12 +353,12 @@ async def async_cube_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -383,12 +388,12 @@ async def async_cube_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -418,12 +423,12 @@ async def async_cube_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py index 3af2872a..90298a2c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py @@ -29,6 +29,11 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresPointType, + PostgresSerialType, +) + TABLE_NAME = "test_earth" ASYNC_TABLE_NAME = "test_earth_async" @@ -51,12 +56,12 @@ def earthdistance_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="loc", data_type="POINT"), + ColumnDefinition(name="loc", data_type=PostgresPointType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -163,12 +168,12 @@ async def async_earthdistance_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="loc", data_type="POINT"), + ColumnDefinition(name="loc", data_type=PostgresPointType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py index 311d54ee..64be7549 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py @@ -36,6 +36,12 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) TABLE_NAME = "test_fuzzy_lev" @@ -49,12 +55,12 @@ def _setup_lev_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="word", data_type="TEXT"), + ColumnDefinition(name="word", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -86,12 +92,12 @@ def _setup_diff_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -133,12 +139,12 @@ async def _async_setup_lev_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="word", data_type="TEXT"), + ColumnDefinition(name="word", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -170,12 +176,12 @@ async def _async_setup_diff_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py index f8b99261..1530b978 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py @@ -38,6 +38,10 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresHstoreType, +) # --- Helper functions --- @@ -48,12 +52,12 @@ def _make_hstore_columns(): return [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="data", data_type="HSTORE"), + ColumnDefinition(name="data", data_type=PostgresHstoreType()), ] diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py index d477159b..7cf7ab52 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py @@ -33,6 +33,12 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) @pytest.fixture @@ -46,13 +52,13 @@ def hypopg_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -262,13 +268,13 @@ async def async_hypopg_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py index 8a70b4ff..ccd5a613 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py @@ -39,6 +39,13 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresArrayType, +) # --- Helper functions --- @@ -49,12 +56,12 @@ def _setup_intarray_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -87,12 +94,12 @@ def _setup_intarray_overlap_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -125,12 +132,12 @@ def _setup_intarray_contained_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -163,12 +170,12 @@ def _setup_intarray_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -201,12 +208,12 @@ def _setup_intarray_gin_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -253,12 +260,12 @@ async def _async_setup_intarray_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -291,12 +298,12 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -329,12 +336,12 @@ async def _async_setup_intarray_contained_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -367,12 +374,12 @@ async def _async_setup_intarray_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -405,12 +412,12 @@ async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py index d0c56ecc..6a968c93 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py @@ -37,6 +37,13 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresLtreeType, +) # --- Sync fixture and tests --- @@ -63,12 +70,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -101,13 +108,13 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) @@ -140,12 +147,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -172,12 +179,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -220,12 +227,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -569,12 +576,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -607,13 +614,13 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) @@ -646,12 +653,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -678,12 +685,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -726,12 +733,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py index 59463877..eb0243b8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py @@ -38,6 +38,14 @@ ensure_extension_installed, async_ensure_extension_installed, ) +from rhosocial.activerecord.backend.expression.types import ( + TextType, + DateType, + DecimalType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) TABLE_NAME = "test_orafce_dates" @@ -55,16 +63,16 @@ def orafce_backend(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="dt", data_type="DATE"), - ColumnDefinition(name="ref_dt", data_type="DATE"), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="discount", data_type="NUMERIC"), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="dt", data_type=DateType()), + ColumnDefinition(name="ref_dt", data_type=DateType()), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="discount", data_type=DecimalType()), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -316,16 +324,16 @@ async def async_orafce_backend(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="dt", data_type="DATE"), - ColumnDefinition(name="ref_dt", data_type="DATE"), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="discount", data_type="NUMERIC"), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="dt", data_type=DateType()), + ColumnDefinition(name="ref_dt", data_type=DateType()), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="discount", data_type=DecimalType()), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py index 29c0be98..b022fb98 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py @@ -36,6 +36,12 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) # Table names for sync tests @@ -56,12 +62,12 @@ def _setup_trgm_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -93,12 +99,12 @@ def _setup_trgm_ops_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -134,12 +140,12 @@ def _setup_trgm_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -174,12 +180,12 @@ def _setup_trgm_word_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -219,12 +225,12 @@ async def _async_setup_trgm_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -256,12 +262,12 @@ async def _async_setup_trgm_ops_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -297,12 +303,12 @@ async def _async_setup_trgm_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -337,12 +343,12 @@ async def _async_setup_trgm_word_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py index 5a6e331f..7005a7dd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py @@ -36,6 +36,13 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresByteaType, +) # Table names for sync tests @@ -52,13 +59,13 @@ def _setup_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="TEXT"), - ColumnDefinition(name="password_hash", data_type="TEXT"), + ColumnDefinition(name="username", data_type=TextType()), + ColumnDefinition(name="password_hash", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -93,12 +100,12 @@ def _setup_encrypt_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="secret_data", data_type="BYTEA"), + ColumnDefinition(name="secret_data", data_type=PostgresByteaType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -144,13 +151,13 @@ async def _async_setup_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="TEXT"), - ColumnDefinition(name="password_hash", data_type="TEXT"), + ColumnDefinition(name="username", data_type=TextType()), + ColumnDefinition(name="password_hash", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -185,12 +192,12 @@ async def _async_setup_encrypt_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="secret_data", data_type="BYTEA"), + ColumnDefinition(name="secret_data", data_type=PostgresByteaType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py index 4452dc02..622b86d7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py @@ -30,6 +30,13 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + FloatType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) @pytest.fixture @@ -43,19 +50,19 @@ def pgrouting_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="source", data_type="INT"), - ColumnDefinition(name="target", data_type="INT"), - ColumnDefinition(name="cost", data_type="FLOAT"), - ColumnDefinition(name="reverse_cost", data_type="FLOAT"), - ColumnDefinition(name="x1", data_type="FLOAT"), - ColumnDefinition(name="y1", data_type="FLOAT"), - ColumnDefinition(name="x2", data_type="FLOAT"), - ColumnDefinition(name="y2", data_type="FLOAT"), + ColumnDefinition(name="source", data_type=IntegerType()), + ColumnDefinition(name="target", data_type=IntegerType()), + ColumnDefinition(name="cost", data_type=FloatType()), + ColumnDefinition(name="reverse_cost", data_type=FloatType()), + ColumnDefinition(name="x1", data_type=FloatType()), + ColumnDefinition(name="y1", data_type=FloatType()), + ColumnDefinition(name="x2", data_type=FloatType()), + ColumnDefinition(name="y2", data_type=FloatType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -172,19 +179,19 @@ async def async_pgrouting_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="source", data_type="INT"), - ColumnDefinition(name="target", data_type="INT"), - ColumnDefinition(name="cost", data_type="FLOAT"), - ColumnDefinition(name="reverse_cost", data_type="FLOAT"), - ColumnDefinition(name="x1", data_type="FLOAT"), - ColumnDefinition(name="y1", data_type="FLOAT"), - ColumnDefinition(name="x2", data_type="FLOAT"), - ColumnDefinition(name="y2", data_type="FLOAT"), + ColumnDefinition(name="source", data_type=IntegerType()), + ColumnDefinition(name="target", data_type=IntegerType()), + ColumnDefinition(name="cost", data_type=FloatType()), + ColumnDefinition(name="reverse_cost", data_type=FloatType()), + ColumnDefinition(name="x1", data_type=FloatType()), + ColumnDefinition(name="y1", data_type=FloatType()), + ColumnDefinition(name="x2", data_type=FloatType()), + ColumnDefinition(name="y2", data_type=FloatType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py index 8a775349..1541c41b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py @@ -37,6 +37,10 @@ from rhosocial.activerecord.backend.expression.query_parts import OrderByClause from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresVectorType, +) # --- Helper: common VECTOR(3) column definitions --- @@ -45,7 +49,7 @@ def _id_column(): return ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], @@ -53,7 +57,7 @@ def _id_column(): def _embedding_column(dim=3): - return ColumnDefinition(name="embedding", data_type=f"VECTOR({dim})") + return ColumnDefinition(name="embedding", data_type=PostgresVectorType(dim)) def _vector_literal(value, vector_type="vector"): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py index aefaf055..a0a89473 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py @@ -33,6 +33,11 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresGeometryType, + PostgresGeographyType, +) # --- Helper functions --- @@ -43,12 +48,12 @@ def _setup_geo_points_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -80,12 +85,12 @@ def _setup_geo_distance_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -117,12 +122,12 @@ def _setup_geo_contains_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POLYGON, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -160,12 +165,12 @@ def _setup_geo_index_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -207,12 +212,12 @@ def _setup_geog_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geog", data_type="GEOGRAPHY(POINT, 4326)"), + ColumnDefinition(name="geog", data_type=PostgresGeographyType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -254,12 +259,12 @@ async def _async_setup_geo_points_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -291,12 +296,12 @@ async def _async_setup_geo_distance_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -328,12 +333,12 @@ async def _async_setup_geo_contains_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POLYGON, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -371,12 +376,12 @@ async def _async_setup_geo_index_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -418,12 +423,12 @@ async def _async_setup_geog_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geog", data_type="GEOGRAPHY(POINT, 4326)"), + ColumnDefinition(name="geog", data_type=PostgresGeographyType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py index 0f1fef5d..e5b462e2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py @@ -36,6 +36,13 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresRasterType, +) TABLE_NAME = "test_raster_data" @@ -83,13 +90,13 @@ def raster_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="rast", data_type="RASTER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="rast", data_type=PostgresRasterType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -202,13 +209,13 @@ async def async_raster_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="rast", data_type="RASTER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="rast", data_type=PostgresRasterType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py index b5e58b99..7b1fa9df 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py @@ -36,6 +36,12 @@ ) from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal +from rhosocial.activerecord.backend.expression.types import ( + TextType, IntegerType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) from rhosocial.activerecord.backend.impl.postgres.functions.tablefunc import ( normal_rand, crosstab, @@ -56,14 +62,14 @@ def tablefunc_env(postgres_backend_single): sales_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="month", data_type="TEXT"), - ColumnDefinition(name="category", data_type="TEXT"), - ColumnDefinition(name="amount", data_type="INT"), + ColumnDefinition(name="month", data_type=TextType()), + ColumnDefinition(name="category", data_type=TextType()), + ColumnDefinition(name="amount", data_type=IntegerType()), ] create_sales = CreateTableExpression( dialect=dialect, @@ -96,13 +102,13 @@ def tablefunc_env(postgres_backend_single): tree_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="node_name", data_type="TEXT"), - ColumnDefinition(name="parent_id", data_type="INT"), + ColumnDefinition(name="node_name", data_type=TextType()), + ColumnDefinition(name="parent_id", data_type=IntegerType()), ] create_tree = CreateTableExpression( dialect=dialect, @@ -334,14 +340,14 @@ async def async_tablefunc_env(async_postgres_backend_single): sales_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="month", data_type="TEXT"), - ColumnDefinition(name="category", data_type="TEXT"), - ColumnDefinition(name="amount", data_type="INT"), + ColumnDefinition(name="month", data_type=TextType()), + ColumnDefinition(name="category", data_type=TextType()), + ColumnDefinition(name="amount", data_type=IntegerType()), ] create_sales = CreateTableExpression( dialect=dialect, @@ -374,13 +380,13 @@ async def async_tablefunc_env(async_postgres_backend_single): tree_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="node_name", data_type="TEXT"), - ColumnDefinition(name="parent_id", data_type="INT"), + ColumnDefinition(name="node_name", data_type=TextType()), + ColumnDefinition(name="parent_id", data_type=IntegerType()), ] create_tree = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py index 9f409c00..388bee98 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py @@ -33,6 +33,12 @@ from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresUUIDType, +) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType @@ -71,7 +77,7 @@ def uuid_ossp_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ColumnConstraint( @@ -80,7 +86,7 @@ def uuid_ossp_env(postgres_backend_single): ), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ], if_not_exists=True, ) @@ -107,12 +113,12 @@ def uuid_ossp_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) @@ -276,7 +282,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ColumnConstraint( @@ -285,7 +291,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): ), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ], if_not_exists=True, ) @@ -312,12 +318,12 @@ async def async_uuid_ossp_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py new file mode 100644 index 00000000..2c8914ed --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -0,0 +1,212 @@ +"""Tests for PostgreSQL core type → SQL string mappings.""" + +import pytest + +from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin + + +@pytest.fixture +def dialect(): + # Import directly to avoid triggering backend init which requires psycopg + from rhosocial.activerecord.backend.impl.postgres.mixins.types import ( + PostgresTypeFormatSupportMixin, + ) + from rhosocial.activerecord.backend.dialect.base import SQLDialectBase + + # Build a minimal dialect class that only has the type formatting + class TestPGDialect(PostgresTypeFormatSupportMixin, SQLDialectBase): + pass + + d = TestPGDialect() + d.version = (16, 0, 0) + return d + + +class TestCoreTypeMappings: + """Verify core DataType → SQL rendering via PostgreSQL dialect.""" + + # The five critical mappings + def test_boolean_to_boolean(self, dialect): + from rhosocial.activerecord.backend.expression.types import BooleanType + assert BooleanType().to_sql(dialect) == ("BOOLEAN", ()) + + def test_tinyint_to_smallint(self, dialect): + from rhosocial.activerecord.backend.expression.types import TinyIntType + assert TinyIntType().to_sql(dialect) == ("SMALLINT", ()) + + def test_blob_to_bytea(self, dialect): + from rhosocial.activerecord.backend.expression.types import BlobType + assert BlobType().to_sql(dialect) == ("BYTEA", ()) + + def test_datetime_to_timestamp(self, dialect): + from rhosocial.activerecord.backend.expression.types import DateTimeType + assert DateTimeType().to_sql(dialect) == ("TIMESTAMP", ()) + + def test_int_to_integer(self, dialect): + from rhosocial.activerecord.backend.expression.types import IntType + assert IntType().to_sql(dialect) == ("INTEGER", ()) + + # Standard integer family + def test_smallint(self, dialect): + from rhosocial.activerecord.backend.expression.types import SmallIntType + assert SmallIntType().to_sql(dialect) == ("SMALLINT", ()) + + def test_integer(self, dialect): + from rhosocial.activerecord.backend.expression.types import IntegerType + assert IntegerType().to_sql(dialect) == ("INTEGER", ()) + + def test_bigint(self, dialect): + from rhosocial.activerecord.backend.expression.types import BigIntType + assert BigIntType().to_sql(dialect) == ("BIGINT", ()) + + # Numeric family + def test_real(self, dialect): + from rhosocial.activerecord.backend.expression.types import RealType + assert RealType().to_sql(dialect) == ("REAL", ()) + + def test_double(self, dialect): + from rhosocial.activerecord.backend.expression.types import DoubleType + assert DoubleType().to_sql(dialect) == ("DOUBLE PRECISION", ()) + + def test_numeric(self, dialect): + from rhosocial.activerecord.backend.expression.types import DecimalType + assert DecimalType(10, 2).to_sql(dialect) == ("DECIMAL(10,2)", ()) + assert DecimalType().to_sql(dialect) == ("DECIMAL", ()) + + # String family + def test_varchar(self, dialect): + from rhosocial.activerecord.backend.expression.types import VarCharType + assert VarCharType(255).to_sql(dialect) == ("VARCHAR(255)", ()) + assert VarCharType().to_sql(dialect) == ("VARCHAR", ()) + + def test_text(self, dialect): + from rhosocial.activerecord.backend.expression.types import TextType + assert TextType().to_sql(dialect) == ("TEXT", ()) + + # JSON family + def test_json(self, dialect): + from rhosocial.activerecord.backend.expression.types import JsonType + assert JsonType().to_sql(dialect) == ("JSON", ()) + + def test_jsonb(self, dialect): + from rhosocial.activerecord.backend.expression.types import JsonBType + assert JsonBType().to_sql(dialect) == ("JSONB", ()) + + +class TestArrayType: + """PostgreSQL array type (T[]) rendering and parsing.""" + + def test_array_1d_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + arr = PostgresArrayType(IntegerType()) + assert arr.to_sql(dialect) == ("INTEGER[]", ()) + + def test_array_2d_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + arr = PostgresArrayType(IntegerType(), dimensions=2) + assert arr.to_sql(dialect) == ("INTEGER[][]", ()) + + def test_array_varchar_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import VarCharType + arr = PostgresArrayType(VarCharType(255)) + assert arr.to_sql(dialect) == ("VARCHAR(255)[]", ()) + + def test_array_boolean_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import BooleanType + arr = PostgresArrayType(BooleanType()) + assert arr.to_sql(dialect) == ("BOOLEAN[]", ()) + + def test_array_equality(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import ( + ArrayType, IntegerType, VarCharType, + ) + a1 = PostgresArrayType(IntegerType(), 2) + a2 = PostgresArrayType(IntegerType(), 2) + a3 = PostgresArrayType(VarCharType(255), 2) + assert a1 == a2 + assert a1 != a3 + assert hash(a1) == hash(a2) + # PostgresArrayType.is_equivalent ignores dimensions + a4 = PostgresArrayType(IntegerType(), 1) + assert a1.is_equivalent(a4) + + def test_parse_array_bracket_suffix(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER[]") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 1 + assert isinstance(result.element_type, IntegerType) + + def test_parse_array_2d_bracket_suffix(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER[][]") + assert isinstance(result, PostgresArrayType) + # PG normalises all array declarations to 1-D internally + assert result.dimensions == 1 + assert isinstance(result.element_type, IntegerType) + + def test_parse_array_keyword(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER ARRAY") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 1 + assert isinstance(result.element_type, IntegerType) + + def test_parse_array_varchar(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import VarCharType + result = dialect.parse_type("VARCHAR(255)[]") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 1 + assert isinstance(result.element_type, VarCharType) + assert result.element_type.length == 255 + + def test_parse_non_array_passthrough(self, dialect): + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER") + assert isinstance(result, IntegerType) + + def test_is_element_type_equivalent(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import ( + IntegerType, SmallIntType, + ) + a1 = PostgresArrayType(IntegerType(), 2) + a2 = PostgresArrayType(IntegerType(), 1) # different dimension + a3 = PostgresArrayType(SmallIntType()) # different element type + # Same element, different dimension → equivalent + assert a1.is_element_type_equivalent(a2) + # Different element → not equivalent + assert not a1.is_element_type_equivalent(a3) + # Plain IntegerType (not array) as other → matches element + assert a1.is_element_type_equivalent(IntegerType()) + # Plain SmallIntType as other → doesn't match Integer element + assert not a1.is_element_type_equivalent(SmallIntType()) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py index 28c02950..e304adc8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py @@ -32,6 +32,12 @@ PostgresVacuumExpression, # noqa: F401 PostgresAnalyzeExpression, # noqa: F401 ) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, TextType, TimestampType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) from rhosocial.activerecord.backend.impl.postgres.mixins.dml.extended_statistics import ( PostgresExtendedStatisticsMixin, # noqa: F401 ) @@ -242,8 +248,8 @@ def test_create_range_partitioned_parent_table(self, dialect): dialect=dialect, table="events", columns=[ - ColumnDefinition("id", "BIGINT"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), + ColumnDefinition("id", BigIntType()), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -263,8 +269,8 @@ def test_create_list_partitioned_parent_table(self, dialect): dialect=dialect, table="events", columns=[ - ColumnDefinition("id", "BIGINT"), - ColumnDefinition("status", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType()), + ColumnDefinition("status", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -283,7 +289,7 @@ def test_create_hash_partitioned_parent_table_pg10(self): expr = CreateTableExpression( dialect=dialect, table="events", - columns=[ColumnDefinition("tenant_id", "BIGINT NOT NULL")], + columns=[ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)])], partition=PartitionClause( dialect=dialect, method=PartitionStrategy.HASH, @@ -300,7 +306,7 @@ def test_create_hash_partitioned_parent_table_pg11(self): expr = CreateTableExpression( dialect=dialect, table="events", - columns=[ColumnDefinition("tenant_id", "BIGINT NOT NULL")], + columns=[ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)])], partition=PartitionClause( dialect=dialect, method=PartitionStrategy.HASH, @@ -318,7 +324,7 @@ def test_create_partitioned_parent_table_pg9(self): expr = CreateTableExpression( dialect=dialect, table="events", - columns=[ColumnDefinition("created_at", "TIMESTAMP NOT NULL")], + columns=[ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)])], partition=PartitionClause( dialect=dialect, method=PartitionStrategy.RANGE, @@ -344,9 +350,9 @@ def test_multi_column_range_partitioned_parent_table(self, dialect): dialect=dialect, table="tenanted_events", columns=[ - ColumnDefinition("tenant_id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT"), + ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py index 6f03f418..f8fa1597 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py @@ -62,20 +62,16 @@ def test_valid_data_type_works(self, postgres_backend, test_table_for_data_type) """) # noqa: F541 assert result is not None - def test_malicious_data_type_rejected_at_dialect_level(self, postgres_backend): - """Test that malicious data type is rejected at dialect level before DB execution.""" + def test_malicious_data_type_rejected_at_construction(self, postgres_backend): + """Test that malicious data type string is rejected at ColumnDefinition construction.""" from rhosocial.activerecord.backend.expression.statements import ColumnDefinition - dialect = postgres_backend.dialect - - # This should raise ValueError before reaching the database - col_def = ColumnDefinition( - name="test_col", - data_type="VARCHAR(255); DROP TABLE users--", - ) - - with pytest.raises(ValueError, match="Invalid data type"): - dialect.format_column_definition(col_def) + # ColumnDefinition now rejects string data_type values at construction time + with pytest.raises(TypeError, match="data_type must be a DataType instance"): + ColumnDefinition( + name="test_col", + data_type="VARCHAR(255); DROP TABLE users--", + ) class TestPostgresExcludeConstraintSecurityIntegration: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py index be4e111c..49f5e03c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py @@ -26,6 +26,9 @@ from rhosocial.activerecord.backend.expression.core import Column from rhosocial.activerecord.backend.expression.query_parts import JoinExpression, JoinType from rhosocial.activerecord.backend.expression.literals import Identifier +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, TextType, +) GRAPH_NAME = "social_graph" @@ -42,48 +45,48 @@ def social_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) people_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", "TEXT"), - ColumnDefinition("email", "TEXT"), - ColumnDefinition("city", "TEXT"), + ColumnDefinition("name", TextType()), + ColumnDefinition("email", TextType()), + ColumnDefinition("city", TextType()), ] backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) follows_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", "INTEGER", + ColumnDefinition("follower_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("followed_id", "INTEGER", + ColumnDefinition("followed_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("since", "TEXT"), + ColumnDefinition("since", TextType()), ] backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) posts_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("author_id", "INTEGER", + ColumnDefinition("author_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("content", "TEXT"), - ColumnDefinition("created_at", "TEXT"), + ColumnDefinition("content", TextType()), + ColumnDefinition("created_at", TextType()), ] backend.execute(*CreateTableExpression(dialect, "posts", posts_cols).to_sql()) likes_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", "INTEGER", + ColumnDefinition("user_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("post_id", "INTEGER", + ColumnDefinition("post_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("posts", ["id"]))]), - ColumnDefinition("created_at", "TEXT"), + ColumnDefinition("created_at", TextType()), ] backend.execute(*CreateTableExpression(dialect, "likes", likes_cols).to_sql()) @@ -324,24 +327,24 @@ async def async_social_data(self, async_postgres_backend): await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) people_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", "TEXT"), - ColumnDefinition("email", "TEXT"), - ColumnDefinition("city", "TEXT"), + ColumnDefinition("name", TextType()), + ColumnDefinition("email", TextType()), + ColumnDefinition("city", TextType()), ] await backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) follows_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", "INTEGER", + ColumnDefinition("follower_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("followed_id", "INTEGER", + ColumnDefinition("followed_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("since", "TEXT"), + ColumnDefinition("since", TextType()), ] await backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py index 2b9afaff..f8485637 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py @@ -8,6 +8,9 @@ import pytest # noqa: F401 from rhosocial.activerecord.backend.expression import CreateTableExpression, ColumnDefinition from rhosocial.activerecord.backend.expression.statements import ColumnConstraint, ColumnConstraintType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, +) from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect @@ -204,10 +207,10 @@ def test_like_ignores_columns(self): """Test that LIKE syntax ignores columns parameter.""" dialect = PostgresDialect() columns = [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition("name", "VARCHAR(255)") + ColumnDefinition("name", VarCharType(255)) ] create_expr = CreateTableExpression( dialect=dialect, @@ -258,10 +261,10 @@ def test_fallback_to_base_when_no_like(self): """Test that base implementation is used when LIKE is not specified.""" dialect = PostgresDialect() columns = [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition("name", "VARCHAR(255)", constraints=[ + ColumnDefinition("name", VarCharType(255), constraints=[ ColumnConstraint(ColumnConstraintType.NOT_NULL) ]) ] diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py index 6f0a7343..fd4677d8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py @@ -32,6 +32,12 @@ PostgresDetachPartitionExpression, PostgresPartitionMetadataExpression, ) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, TextType, TimestampType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) LIST_PARENT = "ar_partition_adv_list_events" @@ -59,9 +65,9 @@ def _create_list_parent_expression(dialect): dialect=dialect, table=LIST_PARENT, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("status", "TEXT NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("status", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -87,9 +93,9 @@ def _create_hash_parent_expression(dialect): dialect=dialect, table=HASH_PARENT, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("bucket", "BIGINT NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("bucket", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -115,9 +121,9 @@ def _create_range_parent_expression(dialect): dialect=dialect, table=RANGE_PARENT, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -318,9 +324,9 @@ def test_default_attach_and_detach_workflow(self, postgres_backend, postgres_ran dialect=dialect, table=default_partition_name, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], ) postgres_backend.execute(*create_default.to_sql()) @@ -378,8 +384,8 @@ def test_metadata_include_partitions_false_and_schema_filter(self, postgres_back dialect=dialect, table=TableExpression(dialect, SCHEMA_PARENT, schema_name=schema), columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py index 5ded526f..fed659ae 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py @@ -51,6 +51,12 @@ ensure_extension_installed, async_ensure_extension_installed, ) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, TextType, TimestampType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) PARTITION_TABLES = ( @@ -76,9 +82,9 @@ def _create_partitioned_parent_sql(dialect, table_name: str): dialect=dialect, table=table_name, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), ], partition=PartitionClause( dialect=dialect, @@ -200,10 +206,10 @@ def _create_production_parent_sql(dialect): dialect=dialect, table=PRODUCTION_PARTITION_TABLE, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP(6) NOT NULL"), - ColumnDefinition("tenant_id", "BIGINT NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(6), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], table_constraints=[ TableConstraint( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py index fe763442..dca80715 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py @@ -17,6 +17,7 @@ TableConstraint, TableConstraintType, ) +from rhosocial.activerecord.backend.expression.types import VarCharType from typing import Tuple, Any # noqa: F401 @@ -41,25 +42,23 @@ def test_postgres_validate_data_type(dialect): def test_postgres_format_column_definition_data_type_validation(dialect): - """Test column definition validates data_type.""" + """Test column definition formats data_type correctly.""" col_def = ColumnDefinition( name="test_col", - data_type="VARCHAR(255)", + data_type=VarCharType(255), ) sql, params = dialect.format_column_definition(col_def) assert "VARCHAR(255)" in sql -def test_postgres_format_column_definition_data_type_rejects_injection(dialect): - """Test that malicious data_type is rejected.""" - col_def = ColumnDefinition( - name="test_col", - data_type="VARCHAR(255); DROP TABLE users--", - ) - - with pytest.raises(ValueError, match="Invalid data type"): - dialect.format_column_definition(col_def) +def test_postgres_column_definition_rejects_string_data_type(dialect): + """Test that ColumnDefinition rejects string data_type at construction.""" + with pytest.raises(TypeError, match="data_type must be a DataType instance"): + ColumnDefinition( + name="test_col", + data_type="VARCHAR(255); DROP TABLE users--", + ) def test_postgres_format_default_constraint_string_escaping(dialect): From e691dc47ae4770e5fc11e8371b653eea6219a57d Mon Sep 17 00:00:00 2001 From: zhao vistart <864657+vistart@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:38:52 +0800 Subject: [PATCH 05/39] feat(cli): add named-migration subcommand for PostgreSQL backend (#47) * feat(cli): add named-migration subcommand (PostgreSQL adapter) Registers the 'named-migration' CLI subcommand for the PostgreSQL backend. Delegates to the shared implementation in rhosocial.activerecord.backend.migration.cli. - cli/named_migration.py: parser + handler adapter - cli/__init__.py: register in COMMAND_NAMES and register_commands() * feat(migration): add --async support, example migrations, and demo scripts for PostgreSQL - Extend named_migration.py adapter with --async support (AsyncPostgresBackend) - Add extensive CLI epilog with 12 scenario examples and demo script references - Create examples/named_migrations/ with migrations.py, expressions.py, run_basic.py, run_chain.py - Add demo_basic.sh, demo_chain.sh, demo_all.sh end-to-end shell scripts - expressions.py uses PostgresSerialType and PostgresCharacterVaryingType * chore: add changelog fragment for PR #47 * chore: add parameterized migration, async, incompatible, and single-transaction demo scripts for PostgreSQL * fix(postgres): use positional arg instead of keyword 'size' for PostgresCharacterVaryingType * ci: trigger rebuild --- changelog.d/47.added.md | 1 + .../backend/impl/postgres/cli/__init__.py | 5 +- .../impl/postgres/cli/named_migration.py | 115 ++++++++++++++++++ .../examples/named_migrations/__init__.py | 2 + .../examples/named_migrations/demo_all.sh | 67 ++++++++++ .../examples/named_migrations/demo_async.sh | 49 ++++++++ .../examples/named_migrations/demo_basic.sh | 66 ++++++++++ .../examples/named_migrations/demo_chain.sh | 66 ++++++++++ .../named_migrations/demo_incompatible.sh | 56 +++++++++ .../examples/named_migrations/demo_params.sh | 56 +++++++++ .../examples/named_migrations/expressions.py | 88 ++++++++++++++ .../examples/named_migrations/migrations.py | 73 +++++++++++ .../examples/named_migrations/run_basic.py | 93 ++++++++++++++ .../examples/named_migrations/run_chain.py | 102 ++++++++++++++++ 14 files changed, 838 insertions(+), 1 deletion(-) create mode 100644 changelog.d/47.added.md create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_chain.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_incompatible.sh create mode 100755 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_params.sh create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/migrations.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py diff --git a/changelog.d/47.added.md b/changelog.d/47.added.md new file mode 100644 index 00000000..597e92e7 --- /dev/null +++ b/changelog.d/47.added.md @@ -0,0 +1 @@ +Added named-migration CLI subcommand with --async support and PostgreSQL-specific example migrations for the PostgreSQL backend. \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/__init__.py index 54045e8f..f1e4968a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/__init__.py @@ -5,7 +5,8 @@ COMMAND_NAMES = [ 'info', 'query', 'introspect', 'status', - 'named-expression', 'named-procedure', 'named-procedure-graph', 'named-connection', + 'named-expression', 'named-procedure', 'named-procedure-graph', + 'named-migration', 'named-connection', ] @@ -18,6 +19,7 @@ def register_commands(subparsers): from .named_expression import create_parser as nq_parser from .named_procedure import create_parser as np_parser from .named_procedure_graph import create_parser as npg_parser + from .named_migration import create_parser as nm_parser from .named_connection import create_parser as nc_parser info_parser(subparsers) query_parser(subparsers) @@ -26,6 +28,7 @@ def register_commands(subparsers): nq_parser(subparsers) np_parser(subparsers) npg_parser(subparsers) + nm_parser(subparsers) nc_parser(subparsers) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py new file mode 100644 index 00000000..3d89eb89 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py @@ -0,0 +1,115 @@ +# src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py +"""named-migration subcommand — PostgreSQL adapter for the shared CLI helper.""" + +from __future__ import annotations + +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend + +from .connection import create_connection_parent_parser, resolve_connection_config_from_args +from .output import create_provider + + +def create_parser(subparsers): + """Create the named-migration subcommand parser. + + Provides connection and output arguments via a parent parser. + """ + from rhosocial.activerecord.backend.migration.cli import create_named_migration_parser + + local_parent = create_connection_parent_parser() + nm_epilog = """Examples: + # Apply a migration + %(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction up + + # Rollback + %(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction down + + # Apply with record store + %(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --record-store ./migrations.json + + # Dry-run + %(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --dry-run + + # Apply asynchronously + %(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --async + + # Pass parameters to the migration + %(prog)s myapp.migrations.v001.CreateUsersTable --param table_name=custom_table + + # List available migrations + %(prog)s myapp.migrations --list + + # Describe a migration + %(prog)s myapp.migrations.v001.CreateUsersTable --describe + + # Batch: apply all pending migrations in a module + %(prog)s myapp.migrations --all --record-store ./migrations.json + + # Batch: dry-run all pending migrations + %(prog)s myapp.migrations --all --record-store ./migrations.json --dry-run + + # Batch: rollback all applied migrations + %(prog)s myapp.migrations --all --record-store ./migrations.json --direction down + + # Batch: wrap all migrations in a single transaction + %(prog)s myapp.migrations --all --record-store ./migrations.json --single-transaction + +End-to-end demo scripts with UP/DOWN round-trip verification: + https://github.com/rhosocial/python-activerecord-postgres/tree/main/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations + PYTHONPATH=src bash demo_basic.sh + PYTHONPATH=src bash demo_chain.sh + PYTHONPATH=src bash demo_all.sh +""" + return create_named_migration_parser(subparsers, local_parent, epilog=nm_epilog) + + +def handle(args): + """Handle the named-migration subcommand.""" + from rhosocial.activerecord.backend.migration.cli import handle_named_migration as handle_nm + + provider = create_provider(args.output, ascii_borders=args.rich_ascii) + + backend = None + + def backend_factory(): + nonlocal backend + config = resolve_connection_config_from_args(args) + backend = PostgresBackend(connection_config=config) + backend.connect() + backend.introspect_and_adapt() + return backend + + def disconnect(): + if backend and getattr(backend, "_connection", None): + backend.disconnect() + + is_async = getattr(args, "is_async", False) + if is_async: + async_backend = None + + def backend_async_factory(): + nonlocal async_backend + config = resolve_connection_config_from_args(args) + async_backend = AsyncPostgresBackend(connection_config=config) + return async_backend + + async def disconnect_async(): + if async_backend and getattr(async_backend, "_connection", None): + await async_backend.disconnect() + + handle_nm( + args, + provider, + backend_factory=backend_factory, + disconnect=disconnect, + backend_async_factory=backend_async_factory, + disconnect_async=disconnect_async, + ) + return + + handle_nm( + args, + provider, + backend_factory=backend_factory, + disconnect=disconnect, + ) \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py new file mode 100644 index 00000000..82a92881 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py @@ -0,0 +1,2 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/__init__.py +"""Named migration examples for PostgreSQL backend.""" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh new file mode 100755 index 00000000..7748605c --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# =========================================================================== +# demo_all.sh — batch migration (--all) for PostgreSQL +# +# Scenarios: +# - --all runs all pending migrations in dependency order +# - --all --dry-run preview without changes +# - --all --direction down rolls back everything +# +# Usage: +# cd python-activerecord-postgres +# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \ +# PYTHONPATH=src \ +# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_all.sh +# =========================================================================== +set -euo pipefail + +if [ -d "./src" ]; then + export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src" +fi + +MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" +STORE="./demo_pg_all_mig.json" +VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}" +PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres" + +rm -f "$STORE" +echo "=== Batch Migration (--all) for PostgreSQL ===" +echo + +echo "[1] --all without --record-store (should error):" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test 2>&1 || true +echo + +echo "[2] --all --dry-run (preview SQL, no changes):" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --dry-run --record-store "$STORE" +echo + +echo "[3] --all apply all pending migrations:" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --record-store "$STORE" +echo + +echo "[4] Re-run --all (all applied, should be no-op):" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test --record-store "$STORE" 2>&1 || true +echo + +echo "[5] --all --single-transaction (rollback in one transaction):" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \ + --direction down --record-store "$STORE" --single-transaction +echo + +echo "[6] --all --single-transaction (re-apply UP):" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \ + --direction up --record-store "$STORE" --single-transaction +echo + +echo "[7] --all --direction down rollback everything:" +$PYTHON named-migration "${MODULE}.migrations" --all --host localhost --database test \ + --direction down --record-store "$STORE" +echo + +echo "[8] Record store final state:" +cat "$STORE" +echo + +rm -f "$STORE" +echo "=== Batch Migration (--all) Complete for PostgreSQL ===" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh new file mode 100755 index 00000000..3832ab4e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# =========================================================================== +# demo_async.sh — async migration execution (--async) for PostgreSQL +# +# Scenarios: +# - apply UP with --async +# - dry-run with --async +# - rollback DOWN with --async +# +# Requires: asyncpg +# +# Usage: +# cd python-activerecord-postgres +# PYTHONPATH=src \ +# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_async.sh +# =========================================================================== +set -euo pipefail + +if [ -d "./src" ]; then + export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src" +fi + +MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" +FQN="${MODULE}.migrations.V001CreateUsers" +STORE="./demo_pg_async_mig.json" +VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}" +PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres" + +rm -f "$STORE" +echo "=== Async Migration (--async) for PostgreSQL ===" +echo + +echo "[1] Async dry-run (preview SQL, no changes):" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction up --dry-run --async +echo + +echo "[2] Async apply UP:" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction up --async --record-store "$STORE" +echo + +echo "[3] Async rollback DOWN:" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction down --async --record-store "$STORE" +echo + +rm -f "$STORE" +echo "=== Async Migration Demo Complete for PostgreSQL ===" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh new file mode 100755 index 00000000..c7dbe68d --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# =========================================================================== +# demo_basic.sh — single migration basic operations (PostgreSQL) +# +# Scenarios: +# - apply / rollback a single migration +# - dry-run preview +# - duplicate execution protection +# +# Usage: +# cd python-activerecord-postgres +# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \ +# PYTHONPATH=src \ +# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_basic.sh +# =========================================================================== +set -euo pipefail + +if [ -d "./src" ]; then + export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src" +fi + +MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" +FQN="${MODULE}.migrations.V001CreateUsers" +STORE="./demo_pg_basic_mig.json" +VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}" +PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres" + +rm -f "$STORE" +echo "=== Single Migration Basic Operations (PostgreSQL) ===" +echo + +echo "[1] List all migrations in the module:" +$PYTHON named-migration "${MODULE}.migrations" --list -o table +echo + +echo "[2] Describe V001CreateUsers (--describe):" +$PYTHON named-migration "$FQN" --describe +echo + +echo "[3] Dry-run preview (no actual changes):" +$PYTHON named-migration "$FQN" --host localhost --database test --direction up --dry-run +echo + +echo "[4] Apply UP (create users table):" +$PYTHON named-migration "$FQN" --host localhost --database test --direction up --record-store "$STORE" +echo + +echo "[5] Record store contents:" +cat "$STORE" +echo + +echo +echo "[6] Duplicate UP (should be rejected):" +$PYTHON named-migration "$FQN" --host localhost --database test --direction up --record-store "$STORE" 2>&1 || true +echo + +echo "[7] Apply DOWN (drop users table):" +$PYTHON named-migration "$FQN" --host localhost --database test --direction down --record-store "$STORE" +echo + +echo "[8] Record store after rollback:" +cat "$STORE" +echo + +rm -f "$STORE" +echo "=== Single Migration Basic Operations Complete (PostgreSQL) ===" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_chain.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_chain.sh new file mode 100755 index 00000000..82c7f2f6 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_chain.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# =========================================================================== +# demo_chain.sh — dependency chain migration (PostgreSQL) +# +# Scenarios: +# - multiple migrations with dependencies executed in order +# - dependency not satisfied — rejected +# - rollback in reverse order +# +# Usage: +# cd python-activerecord-postgres +# DEMO_VENV_PYTHON=.venv3.14-ubuntu26.04/bin/python \ +# PYTHONPATH=src \ +# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_chain.sh +# =========================================================================== +set -euo pipefail + +if [ -d "./src" ]; then + export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src" +fi + +MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" +V001="${MODULE}.migrations.V001CreateUsers" +V002="${MODULE}.migrations.V002CreatePosts" +STORE="./demo_pg_chain_mig.json" +VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}" +PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres" + +rm -f "$STORE" +echo "=== Dependency Chain Migration (PostgreSQL) ===" +echo + +echo "[1] List all migrations (dependencies column):" +$PYTHON named-migration "${MODULE}.migrations" --list -o table +echo + +echo "[2] V002CreatePosts dependencies (should declare v001_create_users):" +$PYTHON named-migration "$V002" --describe +echo + +echo "[3] Run V002 before V001 (should fail):" +$PYTHON named-migration "$V002" --host localhost --database test --direction up --record-store "$STORE" 2>&1 || true +echo + +echo "[4] Apply V001 (create users table):" +$PYTHON named-migration "$V001" --host localhost --database test --direction up --record-store "$STORE" +echo + +echo "[5] Apply V002 (create posts table):" +$PYTHON named-migration "$V002" --host localhost --database test --direction up --record-store "$STORE" +echo + +echo "[6] Rollback V002 (downstream first):" +$PYTHON named-migration "$V002" --host localhost --database test --direction down --record-store "$STORE" +echo + +echo "[7] Rollback V001:" +$PYTHON named-migration "$V001" --host localhost --database test --direction down --record-store "$STORE" +echo + +echo "[8] Record store final state (all rolled back):" +cat "$STORE" +echo + +rm -f "$STORE" +echo "=== Dependency Chain Migration Complete (PostgreSQL) ===" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_incompatible.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_incompatible.sh new file mode 100755 index 00000000..2945110c --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_incompatible.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# =========================================================================== +# demo_incompatible.sh — dialect incompatibility via dry-run for PostgreSQL +# +# Scenarios: +# - compatible migration dry-run succeeds +# - incompatible expression caught as MigrationDialectError +# +# Usage: +# cd python-activerecord-postgres +# PYTHONPATH=src \ +# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_incompatible.sh +# =========================================================================== +set -euo pipefail + +if [ -d "./src" ]; then + export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src" +fi + +MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" +FQN="${MODULE}.migrations.V001CreateUsers" +STORE="./demo_pg_ic_mig.json" +VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}" +PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres" + +rm -f "$STORE" +echo "=== Dialect Incompatibility Detection for PostgreSQL ===" +echo + +echo "[1] Compatible migration dry-run (should succeed):" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction up --dry-run +echo + +echo "[2] Demonstration:" +echo " The named migration system catches dialect incompatibilities at dry-run" +echo " time via to_sql() on each expression. If an expression uses a feature" +echo " not supported by the connected PostgreSQL version, UnsupportedFeatureError" +echo " is caught and wrapped as MigrationDialectError." +echo +echo " Incompatible expressions produce output like:" +echo " Dialect Error: 'postgresql' dialect does not support ..." +echo + +echo "[3] Compatible apply UP (succeeds):" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction up --record-store "$STORE" 2>&1 || true +echo + +echo "[4] Rollback DOWN:" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction down --record-store "$STORE" +echo + +rm -f "$STORE" +echo "=== Dialect Incompatibility Demo Complete for PostgreSQL ===" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_params.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_params.sh new file mode 100755 index 00000000..3f9a25e9 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_params.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# =========================================================================== +# demo_params.sh — parameterized migration (--param) for PostgreSQL +# +# Scenarios: +# - describe a parameterized migration +# - apply with custom --param table_name=my_config +# - dry-run with custom param +# - rollback with matching --param +# +# Usage: +# cd python-activerecord-postgres +# PYTHONPATH=src \ +# bash src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/demo_params.sh +# =========================================================================== +set -euo pipefail + +if [ -d "./src" ]; then + export PYTHONPATH="${PYTHONPATH:-}:$(pwd)/src" +fi + +MODULE="rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" +FQN="${MODULE}.migrations.V003CreateCustomTable" +STORE="./demo_pg_params_mig.json" +VENV_PYTHON="${DEMO_VENV_PYTHON:-python3}" +PYTHON="$VENV_PYTHON -m rhosocial.activerecord.backend.impl.postgres" + +rm -f "$STORE" +echo "=== Parameterized Migration (--param) for PostgreSQL ===" +echo + +echo "[1] Describe V003CreateCustomTable (shows table_name parameter):" +$PYTHON named-migration "$FQN" --describe +echo + +echo "[2] Dry-run with --param table_name=my_config:" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction up --param table_name=my_config --dry-run +echo + +echo "[3] Apply UP with --param table_name=my_config:" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction up --param table_name=my_config --record-store "$STORE" +echo + +echo "[4] Rollback with --param table_name=my_config:" +$PYTHON named-migration "$FQN" --host localhost --database test \ + --direction down --param table_name=my_config --record-store "$STORE" +echo + +echo "[5] Dry-run without --param (uses default 'custom_table'):" +$PYTHON named-migration "$FQN" --host localhost --database test --direction up --dry-run +echo + +rm -f "$STORE" +echo "=== Parameterized Migration Demo Complete for PostgreSQL ===" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py new file mode 100644 index 00000000..a37101fd --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py @@ -0,0 +1,88 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py +""" +DDL named expression functions for PostgreSQL migration examples. + +Each function receives a *dialect* and returns a DDL expression object. +These are the building blocks used by NamedMigration up()/down() methods. +""" + +from rhosocial.activerecord.backend.expression.statements.ddl_table import ( + CreateTableExpression, + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, + DropTableExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresCharacterVaryingType, +) + + +def create_users_table(dialect): + """CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(255), email VARCHAR(255)).""" + return CreateTableExpression( + dialect, + table="users", + columns=[ + ColumnDefinition( + "id", + PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)], + ), + ColumnDefinition("name", PostgresCharacterVaryingType(255)), + ColumnDefinition("email", PostgresCharacterVaryingType(255)), + ], + ) + + +def drop_users_table(dialect): + """DROP TABLE IF EXISTS users.""" + return DropTableExpression(dialect, table="users", if_exists=True) + + +def create_posts_table(dialect): + """CREATE TABLE posts (id SERIAL PRIMARY KEY, title VARCHAR(255), user_id INTEGER).""" + return CreateTableExpression( + dialect, + table="posts", + columns=[ + ColumnDefinition( + "id", + PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)], + ), + ColumnDefinition("title", PostgresCharacterVaryingType(255)), + ColumnDefinition("user_id", PostgresCharacterVaryingType(255)), + ], + ) + + +def drop_posts_table(dialect): + """DROP TABLE IF EXISTS posts.""" + return DropTableExpression(dialect, table="posts", if_exists=True) + + +def create_custom_table(dialect, table_name: str = "custom_table"): + """CREATE TABLE (id SERIAL PRIMARY KEY, value VARCHAR(255)). + + This expression accepts an extra ``table_name`` parameter, allowing + the migration to control the target table name at runtime. + """ + return CreateTableExpression( + dialect, + table=table_name, + columns=[ + ColumnDefinition( + "id", + PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)], + ), + ColumnDefinition("value", PostgresCharacterVaryingType(255)), + ], + ) + + +def drop_custom_table(dialect, table_name: str = "custom_table"): + """DROP TABLE IF EXISTS .""" + return DropTableExpression(dialect, table=table_name, if_exists=True) \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/migrations.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/migrations.py new file mode 100644 index 00000000..ced2fb4e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/migrations.py @@ -0,0 +1,73 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/migrations.py +""" +NamedMigration subclasses for PostgreSQL migration examples. + +Each class represents a single schema migration with version, optional +dependencies, and up()/down() methods that call named expressions. +""" + +from rhosocial.activerecord.backend.migration import NamedMigration, MigrationContext + + +class V001CreateUsers(NamedMigration): + """Create the ``users`` table.""" + + version = "v001_create_users" + + def up(self, ctx: MigrationContext) -> None: + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" + ".expressions.create_users_table" + ) + + def down(self, ctx: MigrationContext) -> None: + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" + ".expressions.drop_users_table" + ) + + +class V002CreatePosts(NamedMigration): + """Create the ``posts`` table after ``users`` exists.""" + + version = "v002_create_posts" + dependencies = ["v001_create_users"] + + def up(self, ctx: MigrationContext) -> None: + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" + ".expressions.create_posts_table" + ) + + def down(self, ctx: MigrationContext) -> None: + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" + ".expressions.drop_posts_table" + ) + + +class V003CreateCustomTable(NamedMigration): + """Create a table with a user-specified name. + + Accepts a ``table_name`` parameter (default ``custom_table``). + Usage:: + + named-migration ... V003CreateCustomTable --param table_name=my_config + """ + + version = "v003_create_custom_table" + table_name: str = "custom_table" + + def up(self, ctx: MigrationContext) -> None: + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" + ".expressions.create_custom_table", + {"table_name": self.table_name}, + ) + + def down(self, ctx: MigrationContext) -> None: + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations" + ".expressions.drop_custom_table", + {"table_name": self.table_name}, + ) \ No newline at end of file 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 new file mode 100644 index 00000000..8effaa83 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py @@ -0,0 +1,93 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py +""" +Basic migration example — single migration UP then DOWN (PostgreSQL). + +This script demonstrates: + 1. Creating a PostgreSQL backend + 2. Running a single ``NamedMigration`` UP (creates ``users`` table) + 3. Verifying the table was created + 4. Running the same migration DOWN (drops ``users`` table) + 5. Showing JSON record store persistence + 6. Dry-run mode (no actual changes) + 7. Duplicate execution protection + +Usage: + python -m rhosocial.activerecord.backend.impl.postgres.examples.named_migrations.run_basic +""" + +from pathlib import Path +import tempfile +import os + +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.migration import ( + MigrationRunner, + MigrationDirection, + JSONFileMigrationRecordStore, + MigrationAlreadyAppliedError, +) + + +def main(): + print("=" * 60) + print("Named Migration Demo — Basic (PostgreSQL)") + print("=" * 60) + + config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), + ) + backend = PostgresBackend(connection_config=config) + backend.connect() + backend.introspect_and_adapt() + print("\n[1] PostgreSQL backend connected.") + + store_path = Path(tempfile.gettempdir()) / "mig_pg_basic.json" + if store_path.exists(): + store_path.unlink() + store = JSONFileMigrationRecordStore(store_path) + print(f"[2] Record store: {store_path}") + + fqn = ( + "rhosocial.activerecord.backend.impl.postgres.examples" + ".named_migrations.migrations.V001CreateUsers" + ) + runner = MigrationRunner(fqn) + + print("\n[3] Dry-run (UP) — no actual changes …") + result = runner.run(backend, MigrationDirection.UP, dry_run=True) + print(f" Result: version={result.version}, success={result.success}") + print(" ✓ Dry-run completed (table not created).") + + print("\n[4] Applying v001_create_users (UP) …") + result = runner.run(backend, MigrationDirection.UP, record_store=store) + print(f" Result: version={result.version}, success={result.success}") + print(" ✓ Table 'users' created.") + + print("\n[5] Duplicate UP (should be rejected) …") + try: + runner.run(backend, MigrationDirection.UP, record_store=store) + print(" ✗ ERROR: should have raised!") + except MigrationAlreadyAppliedError as e: + print(f" ✓ {e}") + + print("\n[6] Rolling back v001_create_users (DOWN) …") + result = runner.run(backend, MigrationDirection.DOWN, record_store=store) + print(f" Result: version={result.version}, success={result.success}") + print(" ✓ Table 'users' dropped.") + + applied = store.get_applied() + print(f"\n[7] Applied migrations: {len(applied)} (should be 0)") + + backend.disconnect() + if store_path.exists(): + store_path.unlink() + print("\n=== PostgreSQL basic migration demo completed ===") + + +if __name__ == "__main__": + main() \ No newline at end of file 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 new file mode 100644 index 00000000..f2c4f07e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py @@ -0,0 +1,102 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py +""" +Multi-step migration chain example — two migrations with dependency (PostgreSQL). + +This script demonstrates: + 1. Running v001_create_users (creates ``users`` table) + 2. Running v002_create_posts (creates ``posts`` table, depends on v001) + 3. Dependency validation (v002 fails if v001 is not applied) + 4. Rolling back in reverse order + 5. Tracking applied migrations in JSON record store + +Usage: + python -m rhosocial.activerecord.backend.impl.postgres.examples.named_migrations.run_chain +""" + +from pathlib import Path +import tempfile +import os + +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.migration import ( + MigrationRunner, + MigrationDirection, + JSONFileMigrationRecordStore, + MigrationDependencyError, + MigrationNotAppliedError, +) + +BASE = "rhosocial.activerecord.backend.impl.postgres.examples.named_migrations.migrations" + + +def main(): + print("=" * 60) + print("Named Migration Demo — Dependency Chain (PostgreSQL)") + print("=" * 60) + + config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), + ) + backend = PostgresBackend(connection_config=config) + backend.connect() + backend.introspect_and_adapt() + + store_path = Path(tempfile.gettempdir()) / "mig_pg_chain.json" + if store_path.exists(): + store_path.unlink() + store = JSONFileMigrationRecordStore(store_path) + print(f"\n[1] Record store: {store_path}") + + r1 = MigrationRunner(f"{BASE}.V001CreateUsers") + r2 = MigrationRunner(f"{BASE}.V002CreatePosts") + + print("\n[2] Attempt v002 before v001 (should fail) …") + try: + r2.run(backend, MigrationDirection.UP, record_store=store) + print(" ✗ ERROR: should have raised!") + except MigrationDependencyError as e: + print(f" ✓ {e}") + + print("\n[3] Applying v001_create_users …") + r1.run(backend, MigrationDirection.UP, record_store=store) + print(" ✓ Table 'users' created.") + + print("\n[4] Applying v002_create_posts …") + r2.run(backend, MigrationDirection.UP, record_store=store) + print(" ✓ Table 'posts' created (dependency check passed).") + + print("\n[5] Applied migrations:") + for rec in store.get_applied(): + print(f" - {rec.version}") + + print("\n[6] Rolling back v002_create_posts …") + r2.run(backend, MigrationDirection.DOWN, record_store=store) + print(" ✓ Table 'posts' dropped; 'users' still exists.") + + print("\n[7] Rolling back v001_create_users …") + r1.run(backend, MigrationDirection.DOWN, record_store=store) + print(" ✓ Table 'users' dropped.") + + assert len(store.get_applied()) == 0 + print("\n[8] All migrations rolled back (0 applied).") + + print("\n[9] DOWN on unapplied migration (should fail) …") + try: + r1.run(backend, MigrationDirection.DOWN, record_store=store) + print(" ✗ ERROR: should have raised!") + except MigrationNotAppliedError as e: + print(f" ✓ {e}") + + backend.disconnect() + if store_path.exists(): + store_path.unlink() + print("\n=== PostgreSQL chain migration demo completed ===") + + +if __name__ == "__main__": + main() \ No newline at end of file From e45fc7906344f77bb32cef28545ac70ef3914772 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 5 Jul 2026 16:31:41 +0800 Subject: [PATCH 06/39] feat(postgres): fulltext_search capability, benchmark markers, composite_pk mapped provider, partition explain test - mixins/ddl/index: implement supports_fulltext_search() (tsvector/tsquery via GIN), supports_fulltext_boolean_mode() (tsquery boolean operators), and refine docstrings distinguishing DDL (CREATE FULLTEXT INDEX, False) from query capability (True). - pyproject: register benchmark* pytest markers. - tests/providers/composite_pk: add MappedOrderItem/Async setup helpers. - Add tests/.../feature/backend/test_partition_explain.py covering partition EXPLAIN plan introspection. --- pyproject.toml | 6 + .../backend/impl/postgres/mixins/ddl/index.py | 137 ++++-- tests/providers/composite_pk.py | 8 + .../feature/backend/test_partition_explain.py | 433 ++++++++++++++++++ 4 files changed, 557 insertions(+), 27 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py diff --git a/pyproject.toml b/pyproject.toml index 77ae651d..0ae319a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,12 @@ markers = [ "requires_functions: marks tests requiring specific database functions", "requires_inner_join: marks tests requiring INNER JOIN support", "benchmark_fastapi: marks FastAPI benchmark tests", + "benchmark: marks benchmark tests", + "benchmark_backend: marks backend benchmark tests", + "benchmark_async: marks async benchmark tests", + "benchmark_sync: marks sync benchmark tests", + "benchmark_write: marks write benchmark tests", + "benchmark_read: marks read benchmark tests", ] [tool.coverage.run] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py index 2e4d1282..3e576e0b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py @@ -194,51 +194,134 @@ def get_supported_index_types(self) -> List[str]: return ["BTREE", "HASH", "GIST", "GIN", "SPGIST", "BRIN"] # ========================================================================= - # Fulltext methods — PostgreSQL does NOT use MySQL-style FULLTEXT indexes + # Fulltext search — PostgreSQL uses tsvector/tsquery # ========================================================================= + _FT_DEFAULT_CONFIG = 'english' + def supports_fulltext_index(self) -> bool: - """PostgreSQL uses GIN index + tsvector/tsquery, not MySQL-style FULLTEXT.""" + """Whether MySQL-style FULLTEXT indexes are supported. + + PostgreSQL does not use CREATE FULLTEXT INDEX; instead it uses + CREATE INDEX ... USING GIN (to_tsvector(...)). + """ return False + def supports_fulltext_search(self) -> bool: + """Whether full-text search is supported. + + PostgreSQL supports full-text search via to_tsvector() @@ to_tsquery() + with GIN/GIST indexes, not via MySQL-style MATCH...AGAINST. + """ + return True + def supports_fulltext_parser(self) -> bool: """PostgreSQL does not have MySQL-style FULLTEXT parsers.""" return False def supports_fulltext_boolean_mode(self) -> bool: - """PostgreSQL does not support MySQL-style FULLTEXT boolean mode.""" - return False + """Whether tsquery-style boolean operators are supported. + + PostgreSQL's to_tsquery() supports & (AND), | (OR), ! (NOT) + operators, equivalent to MySQL's BOOLEAN MODE. + """ + return True def supports_fulltext_query_expansion(self) -> bool: - """PostgreSQL does not support MySQL-style FULLTEXT query expansion.""" + """Whether query expansion is supported. + + PostgreSQL supports this via the thesaurus dictionary or + phraseto_tsquery() for phrase matching. + """ return False - def format_fulltext_match(self, columns, search_term, mode=None) -> Tuple[str, Tuple]: - """Not supported — PostgreSQL uses tsvector @@ tsquery instead.""" - from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError - raise UnsupportedFeatureError( - "PostgreSQL", - "FULLTEXT MATCH", - suggestion="Use tsvector @@ tsquery for full-text search.", - ) + def format_fulltext_match( + self, columns: List[str], search_term: str, mode: Optional[str] = None + ) -> Tuple[str, Tuple]: + """Format tsvector @@ tsquery expression for PostgreSQL full-text search. + + Produces: to_tsvector('config', col1 || ' ' || col2) @@ tsquery_func(search_term) + + Args: + columns: Columns to include in the tsvector expression + search_term: Search term to convert to tsquery + mode: Search mode: + - None / 'NATURAL LANGUAGE': plainto_tsquery (plain text) + - 'BOOLEAN': to_tsquery (operators &, |, !) + - 'PHRASE': phraseto_tsquery (phrase matching) + + Returns: + Tuple of (SQL string, parameters tuple) + """ + config = self._FT_DEFAULT_CONFIG + ph = self.get_parameter_placeholder() + + if len(columns) == 1: + tsvector_expr = f"to_tsvector('{config}', {self.format_identifier(columns[0])})" + else: + parts = [self.format_identifier(c) for c in columns] + tsvector_expr = f"to_tsvector('{config}', {' || ' .join(parts)})" + + if mode: + mode_upper = mode.upper() + if mode_upper == "BOOLEAN": + tsquery_expr = f"to_tsquery('{config}', {ph})" + elif mode_upper == "PHRASE": + tsquery_expr = f"phraseto_tsquery('{config}', {ph})" + else: + tsquery_expr = f"plainto_tsquery('{config}', {ph})" + else: + tsquery_expr = f"plainto_tsquery('{config}', {ph})" + + return f"{tsvector_expr} @@ {tsquery_expr}", (search_term,) def format_create_fulltext_index_statement(self, expr) -> Tuple[str, tuple]: - """Not supported — use CREATE INDEX with GIN and tsvector expression.""" - from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError - raise UnsupportedFeatureError( - "PostgreSQL", - "CREATE FULLTEXT INDEX", - suggestion="Use CREATE INDEX ... ON ... USING GIN (to_tsvector(...))", - ) + """Format CREATE INDEX ... USING GIN for PostgreSQL full-text search. + + Converts a MySQL-style CREATE FULLTEXT INDEX expression into a + PostgreSQL GIN index on to_tsvector(). + + Args: + expr: CreateFulltextIndexExpression object + + Returns: + Tuple of (SQL string, parameters tuple) + """ + config = self._FT_DEFAULT_CONFIG + parts = ["CREATE INDEX"] + if expr.if_not_exists: + parts.append("IF NOT EXISTS") + parts.append(self.format_identifier(expr.index_name)) + parts.append("ON") + parts.append(self.format_identifier(expr.table_name)) + parts.append("USING GIN") + + if len(expr.columns) == 1: + tsvector_expr = f"to_tsvector('{config}', {self.format_identifier(expr.columns[0])})" + else: + col_parts = [self.format_identifier(c) for c in expr.columns] + tsvector_expr = f"to_tsvector('{config}', {' || ' .join(col_parts)})" + parts.append(f"({tsvector_expr})") + + return " ".join(parts), () def format_drop_fulltext_index_statement(self, expr) -> Tuple[str, tuple]: - """Not supported — use DROP INDEX instead.""" - from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError - raise UnsupportedFeatureError( - "PostgreSQL", - "DROP FULLTEXT INDEX", - suggestion="Use DROP INDEX to remove a GIN index.", - ) + """Format DROP INDEX statement for PostgreSQL. + + PostgreSQL uses DROP INDEX (without ON table) for all index types, + including GIN indexes used for full-text search. + + Args: + expr: DropFulltextIndexExpression object + + Returns: + Tuple of (SQL string, parameters tuple) + """ + parts = ["DROP INDEX"] + if expr.if_exists: + parts.append("IF EXISTS") + parts.append(self.format_identifier(expr.index_name)) + return " ".join(parts), () def format_create_index_statement(self, expr: "CreateIndexExpression") -> Tuple[str, tuple]: """Format CREATE INDEX statement with PostgreSQL-specific options. diff --git a/tests/providers/composite_pk.py b/tests/providers/composite_pk.py index 93eed043..a2f7f39f 100644 --- a/tests/providers/composite_pk.py +++ b/tests/providers/composite_pk.py @@ -8,11 +8,13 @@ OrderItem as OrderItemBase, StoreInventory as StoreInventoryBase, Order as OrderBase, + MappedOrderItem as MappedOrderItemBase, ) from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( AsyncOrderItem as AsyncOrderItemBase, AsyncStoreInventory as AsyncStoreInventoryBase, AsyncOrder as AsyncOrderBase, + AsyncMappedOrderItem as AsyncMappedOrderItemBase, ) from .scenarios import get_enabled_scenarios, get_scenario @@ -87,6 +89,12 @@ async def setup_async_store_inventory_model(self, scenario_name: str) -> Type[Ac async def setup_async_order_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncOrderBase, scenario_name, "orders") + def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + return self._setup_model(MappedOrderItemBase, scenario_name, "order_items") + + async def setup_async_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncMappedOrderItemBase, scenario_name, "order_items") + def cleanup_after_test(self, scenario_name: str): tables = ["order_items", "store_inventory", "orders"] for backend in self._active_backends: 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 new file mode 100644 index 00000000..772cfba6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py @@ -0,0 +1,433 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py +"""PostgreSQL EXPLAIN tests for partitioned tables.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict + +import pytest +import pytest_asyncio + +from rhosocial.activerecord.backend.expression import ( + Column, + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, + CreateTableExpression, + DropTableExpression, + InsertExpression, + Literal, + QueryExpression, + TableExpression, + ValuesSource, + WildcardExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ExplainExpression, + ExplainFormat, + ExplainOptions, + PartitionClause, + PartitionStrategy, + TableConstraint, + 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.expression import ( + PostgresCreatePartitionExpression, +) + +PARTITION_EXPLAIN_TABLE = "ar_pg_partition_explain_events" +PARTITION_NAMES = { + "p2026_01": ("ar_pg_partition_explain_events_p2026_01", "2026-01-01", "2026-02-01"), + "p2026_02": ("ar_pg_partition_explain_events_p2026_02", "2026-02-01", "2026-03-01"), + "p2026_03": ("ar_pg_partition_explain_events_p2026_03", "2026-03-01", "2026-04-01"), +} + + +def _all_tables(): + tables = [PARTITION_EXPLAIN_TABLE] + for _, name, _, _ in _iter_partitions(): + tables.append(name) + return tables + + +def _iter_partitions(): + for key, (name, from_val, to_val) in PARTITION_NAMES.items(): + yield key, name, from_val, to_val + + +def _drop_table_sql(dialect, table_name: str): + return DropTableExpression(dialect=dialect, table=table_name, if_exists=True).to_sql() + + +def _create_parent_table_expression(dialect): + return CreateTableExpression( + dialect=dialect, + table=PARTITION_EXPLAIN_TABLE, + if_not_exists=False, + columns=[ + ColumnDefinition("id", BigIntType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(6), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tenant_id", BigIntType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), + ], + table_constraints=[ + TableConstraint( + TableConstraintType.PRIMARY_KEY, + columns=["id", "created_at"], + ), + ], + partition=PartitionClause( + dialect=dialect, + method=PartitionStrategy.RANGE, + keys=[Column(dialect, "created_at")], + ), + ) + + +def _create_partition_sql(dialect, partition_name: str, from_value: str, to_value: str): + expr = PostgresCreatePartitionExpression( + dialect=dialect, + partition_name=partition_name, + parent_table=PARTITION_EXPLAIN_TABLE, + partition_type="RANGE", + partition_values={"from": from_value, "to": to_value}, + ) + return expr.to_sql() + + +def _seed_rows_expression(dialect): + rows = [ + [1, datetime(2026, 1, 15), 100, "jan"], + [2, datetime(2026, 2, 15), 100, "feb"], + [3, datetime(2026, 3, 15), 200, "mar"], + ] + return InsertExpression( + dialect=dialect, + into=PARTITION_EXPLAIN_TABLE, + columns=["id", "created_at", "tenant_id", "payload"], + source=ValuesSource( + dialect, + [[Literal(dialect, value) for value in row] for row in rows], + ), + ) + + +def _range_query_expression(dialect, start, end): + return QueryExpression( + dialect, + select=[WildcardExpression(dialect)], + from_=TableExpression(dialect, PARTITION_EXPLAIN_TABLE), + where=(Column(dialect, "created_at") >= Literal(dialect, start)) + & (Column(dialect, "created_at") < Literal(dialect, end)), + ) + + +def _full_scan_query_expression(dialect): + return QueryExpression( + dialect, + select=[WildcardExpression(dialect)], + from_=TableExpression(dialect, PARTITION_EXPLAIN_TABLE), + ) + + +def _json_explain_sql(dialect, query_expr): + expr = ExplainExpression( + dialect, + query_expr, + options=ExplainOptions(format=ExplainFormat.JSON), + ) + return expr.to_sql() + + +def _setup_partition_table(backend): + dialect = backend.dialect + for table in _all_tables(): + backend.execute(*_drop_table_sql(dialect, table)) + backend.execute(*_create_parent_table_expression(dialect).to_sql()) + for _, name, from_val, to_val in _iter_partitions(): + backend.execute(*_create_partition_sql(dialect, name, from_val, to_val)) + backend.execute(*_seed_rows_expression(dialect).to_sql()) + + +async def _async_setup_partition_table(backend): + dialect = backend.dialect + for table in _all_tables(): + await backend.execute(*_drop_table_sql(dialect, table)) + await backend.execute(*_create_parent_table_expression(dialect).to_sql()) + for _, name, from_val, to_val in _iter_partitions(): + await backend.execute(*_create_partition_sql(dialect, name, from_val, to_val)) + await backend.execute(*_seed_rows_expression(dialect).to_sql()) + + +def _teardown_partition_table(backend): + dialect = backend.dialect + for table in reversed(_all_tables()): + try: + backend.execute(*_drop_table_sql(dialect, table)) + except Exception: + pass + + +async def _async_teardown_partition_table(backend): + dialect = backend.dialect + for table in reversed(_all_tables()): + try: + await backend.execute(*_drop_table_sql(dialect, table)) + except Exception: + pass + + +def _flatten_plan_text(plan: Any, lines: list) -> None: + """Recursively extract text plan lines from a JSON EXPLAIN result.""" + if isinstance(plan, list): + for item in plan: + _flatten_plan_text(item, lines) + elif isinstance(plan, dict): + if "Plan" in plan: + _flatten_plan_text(plan["Plan"], lines) + for key, value in plan.items(): + if key in ("Plans", "Subplans"): + for sub in value: + _flatten_plan_text(sub, lines) + elif key == "Relation Name": + lines.append(value) + elif isinstance(plan, str): + lines.append(plan) + + +def _parse_json_plan(result: PostgresExplainResult) -> Any: + """Extract the JSON plan from a PostgresExplainResult. + + psycopg's JSON adapter may deserialize the column value automatically, + so we accept both parsed (list/dict) and unparsed (str) values. + """ + raw = result.raw_rows + if raw and isinstance(raw[0], dict) and "QUERY PLAN" in raw[0]: + plan_data = raw[0]["QUERY PLAN"] + if isinstance(plan_data, (list, dict)): + return plan_data + import json + return json.loads(plan_data) + if raw and isinstance(raw[0], tuple): + plan_data = raw[0][0] + if isinstance(plan_data, (list, dict)): + return plan_data + import json + return json.loads(plan_data) + return {} + + +def _extract_relation_names(result: PostgresExplainResult) -> list[str]: + """Extract partition relation names from a JSON-format EXPLAIN result.""" + plan = _parse_json_plan(result) + names: list[str] = [] + _flatten_plan_text(plan, names) + return names + + +def _assert_range_partition_support(dialect): + if not dialect.supports_partitioned_table_creation(): + pytest.skip("PostgreSQL scenario does not support declarative partitioning") + version = dialect.get_server_version() + if version and version[0] < 11: + pytest.skip("PostgreSQL 10 does not support primary keys on partitioned tables") + + +@pytest.fixture +def pg_partition_explain_backend(postgres_backend): + """Create a range-partitioned table for EXPLAIN tests.""" + _assert_range_partition_support(postgres_backend.dialect) + _setup_partition_table(postgres_backend) + yield postgres_backend + _teardown_partition_table(postgres_backend) + + +@pytest_asyncio.fixture +async def async_pg_partition_explain_backend(async_postgres_backend): + """Async counterpart for partition explain tests.""" + _assert_range_partition_support(async_postgres_backend.dialect) + await _async_setup_partition_table(async_postgres_backend) + yield async_postgres_backend + await _async_teardown_partition_table(async_postgres_backend) + + +def _combined_plan_text(result: PostgresExplainResult) -> str: + """Join all lines of the EXPLAIN TEXT output into a single string.""" + lines = [] + for row in result.raw_rows: + if isinstance(row, dict): + for val in row.values(): + lines.append(str(val)) + elif isinstance(row, tuple): + for val in row: + lines.append(str(val)) + else: + lines.append(str(row)) + return "\n".join(lines) + + +class TestPostgresPartitionExplain: + """Synchronous EXPLAIN tests for PostgreSQL partition pruning.""" + + def test_explain_partition_pruning_on_range_predicate( + self, + pg_partition_explain_backend, + ): + """Range predicate on partition key should prune unrelated partitions.""" + dialect = pg_partition_explain_backend.dialect + query = _range_query_expression( + dialect, + datetime(2026, 2, 1), + datetime(2026, 3, 1), + ) + result = pg_partition_explain_backend.explain(query) + assert isinstance(result, PostgresExplainResult) + assert len(result.raw_rows) > 0 + + def test_explain_json_reports_target_partition( + self, + pg_partition_explain_backend, + ): + """EXPLAIN FORMAT JSON should expose the pruned target partition name.""" + dialect = pg_partition_explain_backend.dialect + query = _range_query_expression( + dialect, + datetime(2026, 2, 1), + datetime(2026, 3, 1), + ) + sql, params = _json_explain_sql(dialect, query) + rows = pg_partition_explain_backend.fetch_all(sql, params) + result = PostgresExplainResult(raw_rows=rows, sql=sql, duration=0.0) + names = _extract_relation_names(result) + + expected = PARTITION_NAMES["p2026_02"][0] + assert expected in names, f"Expected partition {expected} in query plan, got {names}" + + def test_explain_json_full_scan_lists_scan_method( + self, + pg_partition_explain_backend, + ): + """Full scan on partitioned table should show an Append or Seq Scan plan.""" + dialect = pg_partition_explain_backend.dialect + sql, params = _json_explain_sql(dialect, _full_scan_query_expression(dialect)) + rows = pg_partition_explain_backend.fetch_all(sql, params) + result = PostgresExplainResult(raw_rows=rows, sql=sql, duration=0.0) + plan_text = str(result.raw_rows) + assert "Append" in plan_text or "Seq Scan" in plan_text + + def test_explain_with_verbose_contains_partition_info( + self, + pg_partition_explain_backend, + ): + """EXPLAIN VERBOSE should expose partition-related details.""" + dialect = pg_partition_explain_backend.dialect + query = _range_query_expression( + dialect, + datetime(2026, 1, 1), + datetime(2026, 2, 1), + ) + verbose_expr = ExplainExpression( + dialect, + query, + options=ExplainOptions(verbose=True), + ) + sql, params = verbose_expr.to_sql() + rows = pg_partition_explain_backend.fetch_all(sql, params) + result = PostgresExplainResult(raw_rows=rows, sql=sql, duration=0.0) + plan = _combined_plan_text(result) + expected = PARTITION_NAMES["p2026_01"][0] + assert expected in plan, f"Expected partition {expected} in verbose plan, got {plan}" + + def test_explain_full_scan_returns_rows( + self, + pg_partition_explain_backend, + ): + """Basic EXPLAIN on full scan should return a non-empty result.""" + dialect = pg_partition_explain_backend.dialect + result = pg_partition_explain_backend.explain( + _full_scan_query_expression(dialect) + ) + assert isinstance(result, PostgresExplainResult) + assert len(result.raw_rows) > 0 + + +class TestAsyncPostgresPartitionExplain: + """Asynchronous EXPLAIN tests for PostgreSQL partition pruning.""" + + @pytest.mark.asyncio + async def test_explain_partition_pruning_on_range_predicate( + self, + async_pg_partition_explain_backend, + ): + """Range predicate on partition key should prune unrelated partitions.""" + dialect = async_pg_partition_explain_backend.dialect + query = _range_query_expression( + dialect, + datetime(2026, 2, 1), + datetime(2026, 3, 1), + ) + result = await async_pg_partition_explain_backend.explain(query) + assert isinstance(result, PostgresExplainResult) + assert len(result.raw_rows) > 0 + + @pytest.mark.asyncio + async def test_explain_json_reports_target_partition( + self, + async_pg_partition_explain_backend, + ): + """EXPLAIN FORMAT JSON should expose the pruned target partition name.""" + dialect = async_pg_partition_explain_backend.dialect + query = _range_query_expression( + dialect, + datetime(2026, 2, 1), + datetime(2026, 3, 1), + ) + sql, params = _json_explain_sql(dialect, query) + rows = await async_pg_partition_explain_backend.fetch_all(sql, params) + result = PostgresExplainResult(raw_rows=rows, sql=sql, duration=0.0) + names = _extract_relation_names(result) + expected = PARTITION_NAMES["p2026_02"][0] + assert expected in names, f"Expected partition {expected} in query plan, got {names}" + + @pytest.mark.asyncio + async def test_explain_with_verbose_contains_partition_info( + self, + async_pg_partition_explain_backend, + ): + """EXPLAIN VERBOSE should expose partition-related details.""" + dialect = async_pg_partition_explain_backend.dialect + query = _range_query_expression( + dialect, + datetime(2026, 1, 1), + datetime(2026, 2, 1), + ) + verbose_expr = ExplainExpression( + dialect, + query, + options=ExplainOptions(verbose=True), + ) + sql, params = verbose_expr.to_sql() + rows = await async_pg_partition_explain_backend.fetch_all(sql, params) + result = PostgresExplainResult(raw_rows=rows, sql=sql, duration=0.0) + plan = _combined_plan_text(result) + expected = PARTITION_NAMES["p2026_01"][0] + assert expected in plan, f"Expected partition {expected} in verbose plan, got {plan}" + + @pytest.mark.asyncio + async def test_explain_full_scan_returns_rows( + self, + async_pg_partition_explain_backend, + ): + """Basic EXPLAIN on full scan should return a non-empty result.""" + dialect = async_pg_partition_explain_backend.dialect + result = await async_pg_partition_explain_backend.explain( + _full_scan_query_expression(dialect) + ) + assert isinstance(result, PostgresExplainResult) + assert len(result.raw_rows) > 0 From aca738b2f302accdca4180f07ca0f9653fd0adbc Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 6 Jul 2026 16:52:55 +0800 Subject: [PATCH 07/39] test(postgres): rewrite fulltext tests to reflect tsvector search capability The TestFulltextNotSupported class previously assumed PostgreSQL opposes ALL fulltext features and that the format_* helpers raise UnsupportedFeatureError. That contract is stale: PostgresIndexMixin overrides supports_fulltext_search/supports_fulltext_boolean_mode to return True (queried via tsvector @@ tsquery), and format_fulltext_match / format_create_fulltext_index_statement / format_drop_fulltext_index_statement return working SQL (plainto_tsquery / to_tsquery / CREATE INDEX USING GIN / DROP INDEX). - Rename to TestFulltextDdlNotSupportedSearchSupported to express the decoupled DDL-vs-query contract introduced by core PR #110. - Keep the still-unsupported DDL flags (supports_fulltext_index, supports_fulltext_parser, supports_fulltext_query_expansion) as False. - Add positive coverage for supports_fulltext_search, supports_fulltext_boolean_mode. - Add tsvector/tsquery SQL-shape assertions for natural-language mode, boolean mode, and multi-column variants. - Add CREATE INDEX ... USING GIN / DROP INDEX shape assertions (with and without IF [NOT] EXISTS), constructing CreateFulltextIndexExpression / DropFulltextIndexExpression with a dialect instance as required by BaseExpression. --- .../feature/backend/postgres/test_index_pg.py | 102 +++++++++++++++--- 1 file changed, 88 insertions(+), 14 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py index 19e40de9..6be4882a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py @@ -20,7 +20,9 @@ from rhosocial.activerecord.backend.expression.statements.ddl_table import IndexDefinition from rhosocial.activerecord.backend.expression.statements.ddl_index import ( + CreateFulltextIndexExpression, CreateIndexExpression, + DropFulltextIndexExpression, DropIndexExpression, ) from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect @@ -472,8 +474,17 @@ def test_get_supported_index_types(self): assert types == ["BTREE", "HASH", "GIST", "GIN", "SPGIST", "BRIN"] -class TestFulltextNotSupported: - """PostgreSQL does not support MySQL-style FULLTEXT indexes.""" +class TestFulltextDdlNotSupportedSearchSupported: + """PostgreSQL full-text search via tsvector/tsquery, not MySQL-style DDL. + + PostgreSQL does NOT support ``CREATE FULLTEXT INDEX`` (MySQL-style) — + it uses ``CREATE INDEX ... USING GIN (to_tsvector(...))`` instead. + However, full-text *search querying* IS supported through + ``tsvector @@ tsquery`` operators, so the dialect reports + ``supports_fulltext_search()`` True independently of the DDL capability. + """ + + # --- DDL capability (MySQL-style CREATE FULLTEXT INDEX) --- def test_supports_fulltext_index(self): assert PostgresDialect().supports_fulltext_index() is False @@ -481,23 +492,86 @@ def test_supports_fulltext_index(self): def test_supports_fulltext_parser(self): assert PostgresDialect().supports_fulltext_parser() is False - def test_supports_fulltext_boolean_mode(self): - assert PostgresDialect().supports_fulltext_boolean_mode() is False - def test_supports_fulltext_query_expansion(self): assert PostgresDialect().supports_fulltext_query_expansion() is False - def test_format_fulltext_match_raises(self): - with pytest.raises(UnsupportedFeatureError): - PostgresDialect().format_fulltext_match(["col"], "search") + # --- Query capability (tsvector @@ tsquery) --- + + def test_supports_fulltext_search(self): + assert PostgresDialect().supports_fulltext_search() is True + + def test_supports_fulltext_boolean_mode(self): + # to_tsquery() supports &, |, ! — equivalent to MySQL BOOLEAN MODE. + assert PostgresDialect().supports_fulltext_boolean_mode() is True + + def test_format_fulltext_match_natural_language(self): + sql, params = PostgresDialect().format_fulltext_match(["col"], "search") + assert "to_tsvector" in sql + assert "plainto_tsquery" in sql + assert "@@" in sql + assert params == ("search",) + + def test_format_fulltext_match_boolean(self): + sql, params = PostgresDialect().format_fulltext_match(["col"], "a & b", mode="BOOLEAN") + assert "to_tsvector" in sql + assert "to_tsquery" in sql + assert "plainto_tsquery" not in sql + assert params == ("a & b",) + + def test_format_fulltext_match_multi_column(self): + sql, params = PostgresDialect().format_fulltext_match(["a", "b"], "search") + assert " || " in sql + assert "to_tsvector" in sql + assert params == ("search",) + + def test_format_create_fulltext_index_statement_uses_gin(self): + dialect = PostgresDialect() + expr = CreateFulltextIndexExpression( + dialect, + index_name="idx_ft", + table_name="articles", + columns=["body"], + ) + sql, params = dialect.format_create_fulltext_index_statement(expr) + assert sql.startswith("CREATE INDEX") + assert "USING GIN" in sql + assert "to_tsvector" in sql + assert "articles" in sql + assert params == () + + def test_format_create_fulltext_index_statement_if_not_exists(self): + dialect = PostgresDialect() + expr = CreateFulltextIndexExpression( + dialect, + index_name="idx_ft", + table_name="articles", + columns=["body"], + if_not_exists=True, + ) + sql, _ = dialect.format_create_fulltext_index_statement(expr) + assert "IF NOT EXISTS" in sql - def test_format_create_fulltext_index_statement_raises(self): - with pytest.raises(UnsupportedFeatureError): - PostgresDialect().format_create_fulltext_index_statement(None) + def test_format_drop_fulltext_index_statement(self): + dialect = PostgresDialect() + expr = DropFulltextIndexExpression( + dialect, + index_name="idx_ft", + table_name="articles", + ) + sql, _ = dialect.format_drop_fulltext_index_statement(expr) + assert sql.startswith("DROP INDEX") + assert "idx_ft" in sql - def test_format_drop_fulltext_index_statement_raises(self): - with pytest.raises(UnsupportedFeatureError): - PostgresDialect().format_drop_fulltext_index_statement(None) + def test_format_drop_fulltext_index_statement_if_exists(self): + dialect = PostgresDialect() + expr = DropFulltextIndexExpression( + dialect, + index_name="idx_ft", + table_name="articles", + if_exists=True, + ) + sql, _ = dialect.format_drop_fulltext_index_statement(expr) + assert "IF EXISTS" in sql class TestCreateIndexExpression: From ab3be400f7936494d66fdc445b4ec5653a09ec16 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 6 Jul 2026 17:08:42 +0800 Subject: [PATCH 08/39] fix(backend): defensively consume cursor results in get_server_version() get_server_version() called fetchone() on SELECT version() but did not consume any remaining results before closing the cursor. Some database drivers can leave stale result-set metadata on the connection, causing the next cursor opened on the same connection to read a polluted cursor.description. This mirrors the same fix applied to the core SQLite backend and the MySQL backend. Co-Authored-By: Claude Fable 5 --- .../impl/postgres/backend/async_backend.py | 2 + .../backend/impl/postgres/backend/sync.py | 2 + .../feature/backend/test_cursor_pollution.py | 161 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py 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 7aba7eb1..19aeea55 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -346,6 +346,8 @@ async def get_server_version(self) -> Optional[Tuple[int, int, int]]: cursor = await self._get_cursor() await cursor.execute("SELECT version()") version_row = await cursor.fetchone() + # Defensively consume remaining results to prevent cursor.description pollution + remaining = await cursor.fetchall() if not version_row or not version_row[0]: self.log(logging.WARNING, "PostgreSQL version query returned no result") return None diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index b23d26fa..dce98ddb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -347,6 +347,8 @@ def get_server_version(self) -> Optional[Tuple[int, int, int]]: cursor = self._get_cursor() cursor.execute("SELECT version()") result = cursor.fetchone() + # Defensively consume remaining results to prevent cursor.description pollution + cursor.fetchall() if not result or not result[0]: self.log(logging.WARNING, "PostgreSQL version query returned no result") return None diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py new file mode 100644 index 00000000..a2787e23 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py @@ -0,0 +1,161 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py +""" +PostgreSQL cursor result set pollution tests. + +Verifies that after get_server_version() executes SELECT version() +and closes the cursor, subsequent queries on the same connection +do not see polluted cursor.description. +""" + +import logging + +import pytest + +logger = logging.getLogger(__name__) + +pytestmark = [pytest.mark.feature, pytest.mark.backend] + + +class TestPostgresCursorPollution: + """Cursor pollution: sync PostgreSQL backend.""" + + def test_get_server_version_then_query(self, pg_backend): + """get_server_version() then a normal query.""" + backend = pg_backend + version = backend.get_server_version() + assert version is not None + + cursor = backend._get_cursor() + cursor.execute("SELECT 1 AS marker") + rows = cursor.fetchall() + cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "marker", ( + f"cursor.description polluted! expected 'marker', got: {col_name}" + ) + + def test_introspect_and_adapt_then_query(self, pg_backend): + """introspect_and_adapt() then a normal query.""" + backend = pg_backend + backend.introspect_and_adapt() + + cursor = backend._get_cursor() + cursor.execute("SELECT 'ok' AS status") + rows = cursor.fetchall() + cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "status", ( + f"cursor.description polluted! expected 'status', got: {col_name}" + ) + + def test_high_frequency_version_query_cycle(self, pg_backend): + """Repeated get_server_version → query cycle to expose state leaks.""" + backend = pg_backend + + for i in range(200): + backend.get_server_version() + + cursor = backend._get_cursor() + cursor.execute(f"SELECT {i} AS cycle") + rows = cursor.fetchall() + cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "cycle", ( + f"Iteration {i}: cursor.description polluted! " + f"expected 'cycle', got: {col_name}" + ) + + logger.info("PostgreSQL high-frequency version query cycle 200 iterations passed") + + +class TestAsyncPostgresCursorPollution: + """Cursor pollution: async PostgreSQL backend.""" + + @pytest.mark.asyncio + async def test_get_server_version_then_query(self, async_pg_backend): + """Async get_server_version() then a normal query.""" + backend = async_pg_backend + version = await backend.get_server_version() + assert version is not None + + cursor = await backend._get_cursor() + await cursor.execute("SELECT 1 AS marker") + rows = await cursor.fetchall() + await cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "marker", ( + f"Async cursor.description polluted! expected 'marker', got: {col_name}" + ) + + @pytest.mark.asyncio + async def test_introspect_and_adapt_then_query(self, async_pg_backend): + """Async introspect_and_adapt() then a normal query.""" + backend = async_pg_backend + await backend.introspect_and_adapt() + + cursor = await backend._get_cursor() + await cursor.execute("SELECT 'ok' AS status") + rows = await cursor.fetchall() + await cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "status", ( + f"Async cursor.description polluted after introspect! " + f"expected 'status', got: {col_name}" + ) + + @pytest.mark.asyncio + async def test_context_entry_workflow(self, async_pg_backend): + """backend.context() workflow (production pattern).""" + backend = async_pg_backend + + async with backend.context(): + cursor = await backend._get_cursor() + await cursor.execute("SELECT 'ctx_ok' AS ctx_status") + rows = await cursor.fetchall() + await cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "ctx_status", ( + f"Async cursor.description polluted after context()! " + f"expected 'ctx_status', got: {col_name}" + ) + + @pytest.mark.asyncio + async def test_high_frequency_version_query_cycle(self, async_pg_backend): + """Async high-frequency version query cycle to expose state leaks.""" + backend = async_pg_backend + + for i in range(200): + await backend.get_server_version() + + cursor = await backend._get_cursor() + await cursor.execute(f"SELECT {i} AS cycle") + rows = await cursor.fetchall() + await cursor.close() + + assert len(rows) > 0 + assert cursor.description is not None + col_name = cursor.description[0][0] + assert col_name == "cycle", ( + f"Iteration {i}: async cursor.description polluted! " + f"expected 'cycle', got: {col_name}" + ) + + logger.info("Async PostgreSQL high-frequency version query cycle 200 iterations passed") \ No newline at end of file From 300b87c677097aaf9e5adb7f59a3f30ddd114d31 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 7 Jul 2026 10:21:55 +0800 Subject: [PATCH 09/39] feat(pgq): gate quantified path and comma-separated patterns as unsupported on PG 19 PostgreSQL 19 does not support: - Variable-length (quantified) path patterns (+/*/{n,m}) - Comma-separated multiple patterns in a single MATCH clause Add capability overrides returning False, document known limitations, and add tests for the new gating methods. --- .../postgres/mixins/property_graph_query.py | 15 ++++++++++ .../test_property_graph_query_format.py | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py index 8af4c289..806bd957 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/property_graph_query.py @@ -5,6 +5,11 @@ - GRAPH_TABLE (MATCH ... COLUMNS ...) - CREATE / DROP / ALTER PROPERTY GRAPH - Vertex/edge pattern matching with WHERE + +Known PG 19 PGQ limitations (not supported): +- Variable-length (quantified) path patterns (``+``, ``*``, ``{n,m}``) +- Comma-separated multiple patterns in a single MATCH clause +- Heterogeneous graphs require explicit edge label specification """ @@ -18,6 +23,16 @@ def supports_graph_match(self) -> bool: """MATCH clause is supported since PostgreSQL 19.""" return self.version >= (19, 0, 0) + def supports_quantified_path(self) -> bool: + """Quantified (variable-length) path patterns are **not** supported + in PostgreSQL 19.""" + return False + + def supports_comma_separated_patterns(self) -> bool: + """Comma-separated multi-pattern MATCH is **not** supported + in PostgreSQL 19.""" + return False + def supports_graph_table(self) -> bool: """GRAPH_TABLE expression is supported since PostgreSQL 19.""" return self.version >= (19, 0, 0) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py index 723d351c..2318f5cf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py @@ -52,6 +52,34 @@ def test_graph_support_protocol_implemented_pg15(self): assert isinstance(d, GraphTableSupport) +class TestPGQLimitationGating: + """Tests that PG 19 correctly reports unsupported features.""" + + def test_quantified_path_unsupported_pg19(self): + d = PostgresDialect((19, 0, 0)) + assert d.supports_quantified_path() is False + + def test_quantified_path_unsupported_pg15(self): + d = PostgresDialect((15, 0, 0)) + assert d.supports_quantified_path() is False + + def test_comma_separated_patterns_unsupported_pg19(self): + d = PostgresDialect((19, 0, 0)) + assert d.supports_comma_separated_patterns() is False + + def test_comma_separated_patterns_unsupported_pg15(self): + d = PostgresDialect((15, 0, 0)) + assert d.supports_comma_separated_patterns() is False + + def test_quantified_path_raises_on_format(self): + d = PostgresDialect((19, 0, 0)) + from rhosocial.activerecord.backend.expression import QuantifiedPath + edge = GraphEdge(d, "e", "knows", GraphEdgeDirection.RIGHT) + qp = QuantifiedPath(d, edge) + with pytest.raises(UnsupportedFeatureError): + qp.to_sql() + + @pytest.fixture def pg19_dialect(): return PostgresDialect((19, 0, 0)) From 23be8032c3e7de83f936d1d3315aac27cf3a2964 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 7 Jul 2026 10:51:39 +0800 Subject: [PATCH 10/39] fix: use correct fixture names 'postgres_backend' / 'async_postgres_backend' in cursor pollution tests The cursor pollution tests referenced 'pg_backend' and 'async_pg_backend' which do not exist as pytest fixtures. The correct names are 'postgres_backend' and 'async_postgres_backend', matching the actual fixture definitions used by the rest of the test suite. --- .../feature/backend/test_cursor_pollution.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py index a2787e23..c1fc74cd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py @@ -19,9 +19,9 @@ class TestPostgresCursorPollution: """Cursor pollution: sync PostgreSQL backend.""" - def test_get_server_version_then_query(self, pg_backend): + def test_get_server_version_then_query(self, postgres_backend): """get_server_version() then a normal query.""" - backend = pg_backend + backend = postgres_backend version = backend.get_server_version() assert version is not None @@ -37,9 +37,9 @@ def test_get_server_version_then_query(self, pg_backend): f"cursor.description polluted! expected 'marker', got: {col_name}" ) - def test_introspect_and_adapt_then_query(self, pg_backend): + def test_introspect_and_adapt_then_query(self, postgres_backend): """introspect_and_adapt() then a normal query.""" - backend = pg_backend + backend = postgres_backend backend.introspect_and_adapt() cursor = backend._get_cursor() @@ -54,9 +54,9 @@ def test_introspect_and_adapt_then_query(self, pg_backend): f"cursor.description polluted! expected 'status', got: {col_name}" ) - def test_high_frequency_version_query_cycle(self, pg_backend): + def test_high_frequency_version_query_cycle(self, postgres_backend): """Repeated get_server_version → query cycle to expose state leaks.""" - backend = pg_backend + backend = postgres_backend for i in range(200): backend.get_server_version() @@ -81,9 +81,9 @@ class TestAsyncPostgresCursorPollution: """Cursor pollution: async PostgreSQL backend.""" @pytest.mark.asyncio - async def test_get_server_version_then_query(self, async_pg_backend): + async def test_get_server_version_then_query(self, async_postgres_backend): """Async get_server_version() then a normal query.""" - backend = async_pg_backend + backend = async_postgres_backend version = await backend.get_server_version() assert version is not None @@ -100,9 +100,9 @@ async def test_get_server_version_then_query(self, async_pg_backend): ) @pytest.mark.asyncio - async def test_introspect_and_adapt_then_query(self, async_pg_backend): + async def test_introspect_and_adapt_then_query(self, async_postgres_backend): """Async introspect_and_adapt() then a normal query.""" - backend = async_pg_backend + backend = async_postgres_backend await backend.introspect_and_adapt() cursor = await backend._get_cursor() @@ -119,9 +119,9 @@ async def test_introspect_and_adapt_then_query(self, async_pg_backend): ) @pytest.mark.asyncio - async def test_context_entry_workflow(self, async_pg_backend): + async def test_context_entry_workflow(self, async_postgres_backend): """backend.context() workflow (production pattern).""" - backend = async_pg_backend + backend = async_postgres_backend async with backend.context(): cursor = await backend._get_cursor() @@ -138,9 +138,9 @@ async def test_context_entry_workflow(self, async_pg_backend): ) @pytest.mark.asyncio - async def test_high_frequency_version_query_cycle(self, async_pg_backend): + async def test_high_frequency_version_query_cycle(self, async_postgres_backend): """Async high-frequency version query cycle to expose state leaks.""" - backend = async_pg_backend + backend = async_postgres_backend for i in range(200): await backend.get_server_version() From f146d54d4555f2433e380fcb26bbae9052c353a6 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 7 Jul 2026 23:12:19 +0800 Subject: [PATCH 11/39] feat: sync bridge files with testsuite rename and add 4 async relation bridges - Update bridge files to match testsuite rename: test_sync_queries -> test_queries - Add 4 new async relation bridge files (test_base_async, test_cache_async, test_interfaces_async, test_nested_relationship_access_async) - Include pgq/ example directory --- .../impl/postgres/examples/conftest.py | 8 + .../impl/postgres/examples/pgq}/__init__.py | 0 .../pgq/recursive_cte_graph_traversal.py | 389 ++++++++++++++ tests/benchmark/activerecord_bulk/__init__.py | 0 .../activerecord_bulk/test_bulk_create.py | 4 +- tests/benchmark/backend/__init__.py | 0 .../test_recursive_cte_graph_traversal.py | 502 ++++++++++++++++++ .../connection/test_active_record_context.py | 2 + .../connection/test_active_record_crud.py | 2 + .../feature/basic/test_basic_mapped_models.py | 2 + .../feature/basic/test_bulk_operations.py | 2 + .../feature/basic/test_crud.py | 2 + .../basic/test_field_column_mapping.py | 2 + .../feature/basic/test_fields.py | 2 + .../basic/test_pydantic_native_validation.py | 2 + .../feature/basic/test_type_adapter.py | 2 + .../feature/basic/test_validation.py | 2 + .../worker/test_connection_management.py | 2 + .../basic/worker/test_parallel_crud.py | 2 + .../worker/test_worker_pool_lifecycle.py | 2 + .../test_composite_pk_active_query.py | 2 + .../test_composite_pk_activerecord.py | 2 + .../test_composite_pk_cte_query.py | 2 + .../test_composite_pk_set_operation.py | 2 + .../feature/events/test_handlers.py | 2 + .../feature/events/test_lifecycle.py | 2 + .../feature/mixins/test_combined_articles.py | 2 + .../feature/mixins/test_optimistic_lock.py | 2 + .../feature/mixins/test_soft_delete.py | 2 + .../feature/mixins/test_timestamps.py | 2 + .../connection/test_active_query_context.py | 2 + .../connection/test_cte_query_context.py | 2 + .../connection/test_set_operation_context.py | 2 + .../query/test_active_query_aggregate.py | 2 + .../feature/query/test_active_query_basic.py | 2 + .../feature/query/test_active_query_join.py | 2 + .../feature/query/test_active_query_range.py | 2 + .../query/test_active_query_set_operation.py | 2 + .../feature/query/test_aggregate_queries.py | 2 + .../feature/query/test_basic.py | 2 + .../test_cross_database_compatibility.py | 2 + .../query/test_cte_query_active_query.py | 2 + .../query/test_cte_query_set_operation.py | 2 + .../query/test_eager_loading_caching.py | 2 + .../query/test_eager_loading_nested.py | 2 + .../test_eager_loading_scenarios_sync.py | 6 +- .../query/test_eager_loading_with_all.py | 2 + .../query/test_eager_loading_with_modifier.py | 2 + .../query/test_eager_loading_with_one.py | 2 + .../feature/query/test_error_handling.py | 2 + .../query/test_example_query_fixtures.py | 2 + .../feature/query/test_joins.py | 2 + .../query/test_logging_data_summarization.py | 2 + .../{test_sync_queries.py => test_queries.py} | 4 +- ...async_queries.py => test_queries_async.py} | 5 +- .../feature/query/test_query_optimization.py | 2 + .../feature/query/test_range_queries.py | 2 + .../query/test_range_query_detailed.py | 2 + .../query/test_relational_validation.py | 2 + .../feature/query/test_relations_basic.py | 2 + .../feature/query/test_relations_with_sync.py | 2 +- .../feature/query/test_set_operation.py | 2 + .../feature/query/test_special_queries.py | 2 + .../feature/query/test_window_functions.py | 2 + .../query/worker/test_parallel_queries.py | 2 + .../test_worker_transaction_isolation.py | 2 + .../feature/relation/test_base_async.py | 1 + .../feature/relation/test_boundary.py | 2 + .../feature/relation/test_cache_async.py | 1 + .../feature/relation/test_cache_clearing.py | 2 + .../feature/relation/test_derived_field.py | 2 + .../relation/test_derived_field_json.py | 2 + .../relation/test_descriptor_compat.py | 3 +- .../feature/relation/test_integration.py | 2 + .../feature/relation/test_interfaces_async.py | 1 + .../relation/test_modifier_behavior.py | 2 + .../feature/relation/test_modifier_warning.py | 2 + .../test_nested_relationship_access_async.py | 1 + .../feature/relation/test_path_validation.py | 2 + .../feature/relation/test_validator.py | 3 +- .../feature/relation/test_with_method.py | 2 + 81 files changed, 1048 insertions(+), 10 deletions(-) rename {tests/benchmark => src/rhosocial/activerecord/backend/impl/postgres/examples/pgq}/__init__.py (100%) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py delete mode 100644 tests/benchmark/activerecord_bulk/__init__.py delete mode 100644 tests/benchmark/backend/__init__.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py rename tests/rhosocial/activerecord_postgres_test/feature/query/{test_sync_queries.py => test_queries.py} (70%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{test_async_queries.py => test_queries_async.py} (69%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py index a3c1a4db..2ec48e6a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py @@ -262,6 +262,14 @@ 'min_version': '9', 'max_version': '18', }, + 'pgq/recursive_cte_graph_traversal.py': { + 'title': 'Recursive CTE Graph Traversal (PGQ Quantified Path Alternative)', + 'dialect_protocols': [], + 'priority': 5, + 'min_version': '9', + 'max_version': '18', + 'note': 'Uses recursive CTE + UNION ALL to simulate PGQ quantified path patterns; works on all PG versions', + }, 'transaction/basic.py': { 'title': 'Transaction Control', 'dialect_protocols': [], diff --git a/tests/benchmark/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/__init__.py similarity index 100% rename from tests/benchmark/__init__.py rename to src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/__init__.py 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 new file mode 100644 index 00000000..2c7241df --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py @@ -0,0 +1,389 @@ +""" +Recursive CTE graph traversal — PGQ quantified path alternative +=============================================================== + +PostgreSQL 19 natively supports SQL/PGQ (MATCH / GRAPH_TABLE), with limitations: + - supports_quantified_path() → False (no variable-length paths +/*/{n,m}) + - supports_comma_separated_patterns() → False (no comma-separated multi-pattern) + +This example demonstrates recursive CTE + SetOperationExpression + UNION ALL +as an equivalent alternative for graph traversal patterns. + +Common use cases: + 1. Social network N-degree friend recommendations (replaces MATCH (a)-[e]->+(b)) + 2. AML fund source tracing (replaces MATCH (s)-[t*1..5]->(d)) + +Key components: + - CTEExpression: defines a CTE (WITH ... AS ...) + - SetOperationExpression: combines base + recursive queries (UNION ALL) + - WithQueryExpression(recursive=True): generates WITH RECURSIVE ... SELECT ... +""" + +# ============================================================ +# SECTION: Setup (reference only — creates tables and data) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig + +config = PostgresConnectionConfig( + host=os.getenv('POSTGRES_HOST', 'localhost'), + port=int(os.getenv('POSTGRES_PORT', 5432)), + database=os.getenv('POSTGRES_DATABASE', 'test'), + username=os.getenv('POSTGRES_USER', 'postgres'), + password=os.getenv('POSTGRES_PASSWORD', ''), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +dialect = backend.dialect + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, + InsertExpression, + ValuesSource, + DropTableExpression, + QueryExpression, + TableExpression, + CTEExpression, + WithQueryExpression, + SetOperationExpression, + FunctionCall, + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.core import Literal, Column +from rhosocial.activerecord.backend.expression.predicates import BetweenPredicate +from rhosocial.activerecord.backend.expression.query_parts import ( + WhereClause, + OrderByClause, + JoinExpression, +) +from rhosocial.activerecord.backend.options import ExecutionOptions +from rhosocial.activerecord.backend.schema import StatementType + +dql_options = ExecutionOptions(stmt_type=StatementType.DQL) + +for t in ("transactions", "accounts", "follows", "users"): + sql, params = DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql() + backend.execute(sql, params) + +# ---- Social network tables ---- +users_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('name', 'VARCHAR(100)'), + ColumnDefinition('city', 'VARCHAR(50)'), +] +backend.execute(*CreateTableExpression(dialect, 'users', users_cols, if_not_exists=True).to_sql()) + +follows_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('follower_id', 'INTEGER'), + ColumnDefinition('followed_id', 'INTEGER'), +] +backend.execute(*CreateTableExpression(dialect, 'follows', follows_cols, if_not_exists=True).to_sql()) + +users_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 'Alice'), Literal(dialect, 'NYC')], + [Literal(dialect, 2), Literal(dialect, 'Bob'), Literal(dialect, 'NYC')], + [Literal(dialect, 3), Literal(dialect, 'Charlie'), Literal(dialect, 'LA')], + [Literal(dialect, 4), Literal(dialect, 'Diana'), Literal(dialect, 'NYC')], + [Literal(dialect, 5), Literal(dialect, 'Eve'), Literal(dialect, 'LA')], +]) +backend.execute(*InsertExpression(dialect, 'users', columns=['id', 'name', 'city'], source=users_data).to_sql()) + +follows_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], + [Literal(dialect, 2), Literal(dialect, 2), Literal(dialect, 3)], + [Literal(dialect, 3), Literal(dialect, 1), Literal(dialect, 3)], + [Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, 1)], + [Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 5)], +]) +backend.execute(*InsertExpression(dialect, 'follows', columns=['id', 'follower_id', 'followed_id'], source=follows_data).to_sql()) + +# ---- AML tables ---- +accounts_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('account_holder', 'VARCHAR(100)'), + ColumnDefinition('account_type', 'VARCHAR(20)'), +] +backend.execute(*CreateTableExpression(dialect, 'accounts', accounts_cols, if_not_exists=True).to_sql()) + +txn_cols = [ + ColumnDefinition('id', 'INTEGER', constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition('source_account_id', 'INTEGER'), + ColumnDefinition('target_account_id', 'INTEGER'), + ColumnDefinition('amount', 'NUMERIC(12,2)'), + ColumnDefinition('ts', 'VARCHAR(20)'), +] +backend.execute(*CreateTableExpression(dialect, 'transactions', txn_cols, if_not_exists=True).to_sql()) + +accounts_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 'Alice Smith'), Literal(dialect, 'checking')], + [Literal(dialect, 2), Literal(dialect, 'Bob Corp'), Literal(dialect, 'business')], + [Literal(dialect, 3), Literal(dialect, 'Charlie Ltd'), Literal(dialect, 'business')], + [Literal(dialect, 4), Literal(dialect, 'Diana Offshore'), Literal(dialect, 'offshore')], + [Literal(dialect, 5), Literal(dialect, 'Eve Holding'), Literal(dialect, 'offshore')], + [Literal(dialect, 6), Literal(dialect, 'Foreign Entity'), Literal(dialect, 'offshore')], +]) +backend.execute(*InsertExpression(dialect, 'accounts', columns=['id', 'account_holder', 'account_type'], source=accounts_data).to_sql()) + +txn_data = ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 4), Literal(dialect, 3), Literal(dialect, 500000), Literal(dialect, '2024-01-15')], + [Literal(dialect, 2), Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 300000), Literal(dialect, '2024-02-01')], + [Literal(dialect, 3), Literal(dialect, 3), Literal(dialect, 2), Literal(dialect, 600000), Literal(dialect, '2024-03-01')], + [Literal(dialect, 4), Literal(dialect, 2), Literal(dialect, 1), Literal(dialect, 450000), Literal(dialect, '2024-04-01')], + [Literal(dialect, 5), Literal(dialect, 6), Literal(dialect, 5), Literal(dialect, 1000000), Literal(dialect, '2024-05-01')], +]) +backend.execute(*InsertExpression(dialect, 'transactions', columns=['id', 'source_account_id', 'target_account_id', 'amount', 'ts'], source=txn_data).to_sql()) + +# ============================================================ +# SECTION: Scenario 1 — Social Network N-degree Recommendation +# Equivalent to PGQ: MATCH (a:person {name: 'Alice'})-[e:follows]+{1,3}->(b:person) +# ============================================================ +print("=" * 72) +print("Scenario 1: Social Network N-degree Friend Recommendations") +print("Equivalent: MATCH (a)-[e:follows]+{1,3}->(b)") +print("Technique: Recursive CTE + UNION ALL + depth filter ({1,3})") +print("=" * 72) + +# --- Base query: start vertex Alice, depth=0 --- +base_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id'), + Column(dialect, 'name'), + Literal(dialect, 0).as_('depth'), + ], + from_=TableExpression(dialect, 'users'), + where=WhereClause(dialect, condition=Column(dialect, 'name') == Literal(dialect, 'Alice')), +) + +# --- Recursive query: follow edges one hop, depth+1, bound depth<4 --- +recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, 'traversal', alias='t'), + right_table=TableExpression(dialect, 'follows', alias='f'), + join_type='INNER JOIN', + condition=Column(dialect, 'id', table='t') == Column(dialect, 'follower_id', table='f'), +).inner_join( + right_table=TableExpression(dialect, 'users', alias='u'), + condition=Column(dialect, 'followed_id', table='f') == Column(dialect, 'id', table='u'), +) + +recursive_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id', table='u'), + Column(dialect, 'name', table='u'), + FunctionCall(dialect, '+', Column(dialect, 'depth', table='t'), Literal(dialect, 1)).as_('depth'), + ], + from_=recursive_join, + where=WhereClause(dialect, condition=Column(dialect, 'depth', table='t') < Literal(dialect, 4)), +) + +# --- Combine with UNION ALL --- +union_expr = SetOperationExpression( + dialect=dialect, + left=base_query, + right=recursive_query, + operation='UNION ALL', +) + +# --- Define CTE --- +traversal_cte = CTEExpression( + dialect=dialect, + name='traversal', + query=union_expr, + columns=['id', 'name', 'depth'], +) + +# --- Main query: filter depth 1~3 (simulating quantified path {1,3}) --- +friend_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id'), + Column(dialect, 'name'), + Column(dialect, 'depth'), + ], + from_=TableExpression(dialect, 'traversal'), + where=WhereClause(dialect, condition=BetweenPredicate( + dialect, Column(dialect, 'depth'), Literal(dialect, 1), Literal(dialect, 3), + )), + order_by=OrderByClause(dialect, [Column(dialect, 'depth'), Column(dialect, 'name')]), +) + +with_query = WithQueryExpression( + dialect=dialect, + ctes=[traversal_cte], + main_query=friend_query, + recursive=True, +) + +sql, params = with_query.to_sql() +print(f"\nGenerated SQL:\n{sql}\n") +result = backend.execute(sql, params, options=dql_options) +print("Friend recommendations for Alice (depth 1~3):") +for row in result.data or []: + print(f" depth={row['depth']}: {row['name']}") +print() + +# ============================================================ +# SECTION: Scenario 2 — AML Fund Source Tracing +# Equivalent to PGQ: MATCH (s:account)-[t:transfer*1..5]->(d:account {id: 1}) +# ============================================================ +print("=" * 72) +print("Scenario 2: AML Fund Source Tracing") +print("Equivalent: MATCH (s)-[t:transfer*1..5]->(d {id: 1})") +print("Technique: Recursive CTE + UNION ALL + depth limit + amount aggregation") +print("=" * 72) + +# --- Base query: direct sources sending to target account ACC-001 --- +aml_base_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, 'transactions', alias='tx'), + right_table=TableExpression(dialect, 'accounts', alias='a'), + join_type='INNER JOIN', + condition=Column(dialect, 'source_account_id', table='tx') == Column(dialect, 'id', table='a'), +) + +aml_base = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id', table='a'), + Column(dialect, 'account_holder', table='a'), + Column(dialect, 'account_type', table='a'), + Column(dialect, 'amount', table='tx'), + Literal(dialect, 1).as_('depth'), + ], + from_=aml_base_join, + where=WhereClause(dialect, condition=Column(dialect, 'target_account_id', table='tx') == Literal(dialect, 1)), +) + +# --- Recursive query: trace upstream along the transaction chain --- +aml_recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, 'fund_trace', alias='tr'), + right_table=TableExpression(dialect, 'transactions', alias='tx'), + join_type='INNER JOIN', + condition=Column(dialect, 'target_account_id', table='tx') == Column(dialect, 'id', table='tr'), +).inner_join( + right_table=TableExpression(dialect, 'accounts', alias='a'), + condition=Column(dialect, 'source_account_id', table='tx') == Column(dialect, 'id', table='a'), +) + +aml_recursive = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id', table='a'), + Column(dialect, 'account_holder', table='a'), + Column(dialect, 'account_type', table='a'), + Column(dialect, 'amount', table='tx'), + FunctionCall(dialect, '+', Column(dialect, 'depth', table='tr'), Literal(dialect, 1)).as_('depth'), + ], + from_=aml_recursive_join, + where=WhereClause(dialect, condition=Column(dialect, 'depth', table='tr') < Literal(dialect, 5)), +) + +# --- Combine with UNION ALL --- +aml_union = SetOperationExpression( + dialect=dialect, + left=aml_base, + right=aml_recursive, + operation='UNION ALL', +) + +# --- Define CTE --- +fund_trace_cte = CTEExpression( + dialect=dialect, + name='fund_trace', + query=aml_union, + columns=['id', 'account_holder', 'account_type', 'amount', 'depth'], +) + +# --- Main query: list the full fund chain --- +aml_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'id'), + Column(dialect, 'account_holder'), + Column(dialect, 'account_type'), + Column(dialect, 'amount'), + Column(dialect, 'depth'), + ], + from_=TableExpression(dialect, 'fund_trace'), + where=WhereClause(dialect, condition=Column(dialect, 'depth') >= Literal(dialect, 1)), + order_by=OrderByClause(dialect, [Column(dialect, 'depth'), Column(dialect, 'id')]), +) + +aml_with = WithQueryExpression( + dialect=dialect, + ctes=[fund_trace_cte], + main_query=aml_query, + recursive=True, +) + +sql, params = aml_with.to_sql() +print(f"\nGenerated SQL:\n{sql}\n") +result = backend.execute(sql, params, options=dql_options) +print("Fund source tracing (upstream chain for Alice Smith / ACC-001):") +print(f"{'depth':<6} {'Holder':<20} {'Type':<12} {'Amount':>12}") +print("-" * 52) +for row in result.data or []: + print(f" {row['depth']:<4} {row['account_holder']:<20} {row['account_type']:<12} {float(row['amount']):>10,.0f}") + +# --- Aggregation query: total suspicious amount per depth level --- +agg_query = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'depth'), + FunctionCall(dialect, 'COUNT', Column(dialect, 'id'), alias='account_count'), + FunctionCall(dialect, 'SUM', Column(dialect, 'amount'), alias='total_amount'), + ], + from_=TableExpression(dialect, 'fund_trace'), + order_by=OrderByClause(dialect, [Column(dialect, 'depth')]), +) + +agg_with = WithQueryExpression( + dialect=dialect, + ctes=[fund_trace_cte], + main_query=agg_query, + recursive=True, +) + +sql, params = agg_with.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("\nAggregation: total suspicious amount per depth level") +print(f"{'depth':<8} {'Accounts':<10} {'Total Amount':>14}") +print("-" * 34) +for row in result.data or []: + print(f" {row['depth']:<6} {row['account_count']:<10} {float(row['total_amount']):>10,.0f}") + +# ============================================================ +# SECTION: Summary (key technical points) +# ============================================================ +print() +print("=" * 72) +print("Summary / Key Technical Points") +print("=" * 72) +print(""" +1. SetOperationExpression(operation='UNION ALL') combines base + recursive queries +2. CTEExpression(name=..., query=..., columns=...) defines the CTE column signature +3. WithQueryExpression(recursive=True) generates WITH RECURSIVE +4. Recursive query joins the CTE reference with physical tables via chained JoinExpression +5. depth field simulates the hop count of quantified path patterns +6. WHERE depth BETWEEN x AND y simulates {x,y} range constraint +7. Use cases: social recommendations, fund tracing, knowledge graph traversal +""") + +# ============================================================ +# SECTION: Teardown (reference only — cleanup resources) +# ============================================================ +for t in ("transactions", "accounts", "follows", "users"): + sql, params = DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql() + backend.execute(sql, params) +backend.disconnect() diff --git a/tests/benchmark/activerecord_bulk/__init__.py b/tests/benchmark/activerecord_bulk/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/benchmark/activerecord_bulk/test_bulk_create.py b/tests/benchmark/activerecord_bulk/test_bulk_create.py index 6713f01e..8abfa754 100644 --- a/tests/benchmark/activerecord_bulk/test_bulk_create.py +++ b/tests/benchmark/activerecord_bulk/test_bulk_create.py @@ -1,3 +1,5 @@ # Bridge file for bulk_create benchmark tests from the testsuite. -from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create_sync import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create_async import * # noqa: F403 from rhosocial.activerecord.testsuite.benchmark.crud.test_bulk_create_async import * # noqa: F401, F403 + diff --git a/tests/benchmark/backend/__init__.py b/tests/benchmark/backend/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py new file mode 100644 index 00000000..fa0bf12c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -0,0 +1,502 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +""" +Recursive CTE graph traversal tests — PGQ quantified path alternative. + +These tests verify that recursive CTE + SetOperationExpression + UNION ALL +correctly simulates PGQ quantified path patterns (not natively supported +in PostgreSQL 19). + +Scenarios: +1. Social Network: N-degree friend recommendations +2. Anti-Money Laundering: Fund source tracing +""" +import pytest +import pytest_asyncio + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, + InsertExpression, + ValuesSource, + DropTableExpression, + QueryExpression, + TableExpression, + CTEExpression, + WithQueryExpression, + SetOperationExpression, + FunctionCall, + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.core import Literal, Column +from rhosocial.activerecord.backend.expression.predicates import BetweenPredicate +from rhosocial.activerecord.backend.expression.query_parts import ( + WhereClause, + OrderByClause, + JoinExpression, +) + + +@pytest.fixture +def social_network_data(postgres_backend): + """Fixture: create users + follows tables with test data.""" + backend = postgres_backend + dialect = backend.dialect + + for t in ("follows", "users"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "users", [ + ColumnDefinition("id", "INTEGER", constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", "VARCHAR(100)"), + ]).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "follows", [ + ColumnDefinition("id", "INTEGER", constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("follower_id", "INTEGER"), + ColumnDefinition("followed_id", "INTEGER"), + ]).to_sql()) + + backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, "Alice")], + [Literal(dialect, 2), Literal(dialect, "Bob")], + [Literal(dialect, 3), Literal(dialect, "Charlie")], + [Literal(dialect, 4), Literal(dialect, "Diana")], + [Literal(dialect, 5), Literal(dialect, "Eve")], + ])).to_sql()) + + backend.execute(*InsertExpression(dialect, "follows", columns=["id", "follower_id", "followed_id"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], + [Literal(dialect, 2), Literal(dialect, 2), Literal(dialect, 3)], + [Literal(dialect, 3), Literal(dialect, 1), Literal(dialect, 3)], + [Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, 1)], + ])).to_sql()) + + yield + + for t in ("follows", "users"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + +@pytest.fixture +def aml_data(postgres_backend): + """Fixture: create accounts + transactions tables with test data.""" + backend = postgres_backend + dialect = backend.dialect + + for t in ("transactions", "accounts"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "accounts", [ + ColumnDefinition("id", "INTEGER", constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("account_holder", "VARCHAR(100)"), + ColumnDefinition("account_type", "VARCHAR(20)"), + ]).to_sql()) + + backend.execute(*CreateTableExpression(dialect, "transactions", [ + ColumnDefinition("id", "INTEGER", constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("source_account_id", "INTEGER"), + ColumnDefinition("target_account_id", "INTEGER"), + ColumnDefinition("amount", "NUMERIC(12,2)"), + ]).to_sql()) + + backend.execute(*InsertExpression(dialect, "accounts", columns=["id", "account_holder", "account_type"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, "Alice Smith"), Literal(dialect, "checking")], + [Literal(dialect, 2), Literal(dialect, "Bob Corp"), Literal(dialect, "business")], + [Literal(dialect, 3), Literal(dialect, "Charlie Ltd"), Literal(dialect, "business")], + [Literal(dialect, 4), Literal(dialect, "Diana Offshore"), Literal(dialect, "offshore")], + [Literal(dialect, 5), Literal(dialect, "Eve Holding"), Literal(dialect, "offshore")], + ])).to_sql()) + + backend.execute(*InsertExpression(dialect, "transactions", columns=[ + "id", "source_account_id", "target_account_id", "amount"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 4), Literal(dialect, 3), Literal(dialect, 500000)], + [Literal(dialect, 2), Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 300000)], + [Literal(dialect, 3), Literal(dialect, 3), Literal(dialect, 2), Literal(dialect, 600000)], + [Literal(dialect, 4), Literal(dialect, 2), Literal(dialect, 1), Literal(dialect, 450000)], + ])).to_sql()) + + yield + + for t in ("transactions", "accounts"): + backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + +class TestSocialNetworkTraversal: + """Recursive CTE for N-degree friend recommendations.""" + + def _build_traversal_cte(self, dialect): + """Build recursive CTE expression for social network traversal.""" + base = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Literal(dialect, 0).as_("depth"), + ], + from_=TableExpression(dialect, "users"), + where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), + ) + + recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, "traversal", alias="t"), + right_table=TableExpression(dialect, "follows", alias="f"), + join_type="INNER JOIN", + condition=Column(dialect, "id", table="t") == Column(dialect, "follower_id", table="f"), + ).inner_join( + right_table=TableExpression(dialect, "users", alias="u"), + condition=Column(dialect, "followed_id", table="f") == Column(dialect, "id", table="u"), + ) + + recursive = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id", table="u"), + Column(dialect, "name", table="u"), + FunctionCall(dialect, "+", Column(dialect, "depth", table="t"), Literal(dialect, 1)).as_("depth"), + ], + from_=recursive_join, + where=WhereClause(dialect, condition=Column(dialect, "depth", table="t") < Literal(dialect, 4)), + ) + + union = SetOperationExpression( + dialect=dialect, + left=base, + right=recursive, + operation="UNION ALL", + ) + + return CTEExpression( + dialect=dialect, + name="traversal", + query=union, + columns=["id", "name", "depth"], + ) + + def test_friend_recommendation_depth_1_to_3(self, postgres_backend, social_network_data): + """Q1: Friend recommendations at depths 1-3 (simulate {1,3}).""" + dialect = postgres_backend.dialect + cte = self._build_traversal_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "traversal"), + where=WhereClause(dialect, condition=BetweenPredicate( + dialect, Column(dialect, "depth"), Literal(dialect, 1), Literal(dialect, 3), + )), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "name")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + names_at_depth = {r["name"]: r["depth"] for r in rows} + + # Alice follows Bob (depth 1) and Charlie (depth 1) + assert names_at_depth.get("Bob") == 1 + assert names_at_depth.get("Charlie") == 1 + # Bob follows Diana (depth 2) + assert names_at_depth.get("Diana") == 2 + + def test_friend_recommendation_depth_2(self, postgres_backend, social_network_data): + """Q2: Only depth=2 (simulate {2,2}).""" + dialect = postgres_backend.dialect + cte = self._build_traversal_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "traversal"), + where=WhereClause(dialect, condition=Column(dialect, "depth") == Literal(dialect, 2)), + order_by=OrderByClause(dialect, [Column(dialect, "name")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + names = [r["name"] for r in rows] + assert "Diana" in names + assert len(names) == 1 + + def test_exclude_start_node(self, postgres_backend, social_network_data): + """Q3: Exclude Alice herself by filtering depth >= 1.""" + dialect = postgres_backend.dialect + cte = self._build_traversal_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "name"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "traversal"), + where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 1)), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "name")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + names = [r["name"] for r in rows] + assert "Alice" not in names + assert len(names) == 3 # Bob, Charlie, Diana + + +class TestAMLFundTracing: + """Recursive CTE for fund source tracing (AML).""" + + def _build_aml_cte(self, dialect): + """Build recursive CTE expression for fund tracing.""" + base_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, "transactions", alias="tx"), + right_table=TableExpression(dialect, "accounts", alias="a"), + join_type="INNER JOIN", + condition=Column(dialect, "source_account_id", table="tx") == Column(dialect, "id", table="a"), + ) + + base = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id", table="a"), + Column(dialect, "account_holder", table="a"), + Column(dialect, "account_type", table="a"), + Column(dialect, "amount", table="tx"), + Literal(dialect, 1).as_("depth"), + ], + from_=base_join, + where=WhereClause( + dialect, + condition=Column(dialect, "target_account_id", table="tx") + == Literal(dialect, 1), + ), + ) + + recursive_join = JoinExpression( + dialect=dialect, + left_table=TableExpression(dialect, "fund_trace", alias="tr"), + right_table=TableExpression(dialect, "transactions", alias="tx"), + join_type="INNER JOIN", + condition=Column(dialect, "target_account_id", table="tx") == Column(dialect, "id", table="tr"), + ).inner_join( + right_table=TableExpression(dialect, "accounts", alias="a"), + condition=Column(dialect, "source_account_id", table="tx") == Column(dialect, "id", table="a"), + ) + + recursive = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id", table="a"), + Column(dialect, "account_holder", table="a"), + Column(dialect, "account_type", table="a"), + Column(dialect, "amount", table="tx"), + FunctionCall(dialect, "+", Column(dialect, "depth", table="tr"), Literal(dialect, 1)).as_("depth"), + ], + from_=recursive_join, + where=WhereClause(dialect, condition=Column(dialect, "depth", table="tr") < Literal(dialect, 5)), + ) + + union = SetOperationExpression( + dialect=dialect, + left=base, + right=recursive, + operation="UNION ALL", + ) + + return CTEExpression( + dialect=dialect, + name="fund_trace", + query=union, + columns=["id", "account_holder", "account_type", "amount", "depth"], + ) + + def test_fund_trace_chain(self, postgres_backend, aml_data): + """Trace fund sources for account ACC-001.""" + dialect = postgres_backend.dialect + cte = self._build_aml_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "account_holder"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "fund_trace"), + where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 1)), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "id")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + account_names = [r["account_holder"] for r in rows] + # Direct source: Bob Corp (depth 1) + assert "Bob Corp" in account_names + # Indirect: Charlie Ltd (depth 2) -> Bob Corp + assert "Charlie Ltd" in account_names + # Indirect: Diana Offshore (depth 3) -> Charlie Ltd + assert "Diana Offshore" in account_names + + def test_fund_trace_depth_filter(self, postgres_backend, aml_data): + """Filter fund sources by minimum depth.""" + dialect = postgres_backend.dialect + cte = self._build_aml_cte(dialect) + + main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "id"), + Column(dialect, "account_holder"), + Column(dialect, "depth"), + ], + from_=TableExpression(dialect, "fund_trace"), + where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 2)), + order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "id")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + depths = [r["depth"] for r in rows] + assert all(d >= 2 for d in depths) + # Depth 2: Charlie Ltd (via Bob Corp) + assert any(r["account_holder"] == "Charlie Ltd" for r in rows) + + def test_fund_trace_aggregation(self, postgres_backend, aml_data): + """Aggregate total amount by depth level.""" + dialect = postgres_backend.dialect + cte = self._build_aml_cte(dialect) + + agg_main = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, "depth"), + FunctionCall(dialect, "SUM", Column(dialect, "amount"), alias="total"), + ], + from_=TableExpression(dialect, "fund_trace"), + order_by=OrderByClause(dialect, [Column(dialect, "depth")]), + ) + + with_query = WithQueryExpression( + dialect=dialect, + ctes=[cte], + main_query=agg_main, + recursive=True, + ) + + sql, params = with_query.to_sql() + rows = postgres_backend.fetch_all(sql, params) + totals = {r["depth"]: float(r["total"]) for r in rows} + # Depth 1: Bob Corp sent 450000 to Alice Smith + assert totals.get(1) == 450000 + # Depth 2: Charlie Ltd sent 600000 to Bob Corp + assert totals.get(2) == 600000 + # Depth 3: Diana Offshore sent 500000 to Charlie Ltd + assert totals.get(3) == 500000 + + +class TestAsyncRecursiveCTEGraph: + """Async versions of recursive CTE graph tests.""" + + @pytest_asyncio.fixture + async def async_social_network_data(self, async_postgres_backend): + backend = async_postgres_backend + dialect = backend.dialect + + for t in ("follows", "users"): + await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + await backend.execute(*CreateTableExpression(dialect, "users", [ + ColumnDefinition("id", "INTEGER", constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", "VARCHAR(100)"), + ]).to_sql()) + + await backend.execute(*CreateTableExpression(dialect, "follows", [ + ColumnDefinition("id", "INTEGER", constraints=[ + ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("follower_id", "INTEGER"), + ColumnDefinition("followed_id", "INTEGER"), + ]).to_sql()) + + await backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, "Alice")], + [Literal(dialect, 2), Literal(dialect, "Bob")], + ])).to_sql()) + + await backend.execute(*InsertExpression(dialect, "follows", columns=["id", "follower_id", "followed_id"], + source=ValuesSource(dialect, [ + [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], + ])).to_sql()) + + yield + + for t in ("follows", "users"): + await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + + @pytest.mark.asyncio + async def test_async_single_hop(self, async_postgres_backend, async_social_network_data): + """Async: Alice's 1-hop friends.""" + dialect = async_postgres_backend.dialect + base = QueryExpression( + dialect=dialect, + select=[Column(dialect, "id"), Column(dialect, "name"), Literal(dialect, 0).as_("depth")], + from_=TableExpression(dialect, "users"), + where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), + ) + result = base.to_sql() + sql, params = result + rows = await async_postgres_backend.fetch_all(sql, params) + assert len(rows) == 1 + assert rows[0]["name"] == "Alice" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py index 8f7e8cf3..b5ecbf1d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py index eb4e162b..c9488510 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_crud_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py index 9c3e81ba..4cd150ba 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py @@ -16,3 +16,5 @@ # The `TestBasicMappedModels` class contains the actual test cases. from rhosocial.activerecord.testsuite.feature.basic.test_example_basic_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_example_basic_fixtures_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py index e0936eda..bdacfa62 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py @@ -9,3 +9,5 @@ ) from rhosocial.activerecord.testsuite.feature.basic.test_bulk_operations import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_bulk_operations_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py index c434da79..474f2c71 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py @@ -28,3 +28,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.test_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_crud_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py index e5503f88..7940c241 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py @@ -21,3 +21,5 @@ # The test classes contain the actual test cases. from rhosocial.activerecord.testsuite.feature.basic.test_field_column_mapping import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_field_column_mapping_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py index ed3bdd00..a409bbbf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py @@ -21,3 +21,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.test_fields import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_fields_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py index 752b0a1b..1a0848e5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py @@ -14,3 +14,5 @@ ) from rhosocial.activerecord.testsuite.feature.basic.test_pydantic_native_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_pydantic_native_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py index f9841654..9e30dff5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py @@ -14,3 +14,5 @@ # Import all tests from the generic testsuite file. from rhosocial.activerecord.testsuite.feature.basic.test_type_adapter import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_type_adapter_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py index b522ba20..54666c6b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py @@ -21,3 +21,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.basic.test_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.test_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py index 4486e093..a5150e06 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management.py @@ -8,3 +8,5 @@ user_class_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.basic.worker.test_connection_management import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.worker.test_connection_management_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py index c75a4e8c..287d0828 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud.py @@ -9,3 +9,5 @@ async_user_class_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.basic.worker.test_parallel_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.worker.test_parallel_crud_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py index 53d0a0d0..7df4d9df 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle.py @@ -8,3 +8,5 @@ user_class_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.basic.worker.test_worker_pool_lifecycle import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.worker.test_worker_pool_lifecycle_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py index 66e9faae..6ae36196 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py @@ -1,2 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py from rhosocial.activerecord.testsuite.feature.composite_pk.test_active_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.composite_pk.test_active_query_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py index a4b222c9..08112386 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py @@ -1,2 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py from rhosocial.activerecord.testsuite.feature.composite_pk.test_activerecord import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.composite_pk.test_activerecord_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py index 6790f16b..57d58ab8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py @@ -1,2 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py from rhosocial.activerecord.testsuite.feature.composite_pk.test_cte_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.composite_pk.test_cte_query_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py index 148c1b97..b485fde0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py @@ -1,2 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py from rhosocial.activerecord.testsuite.feature.composite_pk.test_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.composite_pk.test_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py index 02d222d2..5964c8ba 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers.py @@ -21,3 +21,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.events.test_handlers import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.events.test_handlers_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py index 00b29520..43bb6d83 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle.py @@ -21,3 +21,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.events.test_lifecycle import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.events.test_lifecycle_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py index a63c2d24..c0c81379 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_combined_articles import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_combined_articles_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py index 9fd9601a..be5611e5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_optimistic_lock import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_optimistic_lock_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py index e440e286..40a34336 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_soft_delete import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_soft_delete_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py index d776bdbf..64344372 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps.py @@ -7,3 +7,5 @@ """ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.mixins.test_timestamps import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.mixins.test_timestamps_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py index 403c1a9e..9fb3d2f7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.query.connection.test_active_query_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.connection.test_active_query_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py index 1483e319..0b1e6153 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.query.connection.test_cte_query_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.connection.test_cte_query_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py index f6acecbf..ef0d192a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context.py @@ -12,3 +12,5 @@ # Import shared tests from testsuite package from rhosocial.activerecord.testsuite.feature.query.connection.test_set_operation_context import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.connection.test_set_operation_context_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py index 75f400d0..4490f75c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_active_query_aggregate import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_active_query_aggregate_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py index c8e67b7d..b48c30f4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_active_query_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_active_query_basic_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py index 4f750e5b..ea6a7e26 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_active_query_join import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_active_query_join_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py index af663f25..14fe4369 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_active_query_range import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_active_query_range_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py index 04171825..5df3b6c5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_active_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_active_query_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py index 12c99783..7f0d8321 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_aggregate_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_aggregate_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py index f56b9765..809e6cdf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_basic_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py index ce5c074a..8d5fb7dd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_cross_database_compatibility import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_cross_database_compatibility_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py index 1ce77f68..401c9b16 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_cte_query_active_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_cte_query_active_query_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py index 2610511f..2acfc13c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_cte_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_cte_query_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py index 2c23a774..a4e230f6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py @@ -3,3 +3,5 @@ Bridge file for eager loading caching tests. """ from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_caching import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_caching_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py index c90a39ef..e6430701 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py @@ -3,3 +3,5 @@ Bridge file for eager loading nested tests. """ from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_nested import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_nested_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py index ff5df366..414db2d8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py @@ -1,5 +1,7 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py +# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios.py """ Bridge file for sync eager loading scenario tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios_sync import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py index c298cef3..963fbdf0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py @@ -3,3 +3,5 @@ Bridge file for eager loading with_all tests. """ from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_all import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_all_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py index 433a7051..1df4dacb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py @@ -3,3 +3,5 @@ Bridge file for eager loading with_modifier tests. """ from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_modifier import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_modifier_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py index df162324..96fe7a48 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py @@ -3,3 +3,5 @@ Bridge file for eager loading with_one tests. """ from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_one import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_one_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py index d30ddc60..b92024f3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_error_handling import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_error_handling_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py index 79aa6592..15af9f04 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_example_query_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_example_query_fixtures_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py index 56a0a911..ad9af333 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_joins import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_joins_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py index fd09e85d..5a630984 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_logging_data_summarization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_logging_data_summarization_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py similarity index 70% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py index 75205a5d..b641f1f3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py @@ -1,8 +1,8 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_sync_queries.py +# tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py """ Bridge file for synchronous query tests from the testsuite. This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_sync_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_queries import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py similarity index 69% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py index ef4556d1..bd9acb01 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py @@ -1,8 +1,9 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_async_queries.py +# tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py """ Bridge file for async queries tests from the testsuite. This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_async_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py index ad5b3b08..2c250fd1 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_query_optimization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_query_optimization_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py index f6027323..bc33ff4c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_range_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_range_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py index 62bd6a32..28c994f8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_range_query_detailed import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_range_query_detailed_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py index def8f01d..f4766f1f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_relational_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_relational_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py index fbf463d4..272293f6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_relations_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_relations_basic_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py index 9d4a3e0e..5ff8e35c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py @@ -2,4 +2,4 @@ """ Bridge file for sync relation 'with' tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_with_sync import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.test_relations_with import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py index fc7158fb..ea4cfc60 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_set_operation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py index b9eb5614..774bf73c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_special_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_special_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py index aa5d6cc7..a665981a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py @@ -6,3 +6,5 @@ discoverable by pytest in this project's test run. """ from rhosocial.activerecord.testsuite.feature.query.test_window_functions import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.test_window_functions_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py index 7bfe5ecb..f7927c07 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries.py @@ -9,3 +9,5 @@ async_order_fixtures_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.query.worker.test_parallel_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.worker.test_parallel_queries_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py index c7590e84..9913196c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py @@ -10,3 +10,5 @@ async_order_fixtures_for_worker, # noqa: F401 ) from rhosocial.activerecord.testsuite.feature.query.worker.test_transaction_isolation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.worker.test_transaction_isolation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py new file mode 100644 index 00000000..d27862a6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.test_base_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py index 7e44d5a1..4c51f85c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_boundary import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_boundary_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py new file mode 100644 index 00000000..d0fb6c14 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.test_cache_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py index 4a4b246f..5ec530a6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_cache_clearing import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_cache_clearing_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py index 21564139..48265b0d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_derived_field import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py index 67251dc4..8c84a87f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py index 47c04e0f..203c1732 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py @@ -1,3 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py -from rhosocial.activerecord.testsuite.feature.relation.test_descriptor_compat import * # noqa: F401,F403 \ No newline at end of file +from rhosocial.activerecord.testsuite.feature.relation.test_descriptor_compat import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_descriptor_compat_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py index ebd76ceb..de72e480 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_integration import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_integration_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py new file mode 100644 index 00000000..2ae8e7db --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.test_interfaces_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py index bbd9a806..196d1585 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py index eb29123d..1bdf5e5d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py new file mode 100644 index 00000000..272901e9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py @@ -0,0 +1 @@ +from rhosocial.activerecord.testsuite.feature.relation.test_nested_relationship_access_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py index 37e7e8e9..5e35543f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_path_validation import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_path_validation_async import * # noqa: F403 + diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py index 0cebf814..c5ef8611 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py @@ -1,3 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py -from rhosocial.activerecord.testsuite.feature.relation.test_validator import * # noqa: F401,F403 \ No newline at end of file +from rhosocial.activerecord.testsuite.feature.relation.test_validator import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_validator_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py index fdb8bca6..0b998f59 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py @@ -1 +1,3 @@ from rhosocial.activerecord.testsuite.feature.relation.test_with_method import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.test_with_method_async import * # noqa: F403 + From 3d69c84457b298808867e6131779325b62fcbcab Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 9 Jul 2026 21:45:30 +0800 Subject: [PATCH 12/39] feat: add bridge files for derived_field, interface feature tests and relation benchmark - derived_field/conftest.py + test_derived_field_testsuite.py - interface/test_threadsafe_dict.py (sync + async combined) - benchmark/relation/conftest.py + test_cache_comparison.py --- tests/benchmark/relation/conftest.py | 158 ++++++++++++++++++ .../relation/test_cache_comparison.py | 75 +++++++++ .../feature/derived_field/conftest.py | 9 + .../test_derived_field_testsuite.py | 16 ++ .../feature/interface/test_threadsafe_dict.py | 7 + 5 files changed, 265 insertions(+) create mode 100644 tests/benchmark/relation/conftest.py create mode 100644 tests/benchmark/relation/test_cache_comparison.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py diff --git a/tests/benchmark/relation/conftest.py b/tests/benchmark/relation/conftest.py new file mode 100644 index 00000000..cce2ecbd --- /dev/null +++ b/tests/benchmark/relation/conftest.py @@ -0,0 +1,158 @@ +"""Benchmark fixtures for relation cache comparison. + +Bridges ``user_post_comment_classes`` from the testsuite's relation conftest +so that benchmarks can reuse the same model definitions and backend setup. + +Redis benchmark support is not introduced in this release. The related +source snippets are kept as comments for follow-up external cache design. +""" + +from rhosocial.activerecord.testsuite.feature.relation.conftest import ( # noqa: F401 + user_post_comment_classes, +) + +from dataclasses import dataclass +from typing import Any + +import pytest + +from rhosocial.activerecord.relation.cache import CacheConfig, InstanceCache +from rhosocial.activerecord.relation.cache_backends import InMemoryCache + +# Not introduced in this release; keep snippets for follow-up external cache design. +# from rhosocial.activerecord.relation.cache_backends import CacheSerializer, RedisCache + +pytestmark = [ + pytest.mark.benchmark, + pytest.mark.benchmark_relation, +] + + +def pytest_configure(config): + config.addinivalue_line("markers", "benchmark_relation: relation cache benchmark tests") + + +@dataclass +class RelationBenchmarkContext: + """Context for a single benchmark run.""" + + user: Any + post_class: Any + scenario: str + + +# --------------------------------------------------------------------------- +# Redis benchmark is not introduced in this release. Keep the fixture shape as +# comments for follow-up external cache design instead of deleting the source. +# --------------------------------------------------------------------------- +# @pytest.fixture(scope="session") +# def _redis_client(): +# redis = pytest.importorskip("redis") +# try: +# from providers.redis_scenarios import get_redis_scenario +# +# config = get_redis_scenario("benchmark") +# except (FileNotFoundError, KeyError): +# from rhosocial.activerecord.relation.cache_backends import RedisConfig +# +# config = RedisConfig.from_env() +# try: +# client = redis.Redis( +# host=config.host, +# port=config.port, +# password=config.password or None, +# db=config.db, +# socket_connect_timeout=config.socket_connect_timeout, +# ) +# client.ping() +# yield client +# client.flushdb() +# client.close() +# except Exception: +# pytest.skip("Redis not available for benchmark") +# yield None + + +# --------------------------------------------------------------------------- +# Shared model + data setup (function-scoped so each test gets fresh data) +# --------------------------------------------------------------------------- +@pytest.fixture(scope="function") +def _raw_models(user_post_comment_classes): # noqa: F811 + """Create a user + 100 posts + 200 comments under the same user.""" + user_cls, post_cls, comment_cls = user_post_comment_classes + user = user_cls(name="Bench User", email="bench@example.com") + user.save() + + for i in range(100): + p = post_cls(title=f"Post {i}", body=f"Body {i}", user_id=user.id) + p.save() + + yield user, post_cls, comment_cls + + for p in reversed(post_cls.find_all()): + p.delete() + user.delete() + + +# --------------------------------------------------------------------------- +# Cache scenario parametrization +# --------------------------------------------------------------------------- +CACHE_SCENARIOS = [ + pytest.param("no_cache", id="no_cache"), + pytest.param("in_memory", id="in_memory"), + # Not introduced in this release; keep for follow-up external cache design. + # pytest.param("redis", id="redis"), +] + + +@pytest.fixture(params=CACHE_SCENARIOS) +def cache_scenario(request): + """Parametrized cache scenario name.""" + return request.param + + +@pytest.fixture(scope="function") +def relation_benchmark_env(cache_scenario, _raw_models): + """Configure the InstanceCache backend and return a benchmark context. + + Each test function receives a ``RelationBenchmarkContext`` with: + - user — the seeded user instance + - post_class — for queries + - scenario — current scenario name + """ + user, post_cls, comment_cls = _raw_models + original_backend = InstanceCache._backend + + try: + if cache_scenario == "no_cache": + InstanceCache.set_backend(InMemoryCache()) + config = CacheConfig(enabled=False) + elif cache_scenario == "in_memory": + InstanceCache.set_backend(InMemoryCache()) + config = CacheConfig(enabled=True, ttl=None) + # Not introduced in this release; keep for follow-up external cache design. + # elif cache_scenario == "redis": + # InstanceCache.set_backend( + # RedisCache( + # client=_redis_client, + # prefix="bench:cache:", + # serializer=CacheSerializer(format="pickle"), + # ) + # ) + # config = CacheConfig(enabled=True, ttl=None) + else: + raise ValueError(f"Unknown scenario: {cache_scenario}") + + # Apply the cache config to all relation descriptors + for rel_name in ["posts", "comments"]: + rel = user.get_relation(rel_name) + if rel is not None: + rel._cache_config = config + + yield RelationBenchmarkContext( + user=user, + post_class=post_cls, + scenario=cache_scenario, + ) + finally: + InstanceCache.set_backend(original_backend) diff --git a/tests/benchmark/relation/test_cache_comparison.py b/tests/benchmark/relation/test_cache_comparison.py new file mode 100644 index 00000000..364b48af --- /dev/null +++ b/tests/benchmark/relation/test_cache_comparison.py @@ -0,0 +1,75 @@ +"""Benchmark comparing relation cache strategies. + +Measures HasMany relation access time under current-release scenarios: +- ``no_cache`` — CacheConfig(enabled=False), always queries DB +- ``in_memory`` — InMemoryCache, caches after first access + +Redis benchmark support is not introduced in this release. +""" + +import pytest + +pytestmark = [ + pytest.mark.benchmark, + pytest.mark.benchmark_relation, +] + + +class TestRelationFirstAccess: + """First access — always queries DB regardless of cache strategy.""" + + def test_posts_first_access(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + result = benchmark(ctx.user.posts) + assert len(result) == 100 + + +class TestRelationCachedAccess: + """Second+ access — cache hit (except no_cache which hits DB each time).""" + + def test_posts_second_access(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + ctx.user.posts() + result = benchmark(ctx.user.posts) + assert len(result) == 100 + + +class TestRelationRepeatedAccess: + """100 repeated accesses — amplifies cache vs no-cache gap.""" + + def test_posts_repeated_access(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + + def repeated(): + for _ in range(100): + ctx.user.posts() + + benchmark(repeated) + + +class TestRelationWithNewData: + """Access with new data — validates behaviour after mutations.""" + + def test_posts_after_new_record(self, benchmark, relation_benchmark_env): + """After a new record is added, cached backends return stale data. + + This is expected — cache invalidation is handled by model save/delete + events, not by the descriptor layer itself. + """ + ctx = relation_benchmark_env + ctx.user.posts() + ctx.post_class(title="New", body="New", user_id=ctx.user.id).save() + result = benchmark(ctx.user.posts) + if ctx.scenario == "no_cache": + assert len(result) == 101 + else: + assert len(result) == 100 # stale cache + + def test_posts_after_delete(self, benchmark, relation_benchmark_env): + ctx = relation_benchmark_env + ctx.user.posts() + p = ctx.post_class(title="Temp", body="Temp", user_id=ctx.user.id) + p.save() + p.delete() + result = benchmark(ctx.user.posts) + assert len(result) == 100 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py new file mode 100644 index 00000000..8921aa03 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py @@ -0,0 +1,9 @@ +# tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py +""" +Pytest configuration for derived field feature tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" + +from rhosocial.activerecord.testsuite.feature.derived_field.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py b/tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py new file mode 100644 index 00000000..19981432 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py @@ -0,0 +1,16 @@ +# tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py +""" +Bridge file for derived field feature tests from the testsuite. +""" + +from rhosocial.activerecord.testsuite.feature.derived_field.conftest import ( # noqa: F401 + product_class, + product_form_a_class, + product_with_proxy_class, + product_with_column_and_adapter_class, + async_product_class, + async_product_with_proxy_class, + async_product_with_column_and_adapter_class, +) +from rhosocial.activerecord.testsuite.feature.derived_field.test_derived_field import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.derived_field.test_derived_field_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py new file mode 100644 index 00000000..48cf1235 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict.py +""" +Bridge file for ThreadSafeDict interface tests from the testsuite. +""" + +from rhosocial.activerecord.testsuite.feature.interface.test_threadsafe_dict import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.interface.test_threadsafe_dict_async import * # noqa: F403 From d5936ebc5e75097e8cdb87e35aab5090f84e5d47 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 12 Jul 2026 21:46:31 +0800 Subject: [PATCH 13/39] perf(backend): remove redundant fetchall() in get_server_version() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execute() is atomic per DB-API 2.0 — the extra fetchall() after fetchone() is unnecessary and adds a round-trip for no benefit. Follow-up to commit ab3be40 which introduced these defensive calls. --- .../activerecord/backend/impl/postgres/backend/async_backend.py | 2 -- .../activerecord/backend/impl/postgres/backend/sync.py | 2 -- 2 files changed, 4 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 19aeea55..7aba7eb1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -346,8 +346,6 @@ async def get_server_version(self) -> Optional[Tuple[int, int, int]]: cursor = await self._get_cursor() await cursor.execute("SELECT version()") version_row = await cursor.fetchone() - # Defensively consume remaining results to prevent cursor.description pollution - remaining = await cursor.fetchall() if not version_row or not version_row[0]: self.log(logging.WARNING, "PostgreSQL version query returned no result") return None diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index dce98ddb..b23d26fa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -347,8 +347,6 @@ def get_server_version(self) -> Optional[Tuple[int, int, int]]: cursor = self._get_cursor() cursor.execute("SELECT version()") result = cursor.fetchone() - # Defensively consume remaining results to prevent cursor.description pollution - cursor.fetchall() if not result or not result[0]: self.log(logging.WARNING, "PostgreSQL version query returned no result") return None From bae710ebc0da2401c90b8f445e50efc081bc5847 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 16 Jul 2026 10:24:25 +0800 Subject: [PATCH 14/39] refactor(postgres): merge composite_pk/derived_field, switch to expression-based DDL - Delete composite_pk/derived_field bridge files, schema SQL, and providers - Create tests/providers/fixtures/ with TABLE_EXPRESSIONS (basic, query, mixins, events) - Replace _load_postgres_schema with CreateTableExpression/DropTableExpression DDL - Update all query/relation bridge imports to new subdirectory paths - Fix _setup_model_async -> _setup_async_model typo in query provider --- tests/providers/basic.py | 420 +++++++-------- tests/providers/composite_pk.py | 130 ----- tests/providers/events.py | 127 +++-- tests/providers/fixtures/__init__.py | 14 + tests/providers/fixtures/_common.py | 34 ++ tests/providers/fixtures/basic.py | 499 ++++++++++++++++++ tests/providers/fixtures/events.py | 82 +++ tests/providers/fixtures/mixins.py | 132 +++++ tests/providers/fixtures/query.py | 383 ++++++++++++++ tests/providers/mixins.py | 153 +++--- tests/providers/query.py | 417 ++++++--------- tests/providers/registry.py | 37 +- tests/providers/relation.py | 295 ++++++----- .../feature/basic/test_basic_mapped_models.py | 4 +- .../feature/basic/test_bulk_operations.py | 4 +- .../feature/basic/test_crud.py | 4 +- .../basic/test_field_column_mapping.py | 4 +- .../feature/basic/test_fields.py | 4 +- .../basic/test_pydantic_native_validation.py | 4 +- .../feature/basic/test_type_adapter.py | 4 +- .../feature/basic/test_validation.py | 4 +- .../feature/composite_pk/conftest.py | 2 - .../composite_pk/schema/order_items.sql | 7 - .../feature/composite_pk/schema/orders.sql | 6 - .../composite_pk/schema/store_inventory.sql | 7 - .../test_composite_pk_active_query.py | 4 - .../test_composite_pk_activerecord.py | 4 - .../test_composite_pk_cte_query.py | 4 - .../test_composite_pk_set_operation.py | 4 - .../feature/derived_field/conftest.py | 9 - .../test_derived_field_testsuite.py | 16 - .../query/test_active_query_aggregate.py | 4 +- .../feature/query/test_active_query_basic.py | 4 +- .../feature/query/test_active_query_join.py | 4 +- .../feature/query/test_active_query_range.py | 4 +- .../query/test_active_query_set_operation.py | 4 +- .../feature/query/test_aggregate_queries.py | 4 +- .../feature/query/test_basic.py | 4 +- .../test_cross_database_compatibility.py | 4 +- .../query/test_cte_query_active_query.py | 4 +- .../query/test_cte_query_set_operation.py | 4 +- .../query/test_eager_loading_caching.py | 4 +- .../query/test_eager_loading_nested.py | 4 +- .../test_eager_loading_scenarios_async.py | 2 +- .../test_eager_loading_scenarios_sync.py | 4 +- .../query/test_eager_loading_with_all.py | 4 +- .../query/test_eager_loading_with_modifier.py | 4 +- .../query/test_eager_loading_with_one.py | 4 +- .../feature/query/test_error_handling.py | 4 +- .../query/test_example_query_fixtures.py | 4 +- .../feature/query/test_joins.py | 4 +- .../query/test_logging_data_summarization.py | 4 +- .../feature/query/test_queries.py | 2 +- .../feature/query/test_queries_async.py | 2 +- .../feature/query/test_query_optimization.py | 4 +- .../feature/query/test_range_queries.py | 4 +- .../query/test_range_query_detailed.py | 4 +- .../query/test_relational_validation.py | 4 +- .../feature/query/test_relations_basic.py | 4 +- .../query/test_relations_with_async.py | 2 +- .../feature/query/test_relations_with_sync.py | 2 +- .../feature/query/test_set_operation.py | 4 +- .../feature/query/test_special_queries.py | 4 +- .../feature/query/test_window_functions.py | 4 +- .../feature/relation/conftest.py | 4 +- .../feature/relation/test_base.py | 2 +- .../feature/relation/test_base_async.py | 2 +- .../feature/relation/test_boundary.py | 4 +- .../feature/relation/test_boundary_async.py | 2 +- .../feature/relation/test_cache.py | 2 +- .../feature/relation/test_cache_async.py | 2 +- .../feature/relation/test_cache_clearing.py | 4 +- .../feature/relation/test_derived_field.py | 4 +- .../relation/test_derived_field_async.py | 2 +- .../relation/test_derived_field_json.py | 4 +- .../relation/test_derived_field_json_async.py | 2 +- .../relation/test_descriptor_compat.py | 4 +- .../feature/relation/test_descriptors.py | 2 +- .../relation/test_descriptors_async.py | 2 +- .../feature/relation/test_integration.py | 4 +- .../relation/test_integration_async.py | 2 +- .../feature/relation/test_interfaces.py | 2 +- .../feature/relation/test_interfaces_async.py | 2 +- .../relation/test_modifier_behavior.py | 4 +- .../relation/test_modifier_behavior_async.py | 2 +- .../feature/relation/test_modifier_warning.py | 4 +- .../relation/test_modifier_warning_async.py | 2 +- .../test_nested_relationship_access.py | 2 +- .../test_nested_relationship_access_async.py | 2 +- .../feature/relation/test_path_validation.py | 4 +- .../relation/test_path_validation_async.py | 2 +- .../feature/relation/test_validator.py | 4 +- .../feature/relation/test_with_method.py | 4 +- .../relation/test_with_method_async.py | 2 +- 94 files changed, 1964 insertions(+), 1060 deletions(-) delete mode 100644 tests/providers/composite_pk.py create mode 100644 tests/providers/fixtures/__init__.py create mode 100644 tests/providers/fixtures/_common.py create mode 100644 tests/providers/fixtures/basic.py create mode 100644 tests/providers/fixtures/events.py create mode 100644 tests/providers/fixtures/mixins.py create mode 100644 tests/providers/fixtures/query.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py diff --git a/tests/providers/basic.py b/tests/providers/basic.py index ab385dbb..409cb17e 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -19,12 +19,17 @@ from rhosocial.activerecord.model import ActiveRecord # noqa: E402 from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter # noqa: E402 -from rhosocial.activerecord.testsuite.feature.basic.interfaces import IBasicProvider # noqa: E402 +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.basic.interfaces import IBasicSyncProvider, IBasicAsyncProvider # noqa: E402 from rhosocial.activerecord.testsuite.core.protocols import WorkerTestProtocol # noqa: E402 # Import the fixture selector utility from rhosocial.activerecord.testsuite.utils import select_fixture # noqa: E402 +# Expression-based DDL fixtures +from providers.fixtures._common import drop_table +from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_TABLE_EXPRESSIONS + # Import base version models (Python 3.8+) from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ( # noqa: E402 User as UserBase, TypeCase as TypeCaseBase, ValidatedFieldUser as ValidatedFieldUserBase, @@ -179,85 +184,65 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 -class BasicProvider(IBasicProvider, WorkerTestProtocol): - """ - This is the postgres backend's implementation for the basic features test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ - +class BasicProviderBase: def __init__(self): - self._active_backends = [] - self._active_async_backends = [] + self._scenario_db_files = {} def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) - def _track_backend(self, backend_instance, collection: List) -> None: + def get_yes_no_adapter(self) -> 'BaseSQLTypeAdapter': + return YesOrNoBooleanAdapter() + + def _track_backend(self, backend_instance, collection) -> None: if backend_instance not in collection: collection.append(backend_instance) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "basic", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() + + +class BasicSyncProvider(BasicProviderBase, IBasicSyncProvider, WorkerTestProtocol): + + def __init__(self): + super().__init__() + self._active_backends = [] + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - """A generic helper method to handle the setup for any given model.""" backend_class, config = get_scenario(scenario_name) model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ self._track_backend(backend_instance, self._active_backends) - self._reset_table_sync(model_class, table_name) return model_class - async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: # noqa: E501 - """A generic helper method to handle the setup for any given async model.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend - - _, config = get_scenario(scenario_name) - await model_class.configure(config, AsyncPostgresBackend) - - backend_instance = model_class.__backend__ - self._track_backend(backend_instance, self._active_async_backends) - - await self._reset_table_async(model_class, table_name) - return model_class - def _reset_table_sync(self, model_class: Type[ActiveRecord], table_name: str) -> None: + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(model_class.__backend__.dialect, table_name).to_sql() + model_class.__backend__.execute(sql, params, options=opts) except Exception as e: print(f"Could not drop table {table_name}: {e}") - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - model_class.__backend__.execute(schema_sql) - - async def _reset_table_async(self, model_class: Type[ActiveRecord], table_name: str) -> None: - try: - await model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception as e: - print(f"Could not drop table {table_name}: {e}") - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - await model_class.__backend__.execute(schema_sql) + if fn := BASIC_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(model_class.__backend__.dialect, table_name) + sql, params = create_expr.to_sql() + model_class.__backend__.execute(sql, params, options=opts) def _initialize_model_schema(self, model_class: Type[ActiveRecord], table_name: str) -> None: - """Initialize schema for a model that shares backend with another model.""" self._reset_table_sync(model_class, table_name) - async def _initialize_async_model_schema(self, model_class: Type[ActiveRecord], table_name: str) -> None: - """Initialize schema for an async model that shares backend with another model.""" - await self._reset_table_async(model_class, table_name) - - def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """Helper to set up multiple related models for a test, sharing a single backend.""" + def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: if not model_classes: return tuple() - first_model_class, first_table_name = model_classes[0] first_model = self._setup_model(first_model_class, scenario_name, first_table_name) shared_backend = first_model.__backend__ - result = [first_model] - for model_class, table_name in model_classes[1:]: model_class.__connection_config__ = first_model.__connection_config__ model_class.__backend_class__ = first_model.__backend_class__ @@ -265,58 +250,27 @@ def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], s self._track_backend(shared_backend, self._active_backends) self._initialize_model_schema(model_class, table_name) result.append(model_class) - return tuple(result) - async def _setup_multiple_models_async(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """Helper to set up multiple related async models for a test, sharing a single backend.""" - if not model_classes: - return tuple() - - first_model_class, first_table_name = model_classes[0] - first_model = await self._setup_async_model(first_model_class, scenario_name, first_table_name) - shared_backend = first_model.__backend__ - - result = [first_model] - - for model_class, table_name in model_classes[1:]: - model_class.__connection_config__ = first_model.__connection_config__ - model_class.__backend_class__ = first_model.__backend_class__ - model_class.__backend__ = shared_backend - self._track_backend(shared_backend, self._active_async_backends) - await self._initialize_async_model_schema(model_class, table_name) - result.append(model_class) - - return tuple(result) - - # --- Implementation of the IBasicProvider interface --- - def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for user model tests.""" return self._setup_model(User, scenario_name, "users") def setup_type_case_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for type case model tests.""" return self._setup_model(TypeCase, scenario_name, "type_cases") def setup_type_test_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for type test model tests.""" return self._setup_model(TypeTestModel, scenario_name, "type_tests") def setup_validated_field_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for validated field user model tests.""" return self._setup_model(ValidatedFieldUser, scenario_name, "validated_field_users") def setup_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for validated user model tests.""" return self._setup_model(ValidatedUser, scenario_name, "validated_users") def setup_pydantic_validated_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for Pydantic native validation tests.""" return self._setup_model(PydanticValidatedModel, scenario_name, "pydantic_validated_models") - def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for MappedUser, MappedPost, and MappedComment models.""" + def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_multiple_models([ (MappedUser, "users"), (MappedPost, "posts"), @@ -324,7 +278,6 @@ def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], T ], scenario_name) def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for ColumnMappingModel and MixedAnnotationModel.""" from rhosocial.activerecord_postgres_test.feature.basic.fixtures.models import PostgresMixedAnnotationModel return self._setup_multiple_models([ (ColumnMappingModel, "column_mapping_items"), @@ -332,200 +285,211 @@ def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], .. ], scenario_name) def setup_type_adapter_model_and_schema(self, scenario_name: str = None) -> Type[ActiveRecord]: - """Sets up the database for the `TypeAdapterTest` model tests.""" if scenario_name is None: scenario_name = self.get_test_scenarios()[0] if self.get_test_scenarios() else "default" return self._setup_model(TypeAdapterTest, scenario_name, "type_adapter_tests") - async def setup_async_type_adapter_model_and_schema(self, scenario_name: str = None) -> Type[ActiveRecord]: - """Sets up the database for the `AsyncTypeAdapterTest` model tests.""" - if scenario_name is None: - scenario_name = self.get_test_scenarios()[0] if self.get_test_scenarios() else "default" - return await self._setup_async_model(AsyncTypeAdapterTest, scenario_name, "type_adapter_tests") - def setup_bulk_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for the `BulkUser` model tests.""" return self._setup_model(BulkUser, scenario_name, "bulk_users") - async def setup_async_bulk_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for the `AsyncBulkUser` model tests.""" - return await self._setup_async_model(AsyncBulkUser, scenario_name, "bulk_users") + def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - def get_yes_no_adapter(self) -> 'BaseSQLTypeAdapter': - """Returns an instance of the YesOrNoBooleanAdapter.""" - return YesOrNoBooleanAdapter() + def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + def setup_product_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + def setup_product_form_a_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + def setup_product_with_proxy_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + def setup_product_with_column_and_adapter_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + def get_worker_connection_params(self, scenario_name: str, fixture_type: str = None) -> dict: + from .scenarios import SCENARIO_MAP + is_async = fixture_type and fixture_type.startswith('async_') + backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' + table_name = 'users' + if fixture_type: + base_type = fixture_type.replace('async_', '') + table_map = { + 'user': 'users', + 'type_case': 'type_cases', + 'type_test': 'type_tests', + 'validated_field_user': 'validated_field_users', + 'validated_user': 'validated_users', + 'pydantic_validated_model': 'pydantic_validated_models', + 'type_adapter_test': 'type_adapter_tests', + } + table_name = table_map.get(base_type, 'users') + if scenario_name not in SCENARIO_MAP: + if SCENARIO_MAP: + scenario_name = next(iter(SCENARIO_MAP)) + else: + raise ValueError("No scenarios registered") + config_dict = SCENARIO_MAP[scenario_name] + return { + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_class_name': backend_class_name, + 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', + 'config_class_name': 'PostgresConnectionConfig', + 'config_kwargs': config_dict, + 'schema_sql': self._load_postgres_schema(f'{table_name}.sql'), + } + + def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: + return self._load_postgres_schema(f'{table_name}.sql') + + def cleanup_after_test(self, scenario_name: str): + tables_to_drop = [ + 'users', 'type_cases', 'type_tests', 'validated_field_users', + 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', + 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' + ] + for backend_instance in self._active_backends: + try: + for table_name in tables_to_drop: + try: + backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + backend_instance.disconnect() + except: # noqa: E722 + pass + self._active_backends.clear() + + +class BasicAsyncProvider(BasicProviderBase, IBasicAsyncProvider): + + def __init__(self): + super().__init__() + 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 + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + self._track_backend(backend_instance, self._active_async_backends) + await self._reset_table_async(model_class, table_name) + return model_class + + async def _reset_table_async(self, model_class: Type[ActiveRecord], table_name: str) -> None: + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(model_class.__backend__.dialect, table_name).to_sql() + await model_class.__backend__.execute(sql, params, options=opts) + except Exception as e: + print(f"Could not drop table {table_name}: {e}") + if fn := BASIC_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(model_class.__backend__.dialect, table_name) + sql, params = create_expr.to_sql() + await model_class.__backend__.execute(sql, params, options=opts) - # --- Async implementations --- + async def _initialize_async_model_schema(self, model_class: Type[ActiveRecord], table_name: str) -> None: + await self._reset_table_async(model_class, table_name) - async def setup_async_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async user model tests.""" + async def _setup_multiple_models_async(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + if not model_classes: + return tuple() + first_model_class, first_table_name = model_classes[0] + first_model = await self._setup_async_model(first_model_class, scenario_name, first_table_name) + shared_backend = first_model.__backend__ + result = [first_model] + for model_class, table_name in model_classes[1:]: + model_class.__connection_config__ = first_model.__connection_config__ + model_class.__backend_class__ = first_model.__backend_class__ + model_class.__backend__ = shared_backend + self._track_backend(shared_backend, self._active_async_backends) + await self._initialize_async_model_schema(model_class, table_name) + result.append(model_class) + return tuple(result) + + async def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncUser, scenario_name, "users") - async def setup_async_type_case_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async type case model tests.""" + async def setup_type_case_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncTypeCase, scenario_name, "type_cases") - async def setup_async_type_test_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async type test model tests.""" + async def setup_type_test_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncTypeTestModel, scenario_name, "type_tests") - async def setup_async_validated_field_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async validated field user model tests.""" + async def setup_validated_field_user_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncValidatedFieldUser, scenario_name, "validated_field_users") - async def setup_async_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async validated user model tests.""" + async def setup_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncValidatedUser, scenario_name, "validated_users") - async def setup_async_pydantic_validated_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for async Pydantic native validation tests.""" + async def setup_pydantic_validated_model(self, scenario_name: str) -> Type[ActiveRecord]: return await self._setup_async_model(AsyncPydanticValidatedModel, scenario_name, "pydantic_validated_models") - async def setup_async_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for AsyncMappedUser, AsyncMappedPost, and AsyncMappedComment models.""" + async def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return await self._setup_multiple_models_async([ (AsyncMappedUser, "users"), (AsyncMappedPost, "posts"), (AsyncMappedComment, "comments") ], scenario_name) - async def setup_async_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for AsyncColumnMappingModel and AsyncMixedAnnotationModel.""" + async def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: from rhosocial.activerecord_postgres_test.feature.basic.fixtures.models import AsyncPostgresMixedAnnotationModel return await self._setup_multiple_models_async([ (AsyncColumnMappingModel, "column_mapping_items"), (AsyncPostgresMixedAnnotationModel, "mixed_annotation_items") ], scenario_name) - async def cleanup_after_test_async(self, scenario_name: str): - """ - Performs async cleanup after a test, dropping all tables and disconnecting async backends. - """ - tables_to_drop = [ - 'users', 'type_cases', 'type_tests', 'validated_field_users', - 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', - 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' - ] - for backend_instance in self._active_async_backends: - try: - for table_name in tables_to_drop: - try: - await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - finally: - try: - await backend_instance.disconnect() - except: # noqa: E722 - pass + async def setup_type_adapter_model_and_schema(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTypeAdapterTest, scenario_name, "type_adapter_tests") - self._active_async_backends.clear() + async def setup_bulk_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncBulkUser, scenario_name, "bulk_users") - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for basic feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "basic", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) + async def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() + async def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This now iterates through the backends - that were created during setup, drops tables, and explicitly disconnects them. - """ + async def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_form_a_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_with_proxy_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def setup_product_with_column_and_adapter_model(self, scenario_name: str) -> Type[ActiveRecord]: + raise NotImplementedError + + async def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ 'users', 'type_cases', 'type_tests', 'validated_field_users', 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' ] - for backend_instance in self._active_backends: + for backend_instance in self._active_async_backends: try: - # Drop all tables that might have been created for basic tests for table_name in tables_to_drop: try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: - # Continue with other tables if one fails pass finally: - # Always disconnect the backend instance that was used in the test try: - backend_instance.disconnect() + await backend_instance.disconnect() except: # noqa: E722 - # Ignore errors during disconnect pass - - # Clear the list of active backends for the next test - self._active_backends.clear() - - # --- Implementation of WorkerTestProtocol --- - - def get_worker_connection_params(self, scenario_name: str, fixture_type: str = None) -> dict: - """ - Return serializable connection parameters for Worker processes. - - This method provides all information needed to recreate the database - connection in a Worker process, including the schema SQL for table creation. - - Args: - scenario_name: The test scenario name - fixture_type: Optional fixture type hint (e.g., 'user', 'async_user') - Used to determine if async backend is needed - - Returns: - Dictionary with connection parameters and schema SQL - """ - from .scenarios import SCENARIO_MAP - - # Determine if async backend is needed based on fixture_type - is_async = fixture_type and fixture_type.startswith('async_') - backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' - - # Determine schema file based on fixture_type - table_name = 'users' # default - if fixture_type: - # Remove 'async_' prefix if present to get the base type - base_type = fixture_type.replace('async_', '') - table_map = { - 'user': 'users', - 'type_case': 'type_cases', - 'type_test': 'type_tests', - 'validated_field_user': 'validated_field_users', - 'validated_user': 'validated_users', - 'pydantic_validated_model': 'pydantic_validated_models', - 'type_adapter_test': 'type_adapter_tests', - } - table_name = table_map.get(base_type, 'users') - - # Get connection config from scenario - if scenario_name not in SCENARIO_MAP: - if SCENARIO_MAP: - scenario_name = next(iter(SCENARIO_MAP)) - else: - raise ValueError("No scenarios registered") - - config_dict = SCENARIO_MAP[scenario_name] - - return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', - 'backend_class_name': backend_class_name, - 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', - 'config_class_name': 'PostgresConnectionConfig', - 'config_kwargs': config_dict, - 'schema_sql': self._load_postgres_schema(f'{table_name}.sql'), - } - - def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: - """ - Return the SQL statement to create a specific table. - - Args: - scenario_name: The test scenario name (unused for Postgres as schema is fixed) - table_name: Name of the table to create - - Returns: - CREATE TABLE SQL statement - """ - return self._load_postgres_schema(f'{table_name}.sql') + self._active_async_backends.clear() diff --git a/tests/providers/composite_pk.py b/tests/providers/composite_pk.py deleted file mode 100644 index a2f7f39f..00000000 --- a/tests/providers/composite_pk.py +++ /dev/null @@ -1,130 +0,0 @@ -# tests/providers/composite_pk.py -import os -from typing import Type, List - -from rhosocial.activerecord.model import ActiveRecord - -from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( - OrderItem as OrderItemBase, - StoreInventory as StoreInventoryBase, - Order as OrderBase, - MappedOrderItem as MappedOrderItemBase, -) -from rhosocial.activerecord.testsuite.feature.composite_pk.fixtures.models import ( - AsyncOrderItem as AsyncOrderItemBase, - AsyncStoreInventory as AsyncStoreInventoryBase, - AsyncOrder as AsyncOrderBase, - AsyncMappedOrderItem as AsyncMappedOrderItemBase, -) - -from .scenarios import get_enabled_scenarios, get_scenario - - -class CompositePKProvider: - def __init__(self): - self._active_backends = [] - self._active_async_backends = [] - - def get_test_scenarios(self) -> List[str]: - return list(get_enabled_scenarios().keys()) - - def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - backend_class, config = get_scenario(scenario_name) - model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ - if backend_instance not in self._active_backends: - self._active_backends.append(backend_instance) - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - backend_instance.execute(schema_sql) - return model_class - - 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 - _, config = get_scenario(scenario_name) - await model_class.configure(config, AsyncPostgresBackend) - backend_instance = model_class.__backend__ - if backend_instance not in self._active_async_backends: - self._active_async_backends.append(backend_instance) - try: - await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - await backend_instance.execute(schema_sql) - return model_class - - def _load_postgres_schema(self, filename: str) -> str: - schema_dir = os.path.join( - os.path.dirname(__file__), - "..", - "rhosocial", - "activerecord_postgres_test", - "feature", - "composite_pk", - "schema", - ) - schema_path = os.path.join(schema_dir, filename) - with open(schema_path, "r", encoding="utf-8") as f: - return f.read() - - def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(OrderItemBase, scenario_name, "order_items") - - def setup_store_inventory_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(StoreInventoryBase, scenario_name, "store_inventory") - - def setup_order_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(OrderBase, scenario_name, "orders") - - async def setup_async_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncOrderItemBase, scenario_name, "order_items") - - async def setup_async_store_inventory_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncStoreInventoryBase, scenario_name, "store_inventory") - - async def setup_async_order_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncOrderBase, scenario_name, "orders") - - def setup_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return self._setup_model(MappedOrderItemBase, scenario_name, "order_items") - - async def setup_async_mapped_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: - return await self._setup_async_model(AsyncMappedOrderItemBase, scenario_name, "order_items") - - def cleanup_after_test(self, scenario_name: str): - tables = ["order_items", "store_inventory", "orders"] - for backend in self._active_backends: - try: - for t in tables: - try: - backend.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE') - except Exception: - pass - except Exception: - pass - try: - backend.disconnect() - except Exception: - pass - self._active_backends.clear() - - async def cleanup_after_test_async(self, scenario_name: str): - tables = ["order_items", "store_inventory", "orders"] - for backend in self._active_async_backends: - try: - for t in tables: - try: - await backend.execute(f'DROP TABLE IF EXISTS "{t}" CASCADE') - except Exception: - pass - except Exception: - pass - try: - await backend.disconnect() - except Exception: - pass - self._active_async_backends.clear() diff --git a/tests/providers/events.py b/tests/providers/events.py index b27cecc5..d414fa6f 100644 --- a/tests/providers/events.py +++ b/tests/providers/events.py @@ -26,7 +26,8 @@ # Import base version models (Python 3.8+) from rhosocial.activerecord.testsuite.feature.events.fixtures.models import ( # noqa: E402 EventTestModel as EventTestModelBase, - EventTrackingModel as EventTrackingModelBase + EventTrackingModel as EventTrackingModelBase, + AsyncEventTestModel as AsyncEventTestModelBase, ) # Conditionally import Python 3.10+ models @@ -78,95 +79,121 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s # Select models EventTestModel = _select_model_class(EventTestModelBase, EventTestModel312, EventTestModel311, EventTestModel310, "EventTestModel") # noqa: E501 EventTrackingModel = _select_model_class(EventTrackingModelBase, EventTrackingModel312, EventTrackingModel311, EventTrackingModel310, "EventTrackingModel") # noqa: E501 +AsyncEventTestModel = AsyncEventTestModelBase -from rhosocial.activerecord.testsuite.feature.events.interfaces import IEventsProvider # noqa: E402 +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.events.interfaces import IEventsSyncProvider, IEventsAsyncProvider # noqa: E402 # ...and the scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 +# Expression-based DDL fixtures +from .fixtures._common import drop_table +from .fixtures.events import TABLE_EXPRESSIONS as EVENTS_TABLE_EXPRESSIONS -class EventsProvider(IEventsProvider): - """ - This is the postgres backend's implementation for the events test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ +class EventsProviderBase: def __init__(self): - # This list will track the backend instances created during the setup phase. self._active_backends = [] def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "events", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() + + +class EventsSyncProvider(EventsProviderBase, IEventsSyncProvider): + + def __init__(self): + super().__init__() + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - """A generic helper method to handle the setup for any given model.""" - # 1. Get the backend class (PostgresBackend) and connection config for the requested scenario. backend_class, config = get_scenario(scenario_name) - - # 2. Configure the generic model class with our specific backend and config. model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ if backend_instance not in self._active_backends: self._active_backends.append(backend_instance) - - # 3. Prepare the database schema. + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - # Drop the table if it exists, with cascade to handle dependencies. - model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + backend_instance.execute(sql, params, options=opts) except Exception as e: - # Ignore errors if the table didn't exist, but print for debugging. print(f"Could not drop table {table_name}: {e}") - pass - - # Execute the schema SQL to create the table. - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - model_class.__backend__.execute(schema_sql) - + if fn := EVENTS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) return model_class - # --- Implementation of the IEventsProvider interface --- - def setup_event_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for event model tests.""" - return self._setup_model(EventTestModel, scenario_name, "event_tests") def setup_event_tracking_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for event tracking model tests.""" - return self._setup_model(EventTrackingModel, scenario_name, "event_tracking_models") - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for events feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "events", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This now iterates through the backends - that were created during setup, drops tables, and explicitly disconnects them. - """ for backend_instance in self._active_backends: try: - # Drop all tables that might have been created for events tests for table_name in ['event_tests', 'event_tracking_models']: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: - # Continue with other tables if one fails pass finally: - # Always disconnect the backend instance that was used in the test try: backend_instance.disconnect() except: # noqa: E722 - # Ignore errors during disconnect pass + self._active_backends.clear() + + +class EventsAsyncProvider(EventsProviderBase, IEventsAsyncProvider): + + def __init__(self): + super().__init__() + 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 + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + if backend_instance not in self._active_async_backends: + self._active_async_backends.append(backend_instance) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception as e: + print(f"Could not drop table {table_name}: {e}") + if fn := EVENTS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + return model_class - # Clear the list of active backends for the next test - self._active_backends.clear() \ No newline at end of file + async def setup_event_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncEventTestModel, scenario_name, "event_tests") + + async def setup_event_tracking_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(EventTrackingModel, scenario_name, "event_tracking_models") + + async def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_async_backends: + try: + for table_name in ['event_tests', 'event_tracking_models']: + try: + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + await backend_instance.disconnect() + except: # noqa: E722 + pass + self._active_async_backends.clear() diff --git a/tests/providers/fixtures/__init__.py b/tests/providers/fixtures/__init__.py new file mode 100644 index 00000000..70857923 --- /dev/null +++ b/tests/providers/fixtures/__init__.py @@ -0,0 +1,14 @@ +# tests/providers/fixtures/__init__.py +"""PostgreSQL-specific DDL expression fixtures for the testsuite providers. + +Each module in this package exposes a ``TABLE_EXPRESSIONS`` mapping of +table name -> factory callable ``Callable[[DialectLike, str], CreateTableExpression]``. + +The factory functions build :class:`CreateTableExpression` instances that emit +PostgreSQL-compatible DDL (SERIAL / UUID / JSONB / BYTEA / TIMESTAMPTZ / BOOLEAN / +DECIMAL / VARCHAR / TEXT / FK constraints / CHECK / UNIQUE / NOT NULL / DEFAULT). +The pre-existing ``.sql`` schema files under +``tests/rhosocial/activerecord_postgres_test/feature//schema/`` +remain as the authoritative reference for what the expressions here must +produce; they are simply no longer read at runtime by the providers. +""" diff --git a/tests/providers/fixtures/_common.py b/tests/providers/fixtures/_common.py new file mode 100644 index 00000000..d07e02b9 --- /dev/null +++ b/tests/providers/fixtures/_common.py @@ -0,0 +1,34 @@ +# tests/providers/fixtures/_common.py +"""Shared helpers for the PostgreSQL DDL expression fixtures. + +Provides the ``drop_table`` helper used by all feature providers. +""" + +from __future__ import annotations + +from typing import Tuple + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, + DropTableExpression, + TableExpression, +) + + +def drop_table(dialect, table_name: str) -> DropTableExpression: + """Build a ``DROP TABLE IF EXISTS table_name CASCADE`` expression.""" + return DropTableExpression( + dialect=dialect, + table=TableExpression(dialect, table_name), + if_exists=True, + ) + + +def create_table_sql(expr: CreateTableExpression) -> Tuple[str, tuple]: + """Generate canonical PostgreSQL DDL for a :class:`CreateTableExpression`. + + For PostgreSQL no post-processing is required (unlike MySQL where storage + option quotes must be stripped). This pass-through wrapper exists so the + call site is the same as the MySQL counterpart. + """ + return expr.to_sql() diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py new file mode 100644 index 00000000..aaea9129 --- /dev/null +++ b/tests/providers/fixtures/basic.py @@ -0,0 +1,499 @@ +# tests/providers/fixtures/basic.py +"""DDL expressions for the ``feature/basic`` table group (PostgreSQL). + +Each factory builds a :class:`CreateTableExpression` whose generated DDL is +semantically equivalent to the reference ``.sql`` schema files under +``tests/rhosocial/activerecord_postgres_test/feature/basic/schema/``. Those +``.sql`` files are kept as the authoritative reference and are no longer +loaded at runtime. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, + ForeignKeyConstraint, + TableConstraint, + TableConstraintType, + ReferentialAction, +) +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + BigIntType, + BooleanType, + DateType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + JsonBType, + SmallIntType, + TextType, + TimeTzType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, + PostgresUUIDType, + PostgresByteaType, +) + +_CASCADE = ReferentialAction.CASCADE + + +# --------------------------------------------------------------------------- +# basic/users.sql +# --------------------------------------------------------------------------- + +def create_users_table(dialect, table_name: str = "users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("balance", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/type_cases.sql +# --------------------------------------------------------------------------- + +def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresUUIDType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tiny_int", SmallIntType()), + ColumnDefinition("small_int", SmallIntType()), + ColumnDefinition("big_int", BigIntType()), + ColumnDefinition("float_val", FloatType()), + ColumnDefinition("double_val", DoubleType()), + ColumnDefinition("decimal_val", DecimalType(precision=10, scale=4)), + ColumnDefinition("char_val", VarCharType(255)), + ColumnDefinition("varchar_val", VarCharType(255)), + ColumnDefinition("text_val", TextType()), + ColumnDefinition("date_val", DateType()), + ColumnDefinition("time_val", TimeTzType()), + ColumnDefinition("timestamp_val", TimestampTzType()), + ColumnDefinition("blob_val", PostgresByteaType()), + ColumnDefinition("json_val", JsonBType()), + ColumnDefinition("array_val", ArrayType(IntegerType())), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/type_tests.sql +# --------------------------------------------------------------------------- + +def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresUUIDType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("string_field", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("int_field", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("float_field", FloatType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("decimal_field", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("bool_field", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("datetime_field", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("json_field", JsonBType()), + ColumnDefinition("nullable_field", VarCharType(255)), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/validated_field_users.sql +# --------------------------------------------------------------------------- + +def create_validated_field_users_table(dialect, table_name: str = "validated_field_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("balance", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("credit_score", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=300)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="active")]), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/validated_users.sql +# --------------------------------------------------------------------------- + +def create_validated_users_table(dialect, table_name: str = "validated_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/pydantic_validated_models.sql +# --------------------------------------------------------------------------- + +def create_pydantic_validated_models_table(dialect, table_name: str = "pydantic_validated_models") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("code", VarCharType(32)), + ColumnDefinition("quantity", IntegerType()), + ColumnDefinition("step_count", IntegerType()), + ColumnDefinition("price", DecimalType(precision=10, scale=2)), + ColumnDefinition("start_at", TimestampTzType()), + ColumnDefinition("end_at", TimestampTzType()), + ColumnDefinition("status", VarCharType(32)), + ColumnDefinition("normalized_name", VarCharType(50)), + ColumnDefinition("created_token", VarCharType(255)), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/bulk_users.sql +# --------------------------------------------------------------------------- + +def create_bulk_users_table(dialect, table_name: str = "bulk_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="")]), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/posts.sql +# --------------------------------------------------------------------------- + +def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("author", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("published_at", TimestampTzType()), + ColumnDefinition("published", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/comments.sql +# --------------------------------------------------------------------------- + +def create_comments_table(dialect, table_name: str = "comments") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("post_ref", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("author", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("text", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition("approved", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["post_ref"], foreign_key_table="posts", foreign_key_columns=["id"], + on_delete=_CASCADE), + ForeignKeyConstraint(columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/column_mapping_items.sql +# --------------------------------------------------------------------------- + +def create_column_mapping_items_table(dialect, table_name: str = "column_mapping_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("item_total", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("remarks", IntegerType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/mixed_annotation_items.sql +# --------------------------------------------------------------------------- + +def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annotation_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tags", ArrayType(TextType())), + ColumnDefinition("meta", TextType()), + ColumnDefinition("description", TextType()), + ColumnDefinition("status", TextType()), + ], + ) + + +# --------------------------------------------------------------------------- +# basic/type_adapter_tests.sql +# --------------------------------------------------------------------------- + +def create_type_adapter_tests_table(dialect, table_name: str = "type_adapter_tests") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=True, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("optional_name", VarCharType(255)), + ColumnDefinition("optional_age", IntegerType()), + ColumnDefinition("last_login", TimestampTzType()), + ColumnDefinition("is_premium", BooleanType()), + ColumnDefinition("unsupported_union", VarCharType(255)), + ColumnDefinition("custom_bool", VarCharType(10)), + ColumnDefinition("optional_custom_bool", VarCharType(10)), + ], + ) + + +# --------------------------------------------------------------------------- +# composite_pk/order_items.sql (composite PK) +# --------------------------------------------------------------------------- + +def create_composite_pk_order_items_table(dialect, table_name: str = "order_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("order_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("unit_price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["order_id", "product_id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# composite_pk/store_inventory.sql (composite PK, no FK) +# --------------------------------------------------------------------------- + +def create_store_inventory_table(dialect, table_name: str = "store_inventory") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("store_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("batch_id", VarCharType(64), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("stock", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ], + table_constraints=[ + TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + columns=["store_id", "product_id", "batch_id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# derived_field/orders.sql (single PK) +# --------------------------------------------------------------------------- + +def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("total", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TextType()), + ColumnDefinition("updated_at", TextType()), + ], + ) + + +# --------------------------------------------------------------------------- +# derived_field/product.sql (single PK) +# --------------------------------------------------------------------------- + +def create_product_table(dialect, table_name: str = "product") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=True, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("price", FloatType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "users": create_users_table, + "type_cases": create_type_cases_table, + "type_tests": create_type_tests_table, + "validated_field_users": create_validated_field_users_table, + "validated_users": create_validated_users_table, + "pydantic_validated_models": create_pydantic_validated_models_table, + "bulk_users": create_bulk_users_table, + "posts": create_posts_table, + "comments": create_comments_table, + "column_mapping_items": create_column_mapping_items_table, + "mixed_annotation_items": create_mixed_annotation_items_table, + "type_adapter_tests": create_type_adapter_tests_table, + "order_items": create_composite_pk_order_items_table, + "store_inventory": create_store_inventory_table, + "orders": create_orders_table, + "product": create_product_table, +} diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py new file mode 100644 index 00000000..39960fb5 --- /dev/null +++ b/tests/providers/fixtures/events.py @@ -0,0 +1,82 @@ +# tests/providers/fixtures/events.py +"""DDL expressions for the ``feature/events`` table group (PostgreSQL). + +Reference: ``tests/rhosocial/activerecord_postgres_test/feature/events/schema/``. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + TextType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, +) + + +# --------------------------------------------------------------------------- +# events/event_tests.sql +# --------------------------------------------------------------------------- + +def create_event_tests_table(dialect, table_name: str = "event_tests") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="draft")]), + ColumnDefinition("revision", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# events/event_tracking_models.sql +# --------------------------------------------------------------------------- + +def create_event_tracking_models_table(dialect, table_name: str = "event_tracking_models") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("view_count", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition("last_viewed_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "event_tests": create_event_tests_table, + "event_tracking_models": create_event_tracking_models_table, +} diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py new file mode 100644 index 00000000..b7c8f832 --- /dev/null +++ b/tests/providers/fixtures/mixins.py @@ -0,0 +1,132 @@ +# tests/providers/fixtures/mixins.py +"""DDL expressions for the ``feature/mixins`` table group (PostgreSQL). + +Reference: ``tests/rhosocial/activerecord_postgres_test/feature/mixins/schema/``. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + BooleanType, + DecimalType, + IntegerType, + TextType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, +) + + +# --------------------------------------------------------------------------- +# mixins/combined_articles.sql +# --------------------------------------------------------------------------- + +def create_combined_articles_table(dialect, table_name: str = "combined_articles") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="draft")]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition("version", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("deleted_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ], + ) + + +# --------------------------------------------------------------------------- +# mixins/tasks.sql +# --------------------------------------------------------------------------- + +def create_tasks_table(dialect, table_name: str = "tasks") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("is_completed", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition("deleted_at", TimestampTzType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ], + ) + + +# --------------------------------------------------------------------------- +# mixins/timestamped_posts.sql +# --------------------------------------------------------------------------- + +def create_timestamped_posts_table(dialect, table_name: str = "timestamped_posts") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# mixins/versioned_products.sql +# --------------------------------------------------------------------------- + +def create_versioned_products_table(dialect, table_name: str = "versioned_products") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("version", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "combined_articles": create_combined_articles_table, + "tasks": create_tasks_table, + "timestamped_posts": create_timestamped_posts_table, + "versioned_products": create_versioned_products_table, +} diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py new file mode 100644 index 00000000..86e279fc --- /dev/null +++ b/tests/providers/fixtures/query.py @@ -0,0 +1,383 @@ +# tests/providers/fixtures/query.py +"""DDL expressions for the ``feature/query`` table group (PostgreSQL). + +Reference: ``tests/rhosocial/activerecord_postgres_test/feature/query/schema/``. +""" + +from typing import Callable, Dict + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnDefinition, + ColumnConstraint, + ColumnConstraintType, + ForeignKeyConstraint, + ReferentialAction, +) +from rhosocial.activerecord.backend.expression.types import ( + BooleanType, + DecimalType, + IntegerType, + JsonBType, + TextType, + TimestampTzType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresSerialType, +) + +_CASCADE = ReferentialAction.CASCADE + + +# --------------------------------------------------------------------------- +# query/users.sql +# --------------------------------------------------------------------------- + +def create_users_table(dialect, table_name: str = "users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("balance", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("is_active", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# query/posts.sql +# --------------------------------------------------------------------------- + +def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("title", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType()), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="published")]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/comments.sql +# --------------------------------------------------------------------------- + +def create_comments_table(dialect, table_name: str = "comments") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("post_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("content", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("is_hidden", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ForeignKeyConstraint(columns=["post_id"], foreign_key_table="posts", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/orders.sql +# --------------------------------------------------------------------------- + +def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("order_number", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("total_amount", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="pending")]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# query/order_items.sql +# --------------------------------------------------------------------------- + +def create_order_items_table(dialect, table_name: str = "order_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("order_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("unit_price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("subtotal", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["order_id"], foreign_key_table="orders", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/profiles.sql +# --------------------------------------------------------------------------- + +def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("bio", TextType()), + ColumnDefinition("avatar_url", VarCharType(512)), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ], + ) + + +# --------------------------------------------------------------------------- +# query/json_users.sql +# --------------------------------------------------------------------------- + +def create_json_users_table(dialect, table_name: str = "json_users") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("username", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("email", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("age", IntegerType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition("settings", JsonBType()), + ColumnDefinition("tags", JsonBType()), + ColumnDefinition("profile", JsonBType()), + ColumnDefinition("roles", JsonBType()), + ColumnDefinition("scores", JsonBType()), + ColumnDefinition("subscription", JsonBType()), + ColumnDefinition("preferences", JsonBType()), + ], + ) + + +# --------------------------------------------------------------------------- +# query/nodes.sql (self-referential FK) +# --------------------------------------------------------------------------- + +def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("parent_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ColumnDefinition("value", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["parent_id"], foreign_key_table="nodes", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/searchable_items.sql +# --------------------------------------------------------------------------- + +def create_searchable_items_table(dialect, table_name: str = "searchable_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(255)), + ColumnDefinition("tags", TextType()), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + ) + + +# --------------------------------------------------------------------------- +# query/extended_orders.sql +# --------------------------------------------------------------------------- + +def create_extended_orders_table(dialect, table_name: str = "extended_orders") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("order_number", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("total_amount", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition("status", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="pending")]), + ColumnDefinition("priority", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="medium")]), + ColumnDefinition("region", VarCharType(50), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="default")]), + ColumnDefinition("category", VarCharType(255)), + ColumnDefinition("product", VarCharType(255)), + ColumnDefinition("department", VarCharType(255)), + ColumnDefinition("year", VarCharType(10)), + ColumnDefinition("quarter", VarCharType(10)), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +# --------------------------------------------------------------------------- +# query/extended_order_items.sql +# --------------------------------------------------------------------------- + +def create_extended_order_items_table(dialect, table_name: str = "extended_order_items") -> CreateTableExpression: + return CreateTableExpression( + dialect=dialect, + table=table_name, + if_not_exists=False, + columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("order_id", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("product_name", VarCharType(255), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("quantity", IntegerType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition("price", DecimalType(precision=10, scale=2), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("category", VarCharType(255)), + ColumnDefinition("region", VarCharType(50)), + ColumnDefinition("created_at", TimestampTzType()), + ColumnDefinition("updated_at", TimestampTzType()), + ], + table_constraints=[ + ForeignKeyConstraint(columns=["order_id"], foreign_key_table="extended_orders", foreign_key_columns=["id"], + on_delete=_CASCADE), + ], + ) + + +TABLE_EXPRESSIONS: Dict[str, Callable] = { + "users": create_users_table, + "posts": create_posts_table, + "comments": create_comments_table, + "orders": create_orders_table, + "order_items": create_order_items_table, + "profiles": create_profiles_table, + "json_users": create_json_users_table, + "nodes": create_nodes_table, + "searchable_items": create_searchable_items_table, + "extended_orders": create_extended_orders_table, + "extended_order_items": create_extended_order_items_table, +} diff --git a/tests/providers/mixins.py b/tests/providers/mixins.py index d803b6de..fa5b8ed7 100644 --- a/tests/providers/mixins.py +++ b/tests/providers/mixins.py @@ -28,7 +28,11 @@ TimestampedPost as TimestampedPostBase, VersionedProduct as VersionedProductBase, Task as TaskBase, - CombinedArticle as CombinedArticleBase + CombinedArticle as CombinedArticleBase, + AsyncTimestampedPost as AsyncTimestampedPostBase, + AsyncVersionedProduct as AsyncVersionedProductBase, + AsyncTask as AsyncTaskBase, + AsyncCombinedArticle as AsyncCombinedArticleBase, ) # Conditionally import Python 3.10+ models @@ -89,113 +93,138 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s Task = _select_model_class(TaskBase, Task312, Task311, Task310, "Task") CombinedArticle = _select_model_class(CombinedArticleBase, CombinedArticle312, CombinedArticle311, CombinedArticle310, "CombinedArticle") # noqa: E501 -from rhosocial.activerecord.testsuite.feature.mixins.interfaces import IMixinsProvider # noqa: E402 +AsyncTimestampedPost = AsyncTimestampedPostBase +AsyncVersionedProduct = AsyncVersionedProductBase +AsyncTask = AsyncTaskBase +AsyncCombinedArticle = AsyncCombinedArticleBase + +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.mixins.interfaces import IMixinsSyncProvider, IMixinsAsyncProvider # noqa: E402 # ...and the scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 +# Expression-based DDL fixtures +from .fixtures._common import drop_table +from .fixtures.mixins import TABLE_EXPRESSIONS as MIXINS_TABLE_EXPRESSIONS -class MixinsProvider(IMixinsProvider): - """ - This is the postgres backend's implementation for the mixins test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ +class MixinsProviderBase: def __init__(self): - # This list will track the backend instances created during the setup phase. self._active_backends = [] def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "mixins", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() + + +class MixinsSyncProvider(MixinsProviderBase, IMixinsSyncProvider): + + def __init__(self): + super().__init__() + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - """A generic helper method to handle the setup for any given model.""" - # 1. Get the backend class (PostgresBackend) and connection config for the requested scenario. backend_class, config = get_scenario(scenario_name) - - # 2. Configure the generic model class with our specific backend and config. model_class.configure(config, backend_class) - backend_instance = model_class.__backend__ if backend_instance not in self._active_backends: self._active_backends.append(backend_instance) - - # 3. Prepare the database schema. + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - # Drop the table if it exists, with cascade to handle dependencies. - model_class.__backend__.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + backend_instance.execute(sql, params, options=opts) except Exception as e: - # Ignore errors if the table didn't exist, but print for debugging. print(f"Could not drop table {table_name}: {e}") - pass - - # Execute the schema SQL to create the table. - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - model_class.__backend__.execute(schema_sql) - + if fn := MIXINS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) return model_class - # --- Implementation of the IMixinsProvider interface --- - - - - - - - def setup_timestamped_post_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for timestamps model tests.""" - return self._setup_model(TimestampedPost, scenario_name, "timestamped_posts") def setup_versioned_product_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for optimistic lock model tests.""" - return self._setup_model(VersionedProduct, scenario_name, "versioned_products") def setup_task_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for soft delete model tests.""" - return self._setup_model(Task, scenario_name, "tasks") def setup_combined_article_model(self, scenario_name: str) -> Type[ActiveRecord]: - """Sets up the database for combined article model tests.""" - return self._setup_model(CombinedArticle, scenario_name, "combined_articles") - - - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for mixins feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "mixins", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This now iterates through the backends - that were created during setup, drops tables, and explicitly disconnects them. - """ for backend_instance in self._active_backends: try: - # Drop all tables that might have been created for mixins tests for table_name in ['timestamped_posts', 'versioned_products', 'tasks', 'combined_articles']: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: - # Continue with other tables if one fails pass finally: - # Always disconnect the backend instance that was used in the test try: backend_instance.disconnect() except: # noqa: E722 - # Ignore errors during disconnect pass + self._active_backends.clear() + + +class MixinsAsyncProvider(MixinsProviderBase, IMixinsAsyncProvider): + + def __init__(self): + super().__init__() + 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 + _, config = get_scenario(scenario_name) + await model_class.configure(config, AsyncPostgresBackend) + backend_instance = model_class.__backend__ + if backend_instance not in self._active_async_backends: + self._active_async_backends.append(backend_instance) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception as e: + print(f"Could not drop table {table_name}: {e}") + if fn := MIXINS_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + return model_class - # Clear the list of active backends for the next test - self._active_backends.clear() \ No newline at end of file + async def setup_timestamped_post_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTimestampedPost, scenario_name, "timestamped_posts") + + async def setup_versioned_product_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncVersionedProduct, scenario_name, "versioned_products") + + async def setup_task_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncTask, scenario_name, "tasks") + + async def setup_combined_article_model(self, scenario_name: str) -> Type[ActiveRecord]: + return await self._setup_async_model(AsyncCombinedArticle, scenario_name, "combined_articles") + + async def cleanup_after_test(self, scenario_name: str): + for backend_instance in self._active_async_backends: + try: + for table_name in ['timestamped_posts', 'versioned_products', 'tasks', + 'combined_articles']: + try: + await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + await backend_instance.disconnect() + except: # noqa: E722 + pass + self._active_async_backends.clear() diff --git a/tests/providers/query.py b/tests/providers/query.py index 6fac6c84..a5482dc4 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -15,7 +15,7 @@ import logging from typing import Type, List, Tuple -from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord # Setup logging for fixture selection debugging logger = logging.getLogger(__name__) @@ -104,82 +104,73 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s MappedComment = _select_model_class(MappedCommentBase, MappedComment312, MappedComment311, MappedComment310, "MappedComment") # noqa: E501 Profile = _select_model_class(ProfileBase, Profile312, Profile311, Profile310, "Profile") -from rhosocial.activerecord.testsuite.feature.query.interfaces import IQueryProvider # noqa: E402 +from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType # noqa: E402 +from rhosocial.activerecord.testsuite.feature.query.interfaces import IQuerySyncProvider, IQueryAsyncProvider # noqa: E402 from rhosocial.activerecord.testsuite.core.protocols import WorkerTestProtocol # noqa: E402 + +# Composite-PK model classes used by setup_order_item_model. +from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ( # noqa: E402 + OrderItem as CompositeOrderItemBase, + AsyncOrderItem as AsyncCompositeOrderItemBase, +) + +# Expression-based DDL fixtures +from providers.fixtures._common import drop_table +from providers.fixtures.query import TABLE_EXPRESSIONS as QUERY_TABLE_EXPRESSIONS + # The scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario # noqa: E402 -class QueryProvider(IQueryProvider, WorkerTestProtocol): - """ - This is the postgres backend's implementation for the query features test group. - It connects the generic tests in the testsuite with the actual postgres database. - """ - +class QueryProviderBase: def __init__(self): - self._active_backends = [] - self._active_async_backends = [] + self._scenario_db_files = {} def get_test_scenarios(self) -> List[str]: - """Returns a list of names for all enabled scenarios for this backend.""" return list(get_enabled_scenarios().keys()) def _track_backend(self, backend_instance, collection): if backend_instance not in collection: collection.append(backend_instance) - def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: # noqa: E501 - """A generic helper method to handle the setup for any given model.""" - backend_class, config = get_scenario(scenario_name) - - if shared_backend is None: - model_class.configure(config, backend_class) - else: - model_class.__connection_config__ = config - model_class.__backend_class__ = backend_class - model_class.__backend__ = shared_backend - - backend_instance = model_class.__backend__ - self._track_backend(backend_instance, self._active_backends) - - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - pass - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - backend_instance.execute(schema_sql) + def _load_postgres_schema(self, filename: str) -> str: + schema_dir = os.path.join( + os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "query", "schema" + ) + schema_path = os.path.join(schema_dir, filename) + with open(schema_path, 'r', encoding='utf-8') as f: + return f.read() - return model_class - async def _setup_model_async(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: # noqa: E501 - """A generic helper method to handle the setup for any given async model.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +class QuerySyncProvider(QueryProviderBase, IQuerySyncProvider, WorkerTestProtocol): - _, config = get_scenario(scenario_name) + def __init__(self): + super().__init__() + self._active_backends = [] + def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: + backend_class, config = get_scenario(scenario_name) if shared_backend is None: - await model_class.configure(config, AsyncPostgresBackend) + model_class.configure(config, backend_class) else: model_class.__connection_config__ = config - model_class.__backend_class__ = AsyncPostgresBackend + model_class.__backend_class__ = backend_class model_class.__backend__ = shared_backend - backend_instance = model_class.__backend__ - self._track_backend(backend_instance, self._active_async_backends) - + self._track_backend(backend_instance, self._active_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) try: - await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + backend_instance.execute(sql, params, options=opts) except Exception: pass - - schema_sql = self._load_postgres_schema(f"{table_name}.sql") - await backend_instance.execute(schema_sql) - + if fn := QUERY_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) return model_class - def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """A helper to set up multiple models for fixture groups, sharing the same backend.""" + def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: result = [] shared_backend = None for i, (model_class, table_name) in enumerate(models_and_tables): @@ -187,27 +178,11 @@ def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord configured_model = self._setup_model(model_class, scenario_name, table_name) shared_backend = configured_model.__backend__ else: - configured_model = self._setup_model(model_class, scenario_name, table_name, shared_backend=shared_backend) # noqa: E501 + configured_model = self._setup_model(model_class, scenario_name, table_name, shared_backend=shared_backend) result.append(configured_model) return tuple(result) - async def _setup_multiple_models_async(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: # noqa: E501 - """A helper to set up multiple async models for fixture groups, sharing the same backend.""" - result = [] - shared_backend = None - for i, (model_class, table_name) in enumerate(models_and_tables): - if i == 0: - configured_model = await self._setup_model_async(model_class, scenario_name, table_name) - shared_backend = configured_model.__backend__ - else: - configured_model = await self._setup_model_async(model_class, scenario_name, table_name, shared_backend=shared_backend) # noqa: E501 - result.append(configured_model) - return tuple(result) - - # --- Implementation of the IQueryProvider interface --- - - def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for order-related models (User, Order, OrderItem) tests.""" + def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (Order, "orders"), @@ -215,8 +190,7 @@ def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ] return self._setup_multiple_models(models_and_tables, scenario_name) - def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for blog-related models (User, Post, Comment) tests.""" + def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (Post, "posts"), @@ -225,34 +199,24 @@ def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], T return self._setup_multiple_models(models_and_tables, scenario_name) def setup_json_user_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the JSON user model. - - Registers PostgresJSONBAdapter for JSON fields so that psycopg3's - automatic JSONB deserialization (dict/list) is converted back to str - before Pydantic validation, matching the model's Optional[str] type. - """ from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJSONBAdapter - jsonb_adapter = PostgresJSONBAdapter() json_fields = ["settings", "tags", "profile", "roles", "scores", "subscription", "preferences"] for field_name in json_fields: if field_name not in JsonUser.__field_adapters__: JsonUser.__field_adapters__[field_name] = (jsonb_adapter, str) - models_and_tables = [ (JsonUser, "json_users"), ] return self._setup_multiple_models(models_and_tables, scenario_name) def setup_tree_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the tree structure (Node) model.""" models_and_tables = [ (Node, "nodes"), ] return self._setup_multiple_models(models_and_tables, scenario_name) - def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for extended order-related models (User, ExtendedOrder, ExtendedOrderItem) tests.""" + def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (ExtendedOrder, "extended_orders"), @@ -260,8 +224,7 @@ def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[Active ] return self._setup_multiple_models(models_and_tables, scenario_name) - def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for combined models (User, Order, OrderItem, Post, Comment) tests.""" + def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: models_and_tables = [ (User, "users"), (Order, "orders"), @@ -272,40 +235,138 @@ def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord return self._setup_multiple_models(models_and_tables, scenario_name) def setup_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the SearchableItem model tests.""" from rhosocial.activerecord.testsuite.feature.query.fixtures.annotated_adapter_models import SearchableItem return self._setup_multiple_models([ (SearchableItem, "searchable_items"), ], scenario_name) - def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for MappedUser, MappedPost, and MappedComment models.""" + def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_multiple_models([ (MappedUser, "users"), (MappedPost, "posts"), (MappedComment, "comments") ], scenario_name) - def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for User and Profile models.""" + def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_multiple_models([ (User, "users"), (Profile, "profiles"), ], scenario_name) - # --- Async implementations --- + def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: + """Set up the composite-PK OrderItem model for the query feature tests.""" + return self._setup_model(CompositeOrderItemBase, scenario_name, "order_items") + + def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: + schemas = {} + if fixture_type == 'order': + tables = ['users', 'orders', 'order_items'] + elif fixture_type == 'blog': + tables = ['users', 'posts', 'comments'] + elif fixture_type == 'user': + tables = ['users'] + elif fixture_type == 'combined': + tables = ['users', 'orders', 'order_items', 'posts', 'comments'] + else: + tables = ['users'] + for table in tables: + schemas[table] = self._load_postgres_schema(f'{table}.sql') + return schemas + + def get_worker_connection_params(self, scenario_name: str, fixture_type: str = 'order') -> dict: + from .scenarios import SCENARIO_MAP + is_async = fixture_type and fixture_type.startswith('async_') + backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' + base_fixture_type = fixture_type.replace('async_', '') if fixture_type else 'order' + schema_sql = self._get_schema_sql_for_fixture_type(base_fixture_type) + if scenario_name not in SCENARIO_MAP: + if SCENARIO_MAP: + scenario_name = next(iter(SCENARIO_MAP)) + else: + raise ValueError("No scenarios registered") + config_dict = SCENARIO_MAP[scenario_name] + return { + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_class_name': backend_class_name, + 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', + 'config_class_name': 'PostgresConnectionConfig', + 'config_kwargs': config_dict, + 'schema_sql': schema_sql, + } + + def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: + return self._load_postgres_schema(f'{table_name}.sql') + + def cleanup_after_test(self, scenario_name: str): + tables_to_drop = [ + 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', + 'extended_orders', 'extended_order_items', 'searchable_items' + ] + for backend_instance in self._active_backends: + try: + for table_name in tables_to_drop: + try: + backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') + except Exception: + pass + finally: + try: + backend_instance.disconnect() + except: # noqa: E722 + pass + self._active_backends.clear() + + +class QueryAsyncProvider(QueryProviderBase, IQueryAsyncProvider): + + def __init__(self): + super().__init__() + 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 + _, config = get_scenario(scenario_name) + if shared_backend is None: + await model_class.configure(config, AsyncPostgresBackend) + else: + model_class.__connection_config__ = config + model_class.__backend_class__ = AsyncPostgresBackend + model_class.__backend__ = shared_backend + backend_instance = model_class.__backend__ + self._track_backend(backend_instance, self._active_async_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, table_name).to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception: + pass + if fn := QUERY_TABLE_EXPRESSIONS.get(table_name): + create_expr = fn(backend_instance.dialect, table_name) + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + return model_class + + async def _setup_multiple_models_async(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + result = [] + shared_backend = None + for i, (model_class, table_name) in enumerate(models_and_tables): + if i == 0: + configured_model = await self._setup_async_model(model_class, scenario_name, table_name) + shared_backend = configured_model.__backend__ + else: + configured_model = await self._setup_async_model(model_class, scenario_name, table_name, shared_backend=shared_backend) + result.append(configured_model) + return tuple(result) - async def setup_async_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async order-related models (AsyncUser, AsyncOrder, AsyncOrderItem) tests.""" - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem # noqa: E501 + async def setup_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem return await self._setup_multiple_models_async([ (AsyncUser, "users"), (AsyncOrder, "orders"), (AsyncOrderItem, "order_items") ], scenario_name) - async def setup_async_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async blog-related models (AsyncUser, AsyncPost, AsyncComment) tests.""" + async def setup_blog_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser from rhosocial.activerecord.testsuite.feature.query.fixtures.async_blog_models import AsyncPost, AsyncComment return await self._setup_multiple_models_async([ @@ -314,45 +375,34 @@ async def setup_async_blog_fixtures(self, scenario_name: str) -> Tuple[Type[Acti (AsyncComment, "comments") ], scenario_name) - async def setup_async_json_user_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the async JSON user model. - - Registers PostgresJSONBAdapter for JSON fields so that psycopg3's - automatic JSONB deserialization (dict/list) is converted back to str - before Pydantic validation, matching the model's Optional[str] type. - """ + async def setup_json_user_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_json_models import AsyncJsonUser from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJSONBAdapter - jsonb_adapter = PostgresJSONBAdapter() json_fields = ["settings", "tags", "profile", "roles", "scores", "subscription", "preferences"] for field_name in json_fields: if field_name not in AsyncJsonUser.__field_adapters__: AsyncJsonUser.__field_adapters__[field_name] = (jsonb_adapter, str) - return await self._setup_multiple_models_async([ (AsyncJsonUser, "json_users"), ], scenario_name) - async def setup_async_tree_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the async tree structure (AsyncNode) model.""" + async def setup_tree_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_cte_models import AsyncNode return await self._setup_multiple_models_async([ (AsyncNode, "nodes"), ], scenario_name) - async def setup_async_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async extended order-related models (AsyncUser, AsyncExtendedOrder, AsyncExtendedOrderItem) tests.""" # noqa: E501 - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_extended_models import AsyncUser, AsyncExtendedOrder, AsyncExtendedOrderItem # noqa: E501 + async def setup_extended_order_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_extended_models import AsyncUser, AsyncExtendedOrder, AsyncExtendedOrderItem return await self._setup_multiple_models_async([ (AsyncUser, "users"), (AsyncExtendedOrder, "extended_orders"), (AsyncExtendedOrderItem, "extended_order_items") ], scenario_name) - async def setup_async_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for async combined models (AsyncUser, AsyncOrder, AsyncOrderItem, AsyncPost, AsyncComment) tests.""" # noqa: E501 - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem # noqa: E501 + async def setup_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncOrder, AsyncOrderItem from rhosocial.activerecord.testsuite.feature.query.fixtures.async_blog_models import AsyncPost, AsyncComment return await self._setup_multiple_models_async([ (AsyncUser, "users"), @@ -362,32 +412,32 @@ async def setup_async_combined_fixtures(self, scenario_name: str) -> Tuple[Type[ (AsyncComment, "comments") ], scenario_name) - async def setup_async_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: - """Sets up the database for the async SearchableItem model tests.""" - from rhosocial.activerecord.testsuite.feature.query.fixtures.async_annotated_adapter_models import AsyncSearchableItem # noqa: E501 + async def setup_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_annotated_adapter_models import AsyncSearchableItem return await self._setup_multiple_models_async([ (AsyncSearchableItem, "searchable_items"), ], scenario_name) - async def setup_async_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for AsyncMappedUser, AsyncMappedPost, and AsyncMappedComment models.""" - from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import AsyncMappedUser, AsyncMappedPost, AsyncMappedComment # noqa: E501 + async def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import AsyncMappedUser, AsyncMappedPost, AsyncMappedComment return await self._setup_multiple_models_async([ (AsyncMappedUser, "users"), (AsyncMappedPost, "posts"), (AsyncMappedComment, "comments") ], scenario_name) - async def setup_async_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: # noqa: E501 - """Sets up the database for AsyncUser and AsyncProfile models.""" + async def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import AsyncUser, AsyncProfile return await self._setup_multiple_models_async([ (AsyncUser, "users"), (AsyncProfile, "profiles"), ], scenario_name) - async def cleanup_after_test_async(self, scenario_name: str): - """Performs async cleanup after a test.""" + async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: + """Set up the composite-PK AsyncOrderItem model for the query feature tests.""" + return await self._setup_async_model(AsyncCompositeOrderItemBase, scenario_name, "order_items") + + async def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', 'extended_orders', 'extended_order_items', 'searchable_items' @@ -405,130 +455,3 @@ async def cleanup_after_test_async(self, scenario_name: str): except: # noqa: E722 pass self._active_async_backends.clear() - - def _load_postgres_schema(self, filename: str) -> str: - """Helper to load a SQL schema file from this project's fixtures.""" - # Schemas are stored in the centralized location for query feature. - schema_dir = os.path.join(os.path.dirname(__file__), "..", "rhosocial", "activerecord_postgres_test", "feature", "query", "schema") # noqa: E501 - schema_path = os.path.join(schema_dir, filename) - - with open(schema_path, 'r', encoding='utf-8') as f: - return f.read() - - def cleanup_after_test(self, scenario_name: str): - """ - Performs cleanup after a test. This uses the same backend instance - that was used for setup to ensure proper cleanup with foreign key constraints. - """ - tables_to_drop = [ - 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items' - ] - for backend_instance in self._active_backends: - try: - # Drop all tables that might have been created for query tests - # Use CASCADE to handle foreign key dependencies - for table_name in tables_to_drop: - try: - backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') - except Exception: - # Continue with other tables if one fails - pass - finally: - # Always disconnect the backend instance that was used in the test - try: - backend_instance.disconnect() - except: # noqa: E722 - # Ignore errors during disconnect - pass - - # Clear the list of active backends for the next test - self._active_backends.clear() - - # --- Implementation of WorkerTestProtocol --- - - def get_worker_connection_params(self, scenario_name: str, fixture_type: str = 'order') -> dict: - """ - Return serializable connection parameters for Worker processes. - - This method provides all information needed to recreate the database - connection in a Worker process, including the schema SQL for table creation. - - Args: - scenario_name: The test scenario name - fixture_type: Type of fixture ('order', 'blog', 'user', 'combined', - or with 'async_' prefix for async backends) - - Returns: - Dictionary with connection parameters and schema SQL - """ - from .scenarios import SCENARIO_MAP - - # Determine if async backend is needed based on fixture_type - is_async = fixture_type and fixture_type.startswith('async_') - backend_class_name = 'AsyncPostgresBackend' if is_async else 'PostgresBackend' - - # Get base fixture type (remove 'async_' prefix if present) - base_fixture_type = fixture_type.replace('async_', '') if fixture_type else 'order' - - # Build schema SQL based on fixture type - schema_sql = self._get_schema_sql_for_fixture_type(base_fixture_type) - - # Get connection config from scenario - if scenario_name not in SCENARIO_MAP: - if SCENARIO_MAP: - scenario_name = next(iter(SCENARIO_MAP)) - else: - raise ValueError("No scenarios registered") - - config_dict = SCENARIO_MAP[scenario_name] - - return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', - 'backend_class_name': backend_class_name, - 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', - 'config_class_name': 'PostgresConnectionConfig', - 'config_kwargs': config_dict, - 'schema_sql': schema_sql, - } - - def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: - """ - Return the SQL statement to create a specific table. - - Args: - scenario_name: The test scenario name (unused for Postgres as schema is fixed) - table_name: Name of the table to create - - Returns: - CREATE TABLE SQL statement - """ - return self._load_postgres_schema(f'{table_name}.sql') - - def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: - """ - Get schema SQL for a specific fixture type. - - Args: - fixture_type: Type of fixture ('order', 'blog', 'user', 'combined') - - Returns: - Dictionary mapping table names to CREATE TABLE statements - """ - schemas = {} - - if fixture_type == 'order': - tables = ['users', 'orders', 'order_items'] - elif fixture_type == 'blog': - tables = ['users', 'posts', 'comments'] - elif fixture_type == 'user': - tables = ['users'] - elif fixture_type == 'combined': - tables = ['users', 'orders', 'order_items', 'posts', 'comments'] - else: - tables = ['users'] - - for table in tables: - schemas[table] = self._load_postgres_schema(f'{table}.sql') - - return schemas diff --git a/tests/providers/registry.py b/tests/providers/registry.py index 34984fff..32738870 100644 --- a/tests/providers/registry.py +++ b/tests/providers/registry.py @@ -6,14 +6,13 @@ interfaces for the postgres backend. """ from rhosocial.activerecord.testsuite.core.registry import ProviderRegistry -from .basic import BasicProvider -from .events import EventsProvider -from .mixins import MixinsProvider -from .query import QueryProvider -from .relation import RelationProvider +from .basic import BasicSyncProvider, BasicAsyncProvider +from .events import EventsSyncProvider, EventsAsyncProvider +from .mixins import MixinsSyncProvider, MixinsAsyncProvider +from .query import QuerySyncProvider, QueryAsyncProvider +from .relation import RelationSyncProvider, RelationAsyncProvider from .basic_connection import BasicConnectionProvider from .query_connection import QueryConnectionProvider -from .composite_pk import CompositePKProvider from .crud_benchmark import CrudBenchmarkProvider from .fastapi_benchmark import FastAPIBenchmarkProvider from .mixin_benchmark import MixinBenchmarkProvider @@ -25,11 +24,25 @@ # Register the concrete implementations as the providers for the # interfaces defined in the testsuite. -provider_registry.register("feature.basic.IBasicProvider", BasicProvider) -provider_registry.register("feature.events.IEventsProvider", EventsProvider) -provider_registry.register("feature.mixins.IMixinsProvider", MixinsProvider) -provider_registry.register("feature.query.IQueryProvider", QueryProvider) -provider_registry.register("feature.relation.IRelationProvider", RelationProvider) +provider_registry.register("feature.basic.IBasicProvider", BasicSyncProvider) +provider_registry.register("feature.basic.IBasicSyncProvider", BasicSyncProvider) +provider_registry.register("feature.basic.IBasicAsyncProvider", BasicAsyncProvider) + +provider_registry.register("feature.events.IEventsProvider", EventsSyncProvider) +provider_registry.register("feature.events.IEventsSyncProvider", EventsSyncProvider) +provider_registry.register("feature.events.IEventsAsyncProvider", EventsAsyncProvider) + +provider_registry.register("feature.mixins.IMixinsProvider", MixinsSyncProvider) +provider_registry.register("feature.mixins.IMixinsSyncProvider", MixinsSyncProvider) +provider_registry.register("feature.mixins.IMixinsAsyncProvider", MixinsAsyncProvider) + +provider_registry.register("feature.query.IQueryProvider", QuerySyncProvider) +provider_registry.register("feature.query.IQuerySyncProvider", QuerySyncProvider) +provider_registry.register("feature.query.IQueryAsyncProvider", QueryAsyncProvider) + +provider_registry.register("feature.relation.IRelationProvider", RelationSyncProvider) +provider_registry.register("feature.relation.IRelationSyncProvider", RelationSyncProvider) +provider_registry.register("feature.relation.IRelationAsyncProvider", RelationAsyncProvider) # Register connection pool context awareness providers provider_registry.register( @@ -41,8 +54,6 @@ QueryConnectionProvider, ) -provider_registry.register("feature.composite_pk.ICompositePKProvider", CompositePKProvider) - # Register benchmark providers. provider_registry.register("benchmark.crud.ICrudBenchmarkProvider", CrudBenchmarkProvider) provider_registry.register("benchmark.query.IQueryBenchmarkProvider", QueryBenchmarkProvider) diff --git a/tests/providers/relation.py b/tests/providers/relation.py index c852ad55..6a46fb31 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -5,7 +5,7 @@ from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJSONBAdapter -from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationProvider +from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationSyncProvider, IRelationAsyncProvider from rhosocial.activerecord.testsuite.feature.relation.fixtures.models import ( Employee, Department, Author, Book, Chapter, Profile, User, Post, Comment, @@ -104,11 +104,8 @@ """ -class RelationProvider(IRelationProvider): - +class RelationProviderBase: def __init__(self): - self._active_backends = [] - self._active_async_backends = [] self._sync_user_post_comment_setup = False self._async_user_post_comment_setup = False self._sync_relation_boundary_setup = False @@ -117,31 +114,11 @@ def __init__(self): def get_test_scenarios(self) -> List[str]: return list(get_enabled_scenarios().keys()) - def _execute_script(self, backend, sql: str): - for statement in sql.split(";"): - statement = statement.strip() - if statement: - backend.execute(statement) - - async def _execute_script_async(self, backend, sql: str): - for statement in sql.split(";"): - statement = statement.strip() - if statement: - await backend.execute(statement) - def _configure_with_shared_backend(self, model_class, config, backend_class, backend): model_class.__connection_config__ = config model_class.__backend_class__ = backend_class model_class.__backend__ = backend - def _configure_async_model_without_connection(self, model_class, config, backend=None): - if backend is None: - backend = AsyncPostgresBackend(connection_config=config) - model_class.__connection_config__ = config - model_class.__backend_class__ = AsyncPostgresBackend - model_class.__backend__ = backend - return backend - def _configure_json_field_adapters(self): adapter = PostgresJSONBAdapter() User.__field_adapters__["settings"] = (adapter, str) @@ -151,6 +128,25 @@ def _configure_json_field_adapters(self): AsyncPost.__field_adapters__["metadata"] = (adapter, str) AsyncComment.__field_adapters__["meta"] = (adapter, str) + def _reset_setup_state(self): + self._sync_user_post_comment_setup = False + self._async_user_post_comment_setup = False + self._sync_relation_boundary_setup = False + self._async_relation_boundary_setup = False + + +class RelationSyncProvider(RelationProviderBase, IRelationSyncProvider): + + def __init__(self): + super().__init__() + self._active_backends = [] + + def _execute_script(self, backend, sql: str): + for statement in sql.split(";"): + statement = statement.strip() + if statement: + backend.execute(statement) + def _setup_employee_department(self, scenario_name): backend_class, config = get_scenario(scenario_name) Employee.configure(config, backend_class) @@ -189,23 +185,6 @@ def _setup_user_post_comment_sync(self, scenario_name): self._configure_with_shared_backend(Comment, config, backend_class, backend) self._sync_user_post_comment_setup = True - def _setup_user_post_comment_async(self, scenario_name): - if not self._async_user_post_comment_setup: - self._configure_json_field_adapters() - _, config = get_scenario(scenario_name) - backend = self._configure_async_model_without_connection(AsyncUser, config) - self._configure_async_model_without_connection(AsyncPost, config, backend) - self._configure_async_model_without_connection(AsyncComment, config, backend) - self._active_async_backends.append(backend) - self._async_user_post_comment_setup = True - - async def _ensure_user_post_comment_async_schema(self): - backend = AsyncUser.backend() - if backend not in self._active_async_backends: - self._active_async_backends.append(backend) - await backend.introspect_and_adapt() - await self._execute_script_async(backend, USER_POST_COMMENT_SCHEMA) - def _setup_relation_boundary_sync(self, scenario_name): if not self._sync_relation_boundary_setup: backend_class, config = get_scenario(scenario_name) @@ -216,41 +195,13 @@ def _setup_relation_boundary_sync(self, scenario_name): self._active_backends.append(backend) self._execute_script(backend, RELATION_BOUNDARY_SCHEMA) self._configure_with_shared_backend( - BoundaryProfile, - config, - backend_class, - backend, + BoundaryProfile, config, backend_class, backend, ) self._configure_with_shared_backend( - BoundaryPost, - config, - backend_class, - backend, + BoundaryPost, config, backend_class, backend, ) self._sync_relation_boundary_setup = True - async def _setup_relation_boundary_async(self, scenario_name): - if not self._async_relation_boundary_setup: - _, config = get_scenario(scenario_name) - backend = self._configure_async_model_without_connection( - AsyncBoundaryOwner, - config, - ) - self._configure_async_model_without_connection( - AsyncBoundaryProfile, - config, - backend, - ) - self._configure_async_model_without_connection( - AsyncBoundaryPost, - config, - backend, - ) - self._active_async_backends.append(backend) - await backend.introspect_and_adapt() - await self._execute_script_async(backend, RELATION_BOUNDARY_SCHEMA) - self._async_relation_boundary_setup = True - def setup_employee_department_fixtures( self, scenario_name: str, @@ -260,12 +211,7 @@ def setup_employee_department_fixtures( def setup_author_book_fixtures( self, scenario_name: str, - ) -> Tuple[ - Type[ActiveRecord], - Type[ActiveRecord], - Type[ActiveRecord], - Type[ActiveRecord], - ]: + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: return self._setup_author_book(scenario_name) def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: @@ -280,18 +226,6 @@ def setup_comment_model(self, scenario_name: str) -> Type[ActiveRecord]: self._setup_user_post_comment_sync(scenario_name) return Comment - def setup_async_user_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: - self._setup_user_post_comment_async(scenario_name) - return AsyncUser - - def setup_async_post_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: - self._setup_user_post_comment_async(scenario_name) - return AsyncPost - - def setup_async_comment_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: - self._setup_user_post_comment_async(scenario_name) - return AsyncComment - def setup_relation_boundary_fixtures( self, scenario_name: str, @@ -299,40 +233,10 @@ def setup_relation_boundary_fixtures( self._setup_relation_boundary_sync(scenario_name) return BoundaryOwner, BoundaryProfile, BoundaryPost - def setup_async_relation_boundary_fixtures( - self, - scenario_name: str, - ) -> Tuple[ - Type[AsyncActiveRecord], - Type[AsyncActiveRecord], - Type[AsyncActiveRecord], - ]: - _, config = get_scenario(scenario_name) - self._configure_async_model_without_connection(AsyncBoundaryOwner, config) - self._configure_async_model_without_connection( - AsyncBoundaryProfile, - config, - AsyncBoundaryOwner.backend(), - ) - self._configure_async_model_without_connection( - AsyncBoundaryPost, - config, - AsyncBoundaryOwner.backend(), - ) - return AsyncBoundaryOwner, AsyncBoundaryProfile, AsyncBoundaryPost - def load_relation_boundary_dataset(self, scenario_name: str, dataset_name: str) -> Dict[str, int]: self._setup_relation_boundary_sync(scenario_name) return self._load_relation_boundary_dataset(dataset_name) - async def load_async_relation_boundary_dataset( - self, - scenario_name: str, - dataset_name: str, - ) -> Dict[str, int]: - await self._setup_relation_boundary_async(scenario_name) - return await self._load_async_relation_boundary_dataset(dataset_name) - def _load_relation_boundary_dataset(self, dataset_name): if dataset_name == "null_foreign_key": profile = BoundaryProfile(bio="No owner", owner_id=None) @@ -365,6 +269,128 @@ def _load_relation_boundary_dataset(self, dataset_name): raise ValueError(f"Unknown relation boundary dataset: {dataset_name}") + def cleanup_after_test(self, scenario_name: str) -> None: + for backend in self._active_backends: + try: + backend.disconnect() + except Exception: + pass + self._active_backends.clear() + self._reset_setup_state() + + +class RelationAsyncProvider(RelationProviderBase, IRelationAsyncProvider): + + def __init__(self): + super().__init__() + self._active_async_backends = [] + + def _configure_async_model_without_connection(self, model_class, config, backend=None): + if backend is None: + backend = AsyncPostgresBackend(connection_config=config) + model_class.__connection_config__ = config + model_class.__backend_class__ = AsyncPostgresBackend + model_class.__backend__ = backend + return backend + + async def _execute_script_async(self, backend, sql: str): + for statement in sql.split(";"): + statement = statement.strip() + if statement: + await backend.execute(statement) + + async def _setup_employee_department_fixtures_async(self, scenario_name): + _, config = get_scenario(scenario_name) + await Employee.configure(config, AsyncPostgresBackend) + backend = Employee.backend() + await backend.connect() + await backend.introspect_and_adapt() + self._active_async_backends.append(backend) + await self._execute_script_async(backend, EMPLOYEE_DEPARTMENT_SCHEMA) + self._configure_with_shared_backend(Department, config, AsyncPostgresBackend, backend) + return Employee, Department + + async def _setup_author_book_fixtures_async(self, scenario_name): + _, config = get_scenario(scenario_name) + await Author.configure(config, AsyncPostgresBackend) + backend = Author.backend() + await backend.connect() + await backend.introspect_and_adapt() + self._active_async_backends.append(backend) + await self._execute_script_async(backend, AUTHOR_BOOK_SCHEMA) + self._configure_with_shared_backend(Book, config, AsyncPostgresBackend, backend) + self._configure_with_shared_backend(Chapter, config, AsyncPostgresBackend, backend) + self._configure_with_shared_backend(Profile, config, AsyncPostgresBackend, backend) + return Author, Book, Chapter, Profile + + async def _setup_user_post_comment_async(self, scenario_name): + if not self._async_user_post_comment_setup: + self._configure_json_field_adapters() + _, config = get_scenario(scenario_name) + backend = self._configure_async_model_without_connection(AsyncUser, config) + self._configure_async_model_without_connection(AsyncPost, config, backend) + self._configure_async_model_without_connection(AsyncComment, config, backend) + self._active_async_backends.append(backend) + await backend.introspect_and_adapt() + await self._execute_script_async(backend, USER_POST_COMMENT_SCHEMA) + self._async_user_post_comment_setup = True + + async def _setup_relation_boundary_async(self, scenario_name): + if not self._async_relation_boundary_setup: + _, config = get_scenario(scenario_name) + backend = self._configure_async_model_without_connection( + AsyncBoundaryOwner, config, + ) + self._configure_async_model_without_connection( + AsyncBoundaryProfile, config, backend, + ) + self._configure_async_model_without_connection( + AsyncBoundaryPost, config, backend, + ) + self._active_async_backends.append(backend) + await backend.introspect_and_adapt() + await self._execute_script_async(backend, RELATION_BOUNDARY_SCHEMA) + self._async_relation_boundary_setup = True + + async def setup_employee_department_fixtures( + self, + scenario_name: str, + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord]]: + return await self._setup_employee_department_fixtures_async(scenario_name) + + async def setup_author_book_fixtures( + self, + scenario_name: str, + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + return await self._setup_author_book_fixtures_async(scenario_name) + + async def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: + await self._setup_user_post_comment_async(scenario_name) + return AsyncUser + + async def setup_post_model(self, scenario_name: str) -> Type[ActiveRecord]: + await self._setup_user_post_comment_async(scenario_name) + return AsyncPost + + async def setup_comment_model(self, scenario_name: str) -> Type[ActiveRecord]: + await self._setup_user_post_comment_async(scenario_name) + return AsyncComment + + async def setup_relation_boundary_fixtures( + self, + scenario_name: str, + ) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + await self._setup_relation_boundary_async(scenario_name) + return AsyncBoundaryOwner, AsyncBoundaryProfile, AsyncBoundaryPost + + async def load_relation_boundary_dataset( + self, + scenario_name: str, + dataset_name: str, + ) -> Dict[str, int]: + await self._setup_relation_boundary_async(scenario_name) + return await self._load_async_relation_boundary_dataset(dataset_name) + async def _load_async_relation_boundary_dataset(self, dataset_name): if dataset_name == "null_foreign_key": profile = AsyncBoundaryProfile(bio="No owner", owner_id=None) @@ -397,34 +423,7 @@ async def _load_async_relation_boundary_dataset(self, dataset_name): raise ValueError(f"Unknown relation boundary dataset: {dataset_name}") - def _reset_setup_state(self): - self._sync_user_post_comment_setup = False - self._async_user_post_comment_setup = False - self._sync_relation_boundary_setup = False - self._async_relation_boundary_setup = False - - def cleanup_after_test(self, scenario_name: str) -> None: - for backend in self._active_backends: - try: - backend.disconnect() - except Exception: - pass - self._active_backends.clear() - for backend in self._active_async_backends: - try: - asyncio.run(backend.disconnect()) - except Exception: - pass - self._active_async_backends.clear() - self._reset_setup_state() - - async def cleanup_after_test_async(self, scenario_name: str): - for backend in self._active_backends: - try: - backend.disconnect() - except Exception: - pass - self._active_backends.clear() + async def cleanup_after_test(self, scenario_name: str): for backend in self._active_async_backends: try: await backend.disconnect() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py index 4cd150ba..6601e6ad 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py @@ -15,6 +15,6 @@ from rhosocial.activerecord.testsuite.feature.basic.conftest import mapped_models_fixtures # noqa: F401 # The `TestBasicMappedModels` class contains the actual test cases. -from rhosocial.activerecord.testsuite.feature.basic.test_example_basic_fixtures import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_example_basic_fixtures_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py index bdacfa62..8e76ce45 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py @@ -8,6 +8,6 @@ async_bulk_user_class, # noqa: F401 ) -from rhosocial.activerecord.testsuite.feature.basic.test_bulk_operations import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_bulk_operations_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.bulk_crud.test_bulk_operations import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.bulk_crud.test_bulk_operations_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py index 474f2c71..ffc5439b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py @@ -27,6 +27,6 @@ ) # Import shared tests from testsuite package -from rhosocial.activerecord.testsuite.feature.basic.test_crud import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_crud_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.crud.test_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.crud.test_crud_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py index 7940c241..87b920c3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py @@ -20,6 +20,6 @@ ) # The test classes contain the actual test cases. -from rhosocial.activerecord.testsuite.feature.basic.test_field_column_mapping import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_field_column_mapping_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_field_column_mapping import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_field_column_mapping_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py index a409bbbf..13b13fb8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py @@ -20,6 +20,6 @@ ) # Import shared tests from testsuite package -from rhosocial.activerecord.testsuite.feature.basic.test_fields import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_fields_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_fields import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_fields_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py index 1a0848e5..03fc5eeb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py @@ -13,6 +13,6 @@ pydantic_validated_model, # noqa: F401 ) -from rhosocial.activerecord.testsuite.feature.basic.test_pydantic_native_validation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_pydantic_native_validation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_pydantic_native_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_pydantic_native_validation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py index 9e30dff5..c607a556 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py @@ -13,6 +13,6 @@ ) # Import all tests from the generic testsuite file. -from rhosocial.activerecord.testsuite.feature.basic.test_type_adapter import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_type_adapter_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py index 54666c6b..2c2cd094 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py @@ -20,6 +20,6 @@ ) # Import shared tests from testsuite package -from rhosocial.activerecord.testsuite.feature.basic.test_validation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.test_validation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.validation.test_validation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py deleted file mode 100644 index 4d6a5f53..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py +++ /dev/null @@ -1,2 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/conftest.py -from rhosocial.activerecord.testsuite.feature.composite_pk.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql deleted file mode 100644 index 9ea4998a..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/order_items.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE "order_items" ( - "order_id" INTEGER NOT NULL, - "product_id" INTEGER NOT NULL, - "quantity" INTEGER NOT NULL DEFAULT 1, - "unit_price" DECIMAL(10,2) NOT NULL, - PRIMARY KEY ("order_id", "product_id") -); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql deleted file mode 100644 index b96c78fa..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/orders.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE "orders" ( - "id" SERIAL PRIMARY KEY, - "total" DECIMAL(10,2) NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE, - "updated_at" TIMESTAMP WITH TIME ZONE -); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql deleted file mode 100644 index e4bfbab9..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/schema/store_inventory.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE "store_inventory" ( - "store_id" INTEGER NOT NULL, - "product_id" INTEGER NOT NULL, - "batch_id" VARCHAR(64) NOT NULL, - "stock" INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY ("store_id", "product_id", "batch_id") -); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py deleted file mode 100644 index 6ae36196..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py +++ /dev/null @@ -1,4 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_active_query.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_active_query import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.composite_pk.test_active_query_async import * # noqa: F403 - diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py deleted file mode 100644 index 08112386..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py +++ /dev/null @@ -1,4 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_activerecord.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_activerecord import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.composite_pk.test_activerecord_async import * # noqa: F403 - diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py deleted file mode 100644 index 57d58ab8..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py +++ /dev/null @@ -1,4 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_cte_query.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_cte_query import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.composite_pk.test_cte_query_async import * # noqa: F403 - diff --git a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py deleted file mode 100644 index b485fde0..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py +++ /dev/null @@ -1,4 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/composite_pk/test_composite_pk_set_operation.py -from rhosocial.activerecord.testsuite.feature.composite_pk.test_set_operation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.composite_pk.test_set_operation_async import * # noqa: F403 - diff --git a/tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py deleted file mode 100644 index 8921aa03..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/derived_field/conftest.py -""" -Pytest configuration for derived field feature tests. - -This file imports fixtures from the corresponding testsuite, making them -available to the tests in this directory. -""" - -from rhosocial.activerecord.testsuite.feature.derived_field.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py b/tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py deleted file mode 100644 index 19981432..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py +++ /dev/null @@ -1,16 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/derived_field/test_derived_field_testsuite.py -""" -Bridge file for derived field feature tests from the testsuite. -""" - -from rhosocial.activerecord.testsuite.feature.derived_field.conftest import ( # noqa: F401 - product_class, - product_form_a_class, - product_with_proxy_class, - product_with_column_and_adapter_class, - async_product_class, - async_product_with_proxy_class, - async_product_with_column_and_adapter_class, -) -from rhosocial.activerecord.testsuite.feature.derived_field.test_derived_field import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.derived_field.test_derived_field_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py index 4490f75c..deae3685 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_aggregate import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_active_query_aggregate_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_active_query_aggregate import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_active_query_aggregate_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py index b48c30f4..9baba06b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_basic import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_active_query_basic_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_active_query_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_active_query_basic_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py index ea6a7e26..90eb3a08 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_join import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_active_query_join_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_active_query_join import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_active_query_join_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py index 14fe4369..8498bb08 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_range import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_active_query_range_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_active_query_range import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_active_query_range_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py index 5df3b6c5..d3981b53 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_active_query_set_operation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_active_query_set_operation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_active_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_active_query_set_operation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py index 7f0d8321..525ba49a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_aggregate_queries import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_aggregate_queries_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_aggregate_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_aggregate_queries_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py index 809e6cdf..e2f69cef 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_basic import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_basic_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_basic_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py index 8d5fb7dd..b234b681 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_cross_database_compatibility import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_cross_database_compatibility_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cross_database.test_cross_database_compatibility import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cross_database.test_cross_database_compatibility_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py index 401c9b16..6298688f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_cte_query_active_query import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_cte_query_active_query_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_active_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_active_query_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py index 2acfc13c..686d8fc6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_cte_query_set_operation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_cte_query_set_operation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_set_operation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py index a4e230f6..a7c1e676 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py @@ -2,6 +2,6 @@ """ Bridge file for eager loading caching tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_caching import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_caching_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_caching import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_caching_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py index e6430701..09efb41a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py @@ -2,6 +2,6 @@ """ Bridge file for eager loading nested tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_nested import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_nested_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_nested import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_nested_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py index 37b45c60..64cae900 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py @@ -2,4 +2,4 @@ """ Bridge file for async eager loading scenario tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios_async import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py index 414db2d8..53b80607 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py @@ -2,6 +2,6 @@ """ Bridge file for sync eager loading scenario tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_scenarios_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py index 963fbdf0..7571dafa 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py @@ -2,6 +2,6 @@ """ Bridge file for eager loading with_all tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_all import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_all_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_all import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_all_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py index 1df4dacb..c55cb6f8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py @@ -2,6 +2,6 @@ """ Bridge file for eager loading with_modifier tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_modifier import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_modifier_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_modifier import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_modifier_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py index 96fe7a48..8dc3de43 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py @@ -2,6 +2,6 @@ """ Bridge file for eager loading with_one tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_one import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.test_eager_loading_with_one_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_one import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_one_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py index b92024f3..88f48f2c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_error_handling import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_error_handling_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.error_handling.test_error_handling import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.error_handling.test_error_handling_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py index 15af9f04..162cb400 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_example_query_fixtures import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_example_query_fixtures_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_example_query_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_example_query_fixtures_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py index ad9af333..a775027d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_joins import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_joins_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_joins import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.joins.test_joins_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py index 5a630984..5087c2ed 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_logging_data_summarization import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_logging_data_summarization_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.logging.test_logging_data_summarization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.logging.test_logging_data_summarization_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py index b641f1f3..1fda7a9b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py @@ -5,4 +5,4 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_queries import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py index bd9acb01..8e53f6fe 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py @@ -5,5 +5,5 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_queries_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_queries_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py index 2c250fd1..115d70f0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_query_optimization import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_query_optimization_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.optimization.test_query_optimization import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.optimization.test_query_optimization_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py index bc33ff4c..fb8bc85e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_range_queries import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_range_queries_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_queries_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py index 28c994f8..ef05c44b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_range_query_detailed import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_range_query_detailed_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_query_detailed import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_query_detailed_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py index f4766f1f..abd6a3f4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_relational_validation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_relational_validation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relational_validation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relational_validation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py index 272293f6..7a2a089e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_basic import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_relations_basic_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_basic import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_basic_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py index 854df473..8057becf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py @@ -2,4 +2,4 @@ """ Bridge file for async relation 'with' tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_with_async import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_with_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py index 5ff8e35c..8e7d2338 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py @@ -2,4 +2,4 @@ """ Bridge file for sync relation 'with' tests. """ -from rhosocial.activerecord.testsuite.feature.query.test_relations_with import * # noqa: F401, F403 +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_with import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py index ea4cfc60..a23ca1b3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_set_operation import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_set_operation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_set_operation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py index 774bf73c..eb4efe6f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_special_queries import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_special_queries_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_special_queries import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.special.test_special_queries_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py index a665981a..54ee5de3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py @@ -5,6 +5,6 @@ This file imports the generic tests from the testsuite package and makes them discoverable by pytest in this project's test run. """ -from rhosocial.activerecord.testsuite.feature.query.test_window_functions import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.query.test_window_functions_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.window_functions.test_window_functions import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.window_functions.test_window_functions_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py index cb3209b8..c2882883 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py @@ -28,7 +28,7 @@ async def async_comment_class(async_user_post_comment_classes): return comment -@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS) # noqa: F405 +@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS_ASYNC) # noqa: F405 async def async_user_post_comment_classes(request): from rhosocial.activerecord.testsuite.core.registry import get_provider_registry @@ -44,7 +44,7 @@ async def async_user_post_comment_classes(request): await provider.cleanup_after_test_async(scenario) -@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS) # noqa: F405 +@pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS_ASYNC) # noqa: F405 async def async_relation_boundary_context(request): from rhosocial.activerecord.testsuite.core.registry import get_provider_registry diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py index 2b7e6eb2..1fb8802b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py @@ -2,4 +2,4 @@ Tests for relation base functionality. Imports tests from the standardized testsuite. """ -from rhosocial.activerecord.testsuite.feature.relation.test_base import TestRelationDescriptor # noqa: F401 +from rhosocial.activerecord.testsuite.feature.relation.base.test_base import TestRelationDescriptor # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py index d27862a6..5259aa4a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_base_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.base.test_base_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py index 4c51f85c..0efbbf89 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_boundary import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_boundary_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.boundary.test_boundary import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.boundary.test_boundary_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py index 95cbd862..591af194 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_boundary_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.boundary.test_boundary_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py index ccd3017a..7e97b7cd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py @@ -7,7 +7,7 @@ # Import the test classes from the testsuite -from rhosocial.activerecord.testsuite.feature.relation.test_cache import TestRelationCache # noqa: F401 +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache import TestRelationCache # noqa: F401 # This will cause pytest to run all the tests in the imported class diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py index d0fb6c14..33f8eccb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_cache_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py index 5ec530a6..ae990e84 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_cache_clearing import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_cache_clearing_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_clearing import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_clearing_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py index 48265b0d..b67892d2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py index af5beacd..417703a6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py index 8c84a87f..21d13321 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_json import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_json_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py index 0130588f..867655b5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_derived_field_json_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_json_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py index 203c1732..09df177d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py @@ -1,4 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py -from rhosocial.activerecord.testsuite.feature.relation.test_descriptor_compat import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_descriptor_compat_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptor_compat import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptor_compat_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py index 8f5b34e5..9622ce84 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py @@ -2,4 +2,4 @@ Tests for relation descriptor functionality. Imports tests from the standardized testsuite. """ -from rhosocial.activerecord.testsuite.feature.relation.test_descriptors import TestRelationDescriptors # noqa: F401 +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptors import TestRelationDescriptors # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py index 8fa56f6d..b10f32f3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py @@ -1,3 +1,3 @@ # tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py -from rhosocial.activerecord.testsuite.feature.relation.test_descriptors_async import * # noqa: F401,F403 \ No newline at end of file +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptors_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py index de72e480..eb3e57d4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_integration import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_integration_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.integration.test_integration import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.integration.test_integration_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py index 5bc443dc..ef9b2e02 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_integration_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.integration.test_integration_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py index 2e1fab2c..dce937f9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py @@ -2,4 +2,4 @@ Tests for relation interfaces. Imports tests from the standardized testsuite. """ -from rhosocial.activerecord.testsuite.feature.relation.test_interfaces import TestRelationInterfaces # noqa: F401 +from rhosocial.activerecord.testsuite.feature.relation.base.test_interfaces import TestRelationInterfaces # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py index 2ae8e7db..688ea916 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_interfaces_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.base.test_interfaces_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py index 196d1585..8fc621f3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_behavior import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_behavior_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py index 6538d2b1..797fa5e3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_behavior_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_behavior_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py index 1bdf5e5d..2b7b7bc8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_warning import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_warning_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py index b7207a9f..6e884253 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_modifier_warning_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.modifiers.test_modifier_warning_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py index edb06654..f9447d3a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py @@ -2,4 +2,4 @@ Tests for nested relationship access functionality. Imports tests from the standardized testsuite. """ -from rhosocial.activerecord.testsuite.feature.relation.test_nested_relationship_access import TestNestedRelationshipAccess # noqa: E501, F401 +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_nested_relationship_access import TestNestedRelationshipAccess # noqa: E501, F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py index 272901e9..6ea7cbbe 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_nested_relationship_access_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_nested_relationship_access_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py index 5e35543f..b2b0a150 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_path_validation import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_path_validation_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_path_validation import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_path_validation_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py index 599860af..b492478f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_path_validation_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_path_validation_async import * # noqa: F401,F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py index c5ef8611..47df6d4e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py @@ -1,4 +1,4 @@ # tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py -from rhosocial.activerecord.testsuite.feature.relation.test_validator import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_validator_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_validator import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.validation.test_validator_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py index 0b998f59..a1ce8686 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_with_method import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.test_with_method_async import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_with_method import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_with_method_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py index de09d14a..a2c28563 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py @@ -1 +1 @@ -from rhosocial.activerecord.testsuite.feature.relation.test_with_method_async import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.relation.eager_loading.test_with_method_async import * # noqa: F401,F403 From 96f7aaaa2e85e197c14bec0cf008dde286fbca40 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 16 Jul 2026 16:55:53 +0800 Subject: [PATCH 15/39] fix(postgres): move bridge files to subdirectories, add missing conftest, fix composite_pk setup - Move 70 bridge files from flat paths to testsuite-matching subdirectories - Create 6 missing bridge files (composite_pk crud/query/cte/set_operations, derived_field_basic, example_basic_fixtures) - Add missing basic/worker, query/worker, interface conftest bridge files - Rename test_worker_transaction_isolation -> test_transaction_isolation - Fix setup_order_item_model to use basic TABLE_EXPRESSIONS for composite PK order_items table --- tests/providers/query.py | 41 ++++++++++++++++++- .../{ => bulk_crud}/test_bulk_operations.py | 0 .../basic/crud/test_composite_pk_crud.py | 6 +++ .../feature/basic/{ => crud}/test_crud.py | 0 .../basic/fields/test_derived_field_basic.py | 6 +++ .../fields/test_example_basic_fixtures.py | 6 +++ .../{ => fields}/test_field_column_mapping.py | 0 .../feature/basic/{ => fields}/test_fields.py | 0 .../{ => type_adapter}/test_type_adapter.py | 0 .../test_pydantic_native_validation.py | 0 .../basic/{ => validation}/test_validation.py | 0 .../feature/basic/worker/conftest.py | 8 ++++ .../feature/interface/conftest.py | 8 ++++ .../test_active_query_aggregate.py | 0 .../{ => aggregate}/test_aggregate_queries.py | 0 .../{ => basic}/test_active_query_basic.py | 0 .../feature/query/{ => basic}/test_basic.py | 0 .../query/basic/test_composite_pk_query.py | 6 +++ .../feature/query/{ => basic}/test_queries.py | 0 .../query/{ => basic}/test_queries_async.py | 0 .../test_collation_expression.py | 0 .../test_cross_database_compatibility.py | 0 .../query/cte/test_composite_pk_cte.py | 6 +++ .../{ => cte}/test_cte_query_active_query.py | 0 .../{ => cte}/test_cte_query_set_operation.py | 0 .../test_eager_loading_caching.py | 0 .../test_eager_loading_nested.py | 0 .../test_eager_loading_scenarios_async.py | 0 .../test_eager_loading_scenarios_sync.py | 0 .../test_eager_loading_with_all.py | 0 .../test_eager_loading_with_modifier.py | 0 .../test_eager_loading_with_one.py | 0 .../test_error_handling.py | 0 .../{ => joins}/test_active_query_join.py | 0 .../feature/query/{ => joins}/test_joins.py | 0 .../test_logging_data_summarization.py | 0 .../test_query_optimization.py | 0 .../test_active_query_range.py | 0 .../{ => range_queries}/test_range_queries.py | 0 .../test_range_query_detailed.py | 0 .../test_relational_validation.py | 0 .../{ => relations}/test_relations_basic.py | 0 .../test_relations_with_async.py | 0 .../test_relations_with_sync.py | 0 .../test_active_query_set_operation.py | 0 .../test_composite_pk_set_operation.py | 6 +++ .../test_set_operation.py | 0 .../test_example_query_fixtures.py | 0 .../{ => special}/test_special_queries.py | 0 .../test_window_functions.py | 0 .../feature/query/worker/conftest.py | 8 ++++ ...ation.py => test_transaction_isolation.py} | 0 .../feature/relation/{ => base}/test_base.py | 0 .../relation/{ => base}/test_base_async.py | 0 .../relation/{ => base}/test_interfaces.py | 0 .../{ => base}/test_interfaces_async.py | 0 .../relation/{ => boundary}/test_boundary.py | 0 .../{ => boundary}/test_boundary_async.py | 0 .../relation/{ => cache}/test_cache.py | 0 .../relation/{ => cache}/test_cache_async.py | 0 .../{ => cache}/test_cache_clearing.py | 0 .../{ => derived}/test_derived_field.py | 0 .../{ => derived}/test_derived_field_async.py | 0 .../{ => derived}/test_derived_field_json.py | 0 .../test_derived_field_json_async.py | 0 .../test_descriptor_compat.py | 0 .../{ => descriptors}/test_descriptors.py | 0 .../test_descriptors_async.py | 0 .../test_nested_relationship_access.py | 0 .../test_nested_relationship_access_async.py | 0 .../{ => eager_loading}/test_with_method.py | 0 .../test_with_method_async.py | 0 .../{ => integration}/test_integration.py | 0 .../test_integration_async.py | 0 .../{ => modifiers}/test_modifier_behavior.py | 0 .../test_modifier_behavior_async.py | 0 .../{ => modifiers}/test_modifier_warning.py | 0 .../test_modifier_warning_async.py | 0 .../{ => validation}/test_path_validation.py | 0 .../test_path_validation_async.py | 0 .../{ => validation}/test_validator.py | 0 81 files changed, 99 insertions(+), 2 deletions(-) rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => bulk_crud}/test_bulk_operations.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => crud}/test_crud.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => fields}/test_field_column_mapping.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => fields}/test_fields.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => type_adapter}/test_type_adapter.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => validation}/test_pydantic_native_validation.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/basic/{ => validation}/test_validation.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => aggregate}/test_active_query_aggregate.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => aggregate}/test_aggregate_queries.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => basic}/test_active_query_basic.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => basic}/test_basic.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => basic}/test_queries.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => basic}/test_queries_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => collation}/test_collation_expression.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => cross_database}/test_cross_database_compatibility.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => cte}/test_cte_query_active_query.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => cte}/test_cte_query_set_operation.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_caching.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_nested.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_scenarios_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_scenarios_sync.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_with_all.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_with_modifier.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => eager_loading}/test_eager_loading_with_one.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => error_handling}/test_error_handling.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => joins}/test_active_query_join.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => joins}/test_joins.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => logging}/test_logging_data_summarization.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => optimization}/test_query_optimization.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => range_queries}/test_active_query_range.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => range_queries}/test_range_queries.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => range_queries}/test_range_query_detailed.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => relations}/test_relational_validation.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => relations}/test_relations_basic.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => relations}/test_relations_with_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => relations}/test_relations_with_sync.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => set_operations}/test_active_query_set_operation.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => set_operations}/test_set_operation.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => special}/test_example_query_fixtures.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => special}/test_special_queries.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/query/{ => window_functions}/test_window_functions.py (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py rename tests/rhosocial/activerecord_postgres_test/feature/query/worker/{test_worker_transaction_isolation.py => test_transaction_isolation.py} (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => base}/test_base.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => base}/test_base_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => base}/test_interfaces.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => base}/test_interfaces_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => boundary}/test_boundary.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => boundary}/test_boundary_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => cache}/test_cache.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => cache}/test_cache_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => cache}/test_cache_clearing.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => derived}/test_derived_field.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => derived}/test_derived_field_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => derived}/test_derived_field_json.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => derived}/test_derived_field_json_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => descriptors}/test_descriptor_compat.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => descriptors}/test_descriptors.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => descriptors}/test_descriptors_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => eager_loading}/test_nested_relationship_access.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => eager_loading}/test_nested_relationship_access_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => eager_loading}/test_with_method.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => eager_loading}/test_with_method_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => integration}/test_integration.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => integration}/test_integration_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => modifiers}/test_modifier_behavior.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => modifiers}/test_modifier_behavior_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => modifiers}/test_modifier_warning.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => modifiers}/test_modifier_warning_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => validation}/test_path_validation.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => validation}/test_path_validation_async.py (100%) rename tests/rhosocial/activerecord_postgres_test/feature/relation/{ => validation}/test_validator.py (100%) diff --git a/tests/providers/query.py b/tests/providers/query.py index a5482dc4..1e721e42 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -255,7 +255,25 @@ def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveRecord] def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: """Set up the composite-PK OrderItem model for the query feature tests.""" - return self._setup_model(CompositeOrderItemBase, scenario_name, "order_items") + 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 + + backend_class, config = get_scenario(scenario_name) + CompositeOrderItemBase.configure(config, backend_class) + backend_instance = CompositeOrderItemBase.__backend__ + self._track_backend(backend_instance, self._active_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, "order_items").to_sql() + backend_instance.execute(sql, params, options=opts) + except Exception: + pass + if fn := BASIC_EXPRS.get("order_items"): + create_expr = fn(backend_instance.dialect, "order_items") + sql, params = create_expr.to_sql() + backend_instance.execute(sql, params, options=opts) + return CompositeOrderItemBase def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: schemas = {} @@ -435,7 +453,26 @@ 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.""" - return await self._setup_async_model(AsyncCompositeOrderItemBase, scenario_name, "order_items") + from rhosocial.activerecord.backend.impl.postgres 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 + + _, config = get_scenario(scenario_name) + await AsyncCompositeOrderItemBase.configure(config, AsyncPostgresBackend) + backend_instance = AsyncCompositeOrderItemBase.__backend__ + self._track_backend(backend_instance, self._active_async_backends) + opts = ExecutionOptions(stmt_type=StatementType.DDL) + try: + sql, params = drop_table(backend_instance.dialect, "order_items").to_sql() + await backend_instance.execute(sql, params, options=opts) + except Exception: + pass + if fn := BASIC_EXPRS.get("order_items"): + create_expr = fn(backend_instance.dialect, "order_items") + sql, params = create_expr.to_sql() + await backend_instance.execute(sql, params, options=opts) + return AsyncCompositeOrderItemBase async def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_bulk_operations.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py new file mode 100644 index 00000000..1f34b0b8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud.py +""" +Bridge file for composite PK CRUD tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.basic.crud.test_composite_pk_crud import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.crud.test_composite_pk_crud_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_crud.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py new file mode 100644 index 00000000..ccc7fc56 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py +""" +Bridge file for derived field tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py new file mode 100644 index 00000000..a35d9746 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures.py +""" +Bridge file for example basic fixtures tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_fields.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_pydantic_native_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/basic/test_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py new file mode 100644 index 00000000..35e82e19 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py @@ -0,0 +1,8 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/conftest.py +""" +Pytest configuration for basic worker tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" +from rhosocial.activerecord.testsuite.feature.basic.worker.conftest import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py new file mode 100644 index 00000000..52d35dc0 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py @@ -0,0 +1,8 @@ +# tests/rhosocial/activerecord_postgres_test/feature/interface/conftest.py +""" +Pytest configuration for interface tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" +from rhosocial.activerecord.testsuite.feature.interface.conftest import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_aggregate.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_aggregate_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_basic.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_basic.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py new file mode 100644 index 00000000..20bd64fb --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query.py +""" +Bridge file for composite PK query tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_composite_pk_query import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.basic.test_composite_pk_query_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_queries_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_queries_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_collation_expression.py b/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_collation_expression.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_cross_database_compatibility.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py new file mode 100644 index 00000000..986a6c4a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte.py +""" +Bridge file for composite PK CTE tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_composite_pk_cte import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.cte.test_composite_pk_cte_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_active_query.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_cte_query_set_operation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_caching.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_nested.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_sync.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios_sync.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_sync.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_all.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_modifier.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_with_one.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_error_handling.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_join.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_joins.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_logging_data_summarization.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_query_optimization.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_range.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_range_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_range_query_detailed.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relational_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_basic.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_sync.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_relations_with_sync.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_sync.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_active_query_set_operation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py new file mode 100644 index 00000000..8cf2e58c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation.py +""" +Bridge file for composite PK set operation tests from the testsuite. +""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_composite_pk_set_operation import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_composite_pk_set_operation_async import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_set_operation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_example_query_fixtures.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_special_queries.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/test_window_functions.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py new file mode 100644 index 00000000..e9cd5de9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py @@ -0,0 +1,8 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/worker/conftest.py +""" +Pytest configuration for query worker tests. + +This file imports fixtures from the corresponding testsuite, making them +available to the tests in this directory. +""" +from rhosocial.activerecord.testsuite.feature.query.worker.conftest import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_base.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_base_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_base_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_interfaces_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/base/test_interfaces_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_boundary_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/boundary/test_boundary_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_cache_clearing.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_derived_field_json_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field_json_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptor_compat.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_descriptors_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptors_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_nested_relationship_access_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_nested_relationship_access_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_with_method_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/eager_loading/test_with_method_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_integration_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/integration/test_integration_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_behavior_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_behavior_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_modifier_warning_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/modifiers/test_modifier_warning_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation_async.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_path_validation_async.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_path_validation_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/relation/test_validator.py rename to tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator.py From 57aa76f3966d2a773d2eddf990db3a66e5212c46 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 16 Jul 2026 21:06:40 +0800 Subject: [PATCH 16/39] fix(postgres): fix PROVIDER_KEY_ASYNC and method names in relation conftest - Use PROVIDER_KEY_ASYNC instead of undefined PROVIDER_KEY - Fix method call names: setup_async_user_model -> setup_user_model (await) - Fix cleanup_after_test_async -> cleanup_after_test (await) - Align method calls with testsuite IRelationAsyncProvider interface --- .../feature/relation/conftest.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py index c2882883..756647a1 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py @@ -34,14 +34,14 @@ async def async_user_post_comment_classes(request): scenario = request.param provider_registry = get_provider_registry() - provider_class = provider_registry.get_provider(PROVIDER_KEY) # noqa: F405 + provider_class = provider_registry.get_provider(PROVIDER_KEY_ASYNC) # noqa: F405 provider = provider_class() - user = provider.setup_async_user_model(scenario) - post = provider.setup_async_post_model(scenario) - comment = provider.setup_async_comment_model(scenario) + user = await provider.setup_user_model(scenario) + post = await provider.setup_post_model(scenario) + comment = await provider.setup_comment_model(scenario) await provider._ensure_user_post_comment_async_schema() yield user, post, comment - await provider.cleanup_after_test_async(scenario) + await provider.cleanup_after_test(scenario) @pytest_asyncio.fixture(scope="function", params=SCENARIO_PARAMS_ASYNC) # noqa: F405 @@ -50,8 +50,8 @@ async def async_relation_boundary_context(request): scenario = request.param provider_registry = get_provider_registry() - provider_class = provider_registry.get_provider(PROVIDER_KEY) # noqa: F405 + provider_class = provider_registry.get_provider(PROVIDER_KEY_ASYNC) # noqa: F405 provider = provider_class() - owner, profile, post = provider.setup_async_relation_boundary_fixtures(scenario) + owner, profile, post = await provider.setup_relation_boundary_fixtures(scenario) yield provider, scenario, owner, profile, post - await provider.cleanup_after_test_async(scenario) + await provider.cleanup_after_test(scenario) From 4fda61912409ee071b5184ff4c5266229e0bd66c Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 17 Jul 2026 11:37:57 +0800 Subject: [PATCH 17/39] fix(postgres): remove non-existent _ensure_user_post_comment_async_schema call from relation conftest RelationAsyncProvider._setup_user_post_comment_async() already handles all three tables in a single call. --- .../activerecord_postgres_test/feature/relation/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py index 756647a1..77f688a5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/conftest.py @@ -39,7 +39,6 @@ async def async_user_post_comment_classes(request): user = await provider.setup_user_model(scenario) post = await provider.setup_post_model(scenario) comment = await provider.setup_comment_model(scenario) - await provider._ensure_user_post_comment_async_schema() yield user, post, comment await provider.cleanup_after_test(scenario) From eccdc3b7a675f714abff0cca3ecb152aac2f3934 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 17 Jul 2026 17:47:49 +0800 Subject: [PATCH 18/39] fix(postgres): extend cleanup_after_test table lists to ensure no cross-test contamination Add missing tables to PostgreSQL provider cleanup lists so all tables created during a test are reliably removed afterwards: - basic provider (sync + async): bulk_users - query provider (sync + async): profiles These were created by setup methods (setup_bulk_user_model, setup_profile_fixtures) but absent from the cleanup drop list, leading to orphaned tables across sequential test runs and cross-scenario contamination. --- tests/providers/basic.py | 2 ++ tests/providers/query.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 409cb17e..aaf8053c 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -352,6 +352,7 @@ def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ 'users', 'type_cases', 'type_tests', 'validated_field_users', 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', + 'bulk_users', 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' ] for backend_instance in self._active_backends: @@ -478,6 +479,7 @@ async def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ 'users', 'type_cases', 'type_tests', 'validated_field_users', 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', + 'bulk_users', 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' ] for backend_instance in self._active_async_backends: diff --git a/tests/providers/query.py b/tests/providers/query.py index 1e721e42..635baecc 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -318,7 +318,7 @@ def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items' + 'extended_orders', 'extended_order_items', 'searchable_items', 'profiles' ] for backend_instance in self._active_backends: try: @@ -477,7 +477,7 @@ async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRe async def cleanup_after_test(self, scenario_name: str): tables_to_drop = [ 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items' + 'extended_orders', 'extended_order_items', 'searchable_items', 'profiles' ] for backend_instance in self._active_async_backends: try: From 24e0b3aae66a7630e8a40e23712daa951df5d983 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 17 Jul 2026 20:02:54 +0800 Subject: [PATCH 19/39] fix(postgres): add DROP TABLE to relation/connection/benchmark cleanup, fix bare excepts - RelationSyncProvider/RelationAsyncProvider cleanup_after_test now drops all 12 created tables (employees, departments, authors, books, chapters, profiles, users, posts, comments, boundary_*) with CASCADE before disconnect. - BasicConnectionProvider/QueryConnectionProvider cleanup now drops test_users table. - CrudBenchmarkProvider cleanup now drops benchmark_users table. - All 8 bare 'except:' -> 'except Exception:' across basic.py, query.py, mixins.py, events.py providers. --- tests/providers/basic.py | 4 +-- tests/providers/basic_connection.py | 8 ++++++ tests/providers/crud_benchmark.py | 8 ++++++ tests/providers/events.py | 4 +-- tests/providers/mixins.py | 4 +-- tests/providers/query.py | 4 +-- tests/providers/query_connection.py | 8 ++++++ tests/providers/relation.py | 38 ++++++++++++++++++++++++----- 8 files changed, 64 insertions(+), 14 deletions(-) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index aaf8053c..20843722 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -365,7 +365,7 @@ def cleanup_after_test(self, scenario_name: str): finally: try: backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_backends.clear() @@ -492,6 +492,6 @@ async def cleanup_after_test(self, scenario_name: str): finally: try: await backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_async_backends.clear() diff --git a/tests/providers/basic_connection.py b/tests/providers/basic_connection.py index ebcfa369..c15a6668 100644 --- a/tests/providers/basic_connection.py +++ b/tests/providers/basic_connection.py @@ -134,6 +134,10 @@ def cleanup_sync(self, scenario_name: str, pool: BackendPool): # Disconnect backends for backend in self._active_backends: + try: + backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: backend.disconnect() except Exception: @@ -146,6 +150,10 @@ async def cleanup_async(self, scenario_name: str, pool: AsyncBackendPool): # Disconnect backends for backend in self._active_async_backends: + try: + await backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: await backend.disconnect() except Exception: diff --git a/tests/providers/crud_benchmark.py b/tests/providers/crud_benchmark.py index 1c35af46..675a8d14 100644 --- a/tests/providers/crud_benchmark.py +++ b/tests/providers/crud_benchmark.py @@ -164,6 +164,10 @@ def _schema_sql(self): def _cleanup_sync(self): for backend in self._active_backends: + try: + backend.execute("DROP TABLE IF EXISTS benchmark_users CASCADE") + except Exception: + pass try: backend.disconnect() except Exception: @@ -172,6 +176,10 @@ def _cleanup_sync(self): async def _cleanup_async(self): for backend in self._active_async_backends: + try: + await backend.execute("DROP TABLE IF EXISTS benchmark_users CASCADE") + except Exception: + pass try: await backend.disconnect() except Exception: diff --git a/tests/providers/events.py b/tests/providers/events.py index d414fa6f..0b11a7ca 100644 --- a/tests/providers/events.py +++ b/tests/providers/events.py @@ -147,7 +147,7 @@ def cleanup_after_test(self, scenario_name: str): finally: try: backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_backends.clear() @@ -194,6 +194,6 @@ async def cleanup_after_test(self, scenario_name: str): finally: try: await backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_async_backends.clear() diff --git a/tests/providers/mixins.py b/tests/providers/mixins.py index fa5b8ed7..149fd6f8 100644 --- a/tests/providers/mixins.py +++ b/tests/providers/mixins.py @@ -171,7 +171,7 @@ def cleanup_after_test(self, scenario_name: str): finally: try: backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_backends.clear() @@ -225,6 +225,6 @@ async def cleanup_after_test(self, scenario_name: str): finally: try: await backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_async_backends.clear() diff --git a/tests/providers/query.py b/tests/providers/query.py index 635baecc..23a723bc 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -330,7 +330,7 @@ def cleanup_after_test(self, scenario_name: str): finally: try: backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_backends.clear() @@ -489,6 +489,6 @@ async def cleanup_after_test(self, scenario_name: str): finally: try: await backend_instance.disconnect() - except: # noqa: E722 + except Exception: pass self._active_async_backends.clear() diff --git a/tests/providers/query_connection.py b/tests/providers/query_connection.py index ab962f1e..7fab8e23 100644 --- a/tests/providers/query_connection.py +++ b/tests/providers/query_connection.py @@ -134,6 +134,10 @@ def cleanup_sync(self, scenario_name: str, pool: BackendPool): # Disconnect backends for backend in self._active_backends: + try: + backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: backend.disconnect() except Exception: @@ -146,6 +150,10 @@ async def cleanup_async(self, scenario_name: str, pool: AsyncBackendPool): # Disconnect backends for backend in self._active_async_backends: + try: + await backend.execute("DROP TABLE IF EXISTS test_users CASCADE") + except Exception: + pass try: await backend.disconnect() except Exception: diff --git a/tests/providers/relation.py b/tests/providers/relation.py index 6a46fb31..e81bf16a 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -270,11 +270,24 @@ def _load_relation_boundary_dataset(self, dataset_name): raise ValueError(f"Unknown relation boundary dataset: {dataset_name}") def cleanup_after_test(self, scenario_name: str) -> None: + tables_to_drop = [ + 'employees', 'departments', + 'authors', 'books', 'chapters', 'profiles', + 'users', 'posts', 'comments', + 'relation_boundary_owners', 'relation_boundary_profiles', 'relation_boundary_posts', + ] for backend in self._active_backends: try: - backend.disconnect() - except Exception: - pass + for table in tables_to_drop: + try: + backend.execute(f'DROP TABLE IF EXISTS "{table}" CASCADE') + except Exception: + pass + finally: + try: + backend.disconnect() + except Exception: + pass self._active_backends.clear() self._reset_setup_state() @@ -424,10 +437,23 @@ async def _load_async_relation_boundary_dataset(self, dataset_name): raise ValueError(f"Unknown relation boundary dataset: {dataset_name}") async def cleanup_after_test(self, scenario_name: str): + tables_to_drop = [ + 'employees', 'departments', + 'authors', 'books', 'chapters', 'profiles', + 'users', 'posts', 'comments', + 'relation_boundary_owners', 'relation_boundary_profiles', 'relation_boundary_posts', + ] for backend in self._active_async_backends: try: - await backend.disconnect() - except Exception: - pass + for table in tables_to_drop: + try: + await backend.execute(f'DROP TABLE IF EXISTS "{table}" CASCADE') + except Exception: + pass + finally: + try: + await backend.disconnect() + except Exception: + pass self._active_async_backends.clear() self._reset_setup_state() From 80a507d665351033ac5fb1534c49edccdbd1d05f Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 18 Jul 2026 17:24:42 +0800 Subject: [PATCH 20/39] refactor(test): add missing _async bridge files (61), fix naming - Generate 61 _async.py bridge files for sync/async pairing compliance - Rename test_eager_loading_scenarios_sync.py -> test_eager_loading_scenarios.py - Rename test_relations_with_sync.py -> test_relations_with.py - Split test_derived_field_basic.py into sync-only + test_derived_field_basic_async.py - Add missing bridge files for basic/fields/ subcategories - Fix testsuite module path in relation/derived/test_derived_field.py --- .../feature/basic/bulk_crud/test_bulk_operations_async.py | 3 +++ .../basic/connection/test_active_record_context_async.py | 3 +++ .../basic/connection/test_active_record_crud_async.py | 3 +++ .../feature/basic/crud/test_composite_pk_crud_async.py | 3 +++ .../feature/basic/crud/test_crud_async.py | 3 +++ .../feature/basic/fields/test_derived_field.py | 3 +++ .../feature/basic/fields/test_derived_field_async.py | 3 +++ .../feature/basic/fields/test_derived_field_basic.py | 3 +-- .../feature/basic/fields/test_derived_field_basic_async.py | 3 +++ .../basic/fields/test_example_basic_fixtures_async.py | 3 +++ .../basic/fields/test_field_column_mapping_async.py | 3 +++ .../feature/basic/fields/test_fields_async.py | 3 +++ .../feature/basic/type_adapter/test_type_adapter_async.py | 3 +++ .../validation/test_pydantic_native_validation_async.py | 3 +++ .../feature/basic/validation/test_validation_async.py | 3 +++ .../basic/worker/test_connection_management_async.py | 3 +++ .../feature/basic/worker/test_parallel_crud_async.py | 3 +++ .../basic/worker/test_worker_pool_lifecycle_async.py | 3 +++ .../feature/events/test_handlers_async.py | 3 +++ .../feature/events/test_lifecycle_async.py | 3 +++ .../feature/interface/test_threadsafe_dict_async.py | 3 +++ .../feature/mixins/test_combined_articles_async.py | 3 +++ .../feature/mixins/test_optimistic_lock_async.py | 3 +++ .../feature/mixins/test_soft_delete_async.py | 3 +++ .../feature/mixins/test_timestamps_async.py | 3 +++ .../query/aggregate/test_active_query_aggregate_async.py | 3 +++ .../query/aggregate/test_aggregate_queries_async.py | 3 +++ .../feature/query/basic/test_active_query_basic_async.py | 3 +++ .../feature/query/basic/test_basic_async.py | 3 +++ .../feature/query/basic/test_composite_pk_query_async.py | 3 +++ .../query/connection/test_active_query_context_async.py | 3 +++ .../query/connection/test_cte_query_context_async.py | 3 +++ .../query/connection/test_set_operation_context_async.py | 3 +++ .../test_cross_database_compatibility_async.py | 3 +++ .../feature/query/cte/test_composite_pk_cte_async.py | 3 +++ .../feature/query/cte/test_cte_query_active_query_async.py | 3 +++ .../query/cte/test_cte_query_set_operation_async.py | 3 +++ .../eager_loading/test_eager_loading_caching_async.py | 3 +++ .../query/eager_loading/test_eager_loading_nested_async.py | 3 +++ .../query/eager_loading/test_eager_loading_scenarios.py | 2 ++ .../eager_loading/test_eager_loading_scenarios_sync.py | 7 ------- .../eager_loading/test_eager_loading_with_all_async.py | 3 +++ .../test_eager_loading_with_modifier_async.py | 3 +++ .../eager_loading/test_eager_loading_with_one_async.py | 3 +++ .../query/error_handling/test_error_handling_async.py | 3 +++ .../feature/query/joins/test_active_query_join_async.py | 3 +++ .../feature/query/joins/test_joins_async.py | 3 +++ .../query/logging/test_logging_data_summarization_async.py | 3 +++ .../query/optimization/test_query_optimization_async.py | 3 +++ .../query/range_queries/test_active_query_range_async.py | 3 +++ .../query/range_queries/test_range_queries_async.py | 3 +++ .../query/range_queries/test_range_query_detailed_async.py | 3 +++ .../query/relations/test_relational_validation_async.py | 3 +++ .../feature/query/relations/test_relations_basic_async.py | 3 +++ ...{test_relations_with_sync.py => test_relations_with.py} | 0 .../test_active_query_set_operation_async.py | 3 +++ .../test_composite_pk_set_operation_async.py | 3 +++ .../query/set_operations/test_set_operation_async.py | 3 +++ .../query/special/test_example_query_fixtures_async.py | 3 +++ .../feature/query/special/test_special_queries_async.py | 3 +++ .../query/window_functions/test_window_functions_async.py | 3 +++ .../feature/query/worker/test_parallel_queries_async.py | 3 +++ .../query/worker/test_transaction_isolation_async.py | 3 +++ .../feature/relation/cache/test_cache_clearing_async.py | 3 +++ .../feature/relation/derived/test_derived_field.py | 6 +++--- .../relation/descriptors/test_descriptor_compat_async.py | 3 +++ .../feature/relation/validation/test_validator_async.py | 3 +++ 67 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_sync.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py rename tests/rhosocial/activerecord_postgres_test/feature/query/relations/{test_relations_with_sync.py => test_relations_with.py} (100%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py new file mode 100644 index 00000000..2a67aa45 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/bulk_crud/test_bulk_operations_async.py +"""Bridge file for async feature.basic.bulk_crud.test_bulk_operations_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.bulk_crud.test_bulk_operations_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py new file mode 100644 index 00000000..8d360f89 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_context_async.py +"""Bridge file for async feature.basic.connection.test_active_record_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py new file mode 100644 index 00000000..a826a9e8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_active_record_crud_async.py +"""Bridge file for async feature.basic.connection.test_active_record_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.connection.test_active_record_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py new file mode 100644 index 00000000..5bf67d8f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_composite_pk_crud_async.py +"""Bridge file for async feature.basic.crud.test_composite_pk_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.crud.test_composite_pk_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py new file mode 100644 index 00000000..b8ffe646 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/crud/test_crud_async.py +"""Bridge file for async feature.basic.crud.test_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.crud.test_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py new file mode 100644 index 00000000..d9f98dfa --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py +"""Bridge file for sync feature.basic.fields.test_derived_field tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py new file mode 100644 index 00000000..383c8020 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py +"""Bridge file for async feature.basic.fields.test_derived_field_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py index ccc7fc56..cb781f9d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic.py @@ -2,5 +2,4 @@ """ Bridge file for derived field tests from the testsuite. """ -from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field_async import * # noqa: F403 \ No newline at end of file +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field import * # noqa: F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py new file mode 100644 index 00000000..e6cf4fff --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_basic_async.py +"""Bridge file for async derived field basic tests from the testsuite.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field_async import * # noqa: F401, F403 \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py new file mode 100644 index 00000000..cef55fc3 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_example_basic_fixtures_async.py +"""Bridge file for async feature.basic.fields.test_example_basic_fixtures_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_example_basic_fixtures_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py new file mode 100644 index 00000000..d46911b2 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_field_column_mapping_async.py +"""Bridge file for async feature.basic.fields.test_field_column_mapping_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_field_column_mapping_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py new file mode 100644 index 00000000..2dace749 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_fields_async.py +"""Bridge file for async feature.basic.fields.test_fields_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.fields.test_fields_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py new file mode 100644 index 00000000..533df4fa --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter_async.py +"""Bridge file for async feature.basic.type_adapter.test_type_adapter_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py new file mode 100644 index 00000000..1b5019de --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_pydantic_native_validation_async.py +"""Bridge file for async feature.basic.validation.test_pydantic_native_validation_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.validation.test_pydantic_native_validation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py new file mode 100644 index 00000000..df784d78 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/validation/test_validation_async.py +"""Bridge file for async feature.basic.validation.test_validation_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.validation.test_validation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py new file mode 100644 index 00000000..3340c392 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_connection_management_async.py +"""Bridge file for async feature.basic.worker.test_connection_management_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.worker.test_connection_management_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py new file mode 100644 index 00000000..62eedf04 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_parallel_crud_async.py +"""Bridge file for async feature.basic.worker.test_parallel_crud_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.worker.test_parallel_crud_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py new file mode 100644 index 00000000..15ccdd00 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/worker/test_worker_pool_lifecycle_async.py +"""Bridge file for async feature.basic.worker.test_worker_pool_lifecycle_async tests.""" +from rhosocial.activerecord.testsuite.feature.basic.worker.test_worker_pool_lifecycle_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py new file mode 100644 index 00000000..6b946f79 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/events/test_handlers_async.py +"""Bridge file for async feature.events.test_handlers_async tests.""" +from rhosocial.activerecord.testsuite.feature.events.test_handlers_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py new file mode 100644 index 00000000..94312c2a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/events/test_lifecycle_async.py +"""Bridge file for async feature.events.test_lifecycle_async tests.""" +from rhosocial.activerecord.testsuite.feature.events.test_lifecycle_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py new file mode 100644 index 00000000..b5e43962 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/interface/test_threadsafe_dict_async.py +"""Bridge file for async feature.interface.test_threadsafe_dict_async tests.""" +from rhosocial.activerecord.testsuite.feature.interface.test_threadsafe_dict_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py new file mode 100644 index 00000000..1dc7d5c5 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_combined_articles_async.py +"""Bridge file for async feature.mixins.test_combined_articles_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_combined_articles_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py new file mode 100644 index 00000000..4c233401 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_optimistic_lock_async.py +"""Bridge file for async feature.mixins.test_optimistic_lock_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_optimistic_lock_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py new file mode 100644 index 00000000..14371fd5 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_soft_delete_async.py +"""Bridge file for async feature.mixins.test_soft_delete_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_soft_delete_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py new file mode 100644 index 00000000..0260c373 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/mixins/test_timestamps_async.py +"""Bridge file for async feature.mixins.test_timestamps_async tests.""" +from rhosocial.activerecord.testsuite.feature.mixins.test_timestamps_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py new file mode 100644 index 00000000..c3568677 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_active_query_aggregate_async.py +"""Bridge file for async feature.query.aggregate.test_active_query_aggregate_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_active_query_aggregate_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py new file mode 100644 index 00000000..df180f44 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/aggregate/test_aggregate_queries_async.py +"""Bridge file for async feature.query.aggregate.test_aggregate_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.aggregate.test_aggregate_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py new file mode 100644 index 00000000..4bc013b4 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_active_query_basic_async.py +"""Bridge file for async feature.query.basic.test_active_query_basic_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_active_query_basic_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py new file mode 100644 index 00000000..55d0570f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_basic_async.py +"""Bridge file for async feature.query.basic.test_basic_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_basic_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py new file mode 100644 index 00000000..a4d65b87 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/basic/test_composite_pk_query_async.py +"""Bridge file for async feature.query.basic.test_composite_pk_query_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.basic.test_composite_pk_query_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py new file mode 100644 index 00000000..cd8c61c6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_active_query_context_async.py +"""Bridge file for async feature.query.connection.test_active_query_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.connection.test_active_query_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py new file mode 100644 index 00000000..e8b21313 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_cte_query_context_async.py +"""Bridge file for async feature.query.connection.test_cte_query_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.connection.test_cte_query_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py new file mode 100644 index 00000000..9bc0e34e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/connection/test_set_operation_context_async.py +"""Bridge file for async feature.query.connection.test_set_operation_context_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.connection.test_set_operation_context_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py new file mode 100644 index 00000000..1c8d2108 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cross_database/test_cross_database_compatibility_async.py +"""Bridge file for async feature.query.cross_database.test_cross_database_compatibility_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cross_database.test_cross_database_compatibility_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py new file mode 100644 index 00000000..46972049 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_composite_pk_cte_async.py +"""Bridge file for async feature.query.cte.test_composite_pk_cte_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_composite_pk_cte_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py new file mode 100644 index 00000000..3429c45a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_active_query_async.py +"""Bridge file for async feature.query.cte.test_cte_query_active_query_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_active_query_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py new file mode 100644 index 00000000..4bf171be --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/cte/test_cte_query_set_operation_async.py +"""Bridge file for async feature.query.cte.test_cte_query_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.cte.test_cte_query_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py new file mode 100644 index 00000000..f946bcec --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_caching_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_caching_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_caching_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py new file mode 100644 index 00000000..b7921aa6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_nested_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_nested_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_nested_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py new file mode 100644 index 00000000..7b5ca3cd --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios.py @@ -0,0 +1,2 @@ +"""Bridge file for sync eager loading scenario tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_sync.py deleted file mode 100644 index 53b80607..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_scenarios_sync.py +++ /dev/null @@ -1,7 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/query/test_eager_loading_scenarios.py -""" -Bridge file for sync eager loading scenario tests. -""" -from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios import * # noqa: F401, F403 -from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_scenarios_async import * # noqa: F403 - diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py new file mode 100644 index 00000000..197ae5d6 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_all_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_with_all_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_all_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py new file mode 100644 index 00000000..c558e117 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_modifier_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_with_modifier_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_modifier_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py new file mode 100644 index 00000000..1ad43fb7 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/eager_loading/test_eager_loading_with_one_async.py +"""Bridge file for async feature.query.eager_loading.test_eager_loading_with_one_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.eager_loading.test_eager_loading_with_one_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py new file mode 100644 index 00000000..ef91b73c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/error_handling/test_error_handling_async.py +"""Bridge file for async feature.query.error_handling.test_error_handling_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.error_handling.test_error_handling_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py new file mode 100644 index 00000000..63a0b532 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_active_query_join_async.py +"""Bridge file for async feature.query.joins.test_active_query_join_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.joins.test_active_query_join_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py new file mode 100644 index 00000000..f5710f45 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/joins/test_joins_async.py +"""Bridge file for async feature.query.joins.test_joins_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.joins.test_joins_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py new file mode 100644 index 00000000..1f9e0aa4 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/logging/test_logging_data_summarization_async.py +"""Bridge file for async feature.query.logging.test_logging_data_summarization_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.logging.test_logging_data_summarization_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py new file mode 100644 index 00000000..9688b94b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/optimization/test_query_optimization_async.py +"""Bridge file for async feature.query.optimization.test_query_optimization_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.optimization.test_query_optimization_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py new file mode 100644 index 00000000..667a3718 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_active_query_range_async.py +"""Bridge file for async feature.query.range_queries.test_active_query_range_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_active_query_range_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py new file mode 100644 index 00000000..c6ef096e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_queries_async.py +"""Bridge file for async feature.query.range_queries.test_range_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py new file mode 100644 index 00000000..df8937fe --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/range_queries/test_range_query_detailed_async.py +"""Bridge file for async feature.query.range_queries.test_range_query_detailed_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.range_queries.test_range_query_detailed_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py new file mode 100644 index 00000000..9276ed3a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relational_validation_async.py +"""Bridge file for async feature.query.relations.test_relational_validation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.relations.test_relational_validation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py new file mode 100644 index 00000000..6ff408c0 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_basic_async.py +"""Bridge file for async feature.query.relations.test_relations_basic_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.relations.test_relations_basic_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_sync.py b/tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with_sync.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/relations/test_relations_with.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py new file mode 100644 index 00000000..97e7fa6f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_active_query_set_operation_async.py +"""Bridge file for async feature.query.set_operations.test_active_query_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_active_query_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py new file mode 100644 index 00000000..fe46fbee --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_composite_pk_set_operation_async.py +"""Bridge file for async feature.query.set_operations.test_composite_pk_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_composite_pk_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py new file mode 100644 index 00000000..20f605cb --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/set_operations/test_set_operation_async.py +"""Bridge file for async feature.query.set_operations.test_set_operation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.set_operations.test_set_operation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py new file mode 100644 index 00000000..aa20fa09 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/special/test_example_query_fixtures_async.py +"""Bridge file for async feature.query.special.test_example_query_fixtures_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.special.test_example_query_fixtures_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py new file mode 100644 index 00000000..4bba9f80 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/special/test_special_queries_async.py +"""Bridge file for async feature.query.special.test_special_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.special.test_special_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py new file mode 100644 index 00000000..f69a0000 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/window_functions/test_window_functions_async.py +"""Bridge file for async feature.query.window_functions.test_window_functions_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.window_functions.test_window_functions_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py new file mode 100644 index 00000000..326c49d9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_parallel_queries_async.py +"""Bridge file for async feature.query.worker.test_parallel_queries_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.worker.test_parallel_queries_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py new file mode 100644 index 00000000..f73efd1c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation_async.py +"""Bridge file for async feature.query.worker.test_transaction_isolation_async tests.""" +from rhosocial.activerecord.testsuite.feature.query.worker.test_transaction_isolation_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py new file mode 100644 index 00000000..e5e95b6a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/cache/test_cache_clearing_async.py +"""Bridge file for async feature.relation.cache.test_cache_clearing_async tests.""" +from rhosocial.activerecord.testsuite.feature.relation.cache.test_cache_clearing_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py index b67892d2..cd7185c5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py @@ -1,3 +1,3 @@ -from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field_async import * # noqa: F403 - +# tests/rhosocial/activerecord_postgres_test/feature/relation/derived/test_derived_field.py +"""Bridge file for sync relation derived field tests.""" +from rhosocial.activerecord.testsuite.feature.relation.derived.test_derived_field import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py new file mode 100644 index 00000000..53052583 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/descriptors/test_descriptor_compat_async.py +"""Bridge file for async feature.relation.descriptors.test_descriptor_compat_async tests.""" +from rhosocial.activerecord.testsuite.feature.relation.descriptors.test_descriptor_compat_async import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py new file mode 100644 index 00000000..799baf9f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py @@ -0,0 +1,3 @@ +# tests/rhosocial/activerecord_postgres_test/feature/relation/validation/test_validator_async.py +"""Bridge file for async feature.relation.validation.test_validator_async tests.""" +from rhosocial.activerecord.testsuite.feature.relation.validation.test_validator_async import * # noqa: F401, F403 From d326af592eed49fd192a940cd24cd085df3b78ae Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 10:43:18 +0800 Subject: [PATCH 21/39] fix(test): remove conflicting test_derived_field bridges from basic/fields/ Same fix as python-activerecord-mysql: identical leaf filename test_derived_field.py from two different testsuite modules collide under pytest's import model. Drop the basic/fields duplicates to keep only the relation/derived/ bridges, matching SQLite. --- .../feature/basic/fields/test_derived_field.py | 3 --- .../feature/basic/fields/test_derived_field_async.py | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py deleted file mode 100644 index d9f98dfa..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py +++ /dev/null @@ -1,3 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py -"""Bridge file for sync feature.basic.fields.test_derived_field tests.""" -from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field import * # noqa: F401, F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py deleted file mode 100644 index 383c8020..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py +++ /dev/null @@ -1,3 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py -"""Bridge file for async feature.basic.fields.test_derived_field_async tests.""" -from rhosocial.activerecord.testsuite.feature.basic.fields.test_derived_field_async import * # noqa: F401, F403 From f5fe767a96135e29476c6298308b4140db973ffd Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 11:40:35 +0800 Subject: [PATCH 22/39] fix(test): restore __init__.py under tests/benchmark/packages Same fix as python-activerecord-mysql: the four __init__.py markers under tests/benchmark/{,backend,activerecord_bulk,relation} were removed in commit f146d54, breaking pytest collection on tests that use relative imports (e.g. 'from .workloads import ...' in tests/benchmark/backend/test_execute_crud_*.py). Restore them so pytest treats those directories as packages and the relative imports resolve. --- tests/benchmark/__init__.py | 0 tests/benchmark/activerecord_bulk/__init__.py | 0 tests/benchmark/backend/__init__.py | 0 tests/benchmark/relation/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/benchmark/__init__.py create mode 100644 tests/benchmark/activerecord_bulk/__init__.py create mode 100644 tests/benchmark/backend/__init__.py create mode 100644 tests/benchmark/relation/__init__.py diff --git a/tests/benchmark/__init__.py b/tests/benchmark/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/benchmark/activerecord_bulk/__init__.py b/tests/benchmark/activerecord_bulk/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/benchmark/backend/__init__.py b/tests/benchmark/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/benchmark/relation/__init__.py b/tests/benchmark/relation/__init__.py new file mode 100644 index 00000000..e69de29b From be5daf386790e839b4f35c23e62cec57d132efcb Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 15:33:03 +0800 Subject: [PATCH 23/39] fix(test): resolve cursor pollution, DataType errors, and import-file-mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move cursor.description assertions before cursor.close() in all 8 cursor pollution tests (both sync and async) to fix assert None - Replace string type names ('INTEGER', 'VARCHAR', 'NUMERIC') with proper DataType instances (IntegerType, VarCharType, DecimalType) in test_recursive_cte_graph_traversal.py to fix TypeError - Rename test_transaction_isolation.py → test_worker_transaction_isolation.py to avoid import-file-mismatch collision with backend/transaction/ --- .../test_recursive_cte_graph_traversal.py | 39 +++++++++++-------- .../feature/backend/test_cursor_pollution.py | 14 +++---- ...y => test_worker_transaction_isolation.py} | 0 3 files changed, 29 insertions(+), 24 deletions(-) rename tests/rhosocial/activerecord_postgres_test/feature/query/worker/{test_transaction_isolation.py => test_worker_transaction_isolation.py} (100%) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index fa0bf12c..a02f81a1 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -28,6 +28,11 @@ ColumnConstraint, ColumnConstraintType, ) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + VarCharType, + DecimalType, +) from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import BetweenPredicate from rhosocial.activerecord.backend.expression.query_parts import ( @@ -47,16 +52,16 @@ def social_network_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) backend.execute(*CreateTableExpression(dialect, "users", [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", "VARCHAR(100)"), + ColumnDefinition("name", VarCharType(100)), ]).to_sql()) backend.execute(*CreateTableExpression(dialect, "follows", [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", "INTEGER"), - ColumnDefinition("followed_id", "INTEGER"), + ColumnDefinition("follower_id", IntegerType()), + ColumnDefinition("followed_id", IntegerType()), ]).to_sql()) backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], @@ -92,18 +97,18 @@ def aml_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) backend.execute(*CreateTableExpression(dialect, "accounts", [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("account_holder", "VARCHAR(100)"), - ColumnDefinition("account_type", "VARCHAR(20)"), + ColumnDefinition("account_holder", VarCharType(100)), + ColumnDefinition("account_type", VarCharType(20)), ]).to_sql()) backend.execute(*CreateTableExpression(dialect, "transactions", [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("source_account_id", "INTEGER"), - ColumnDefinition("target_account_id", "INTEGER"), - ColumnDefinition("amount", "NUMERIC(12,2)"), + ColumnDefinition("source_account_id", IntegerType()), + ColumnDefinition("target_account_id", IntegerType()), + ColumnDefinition("amount", DecimalType(12, 2)), ]).to_sql()) backend.execute(*InsertExpression(dialect, "accounts", columns=["id", "account_holder", "account_type"], @@ -457,16 +462,16 @@ async def async_social_network_data(self, async_postgres_backend): await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) await backend.execute(*CreateTableExpression(dialect, "users", [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", "VARCHAR(100)"), + ColumnDefinition("name", VarCharType(100)), ]).to_sql()) await backend.execute(*CreateTableExpression(dialect, "follows", [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", "INTEGER"), - ColumnDefinition("followed_id", "INTEGER"), + ColumnDefinition("follower_id", IntegerType()), + ColumnDefinition("followed_id", IntegerType()), ]).to_sql()) await backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py index c1fc74cd..c174b2ce 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cursor_pollution.py @@ -28,7 +28,6 @@ def test_get_server_version_then_query(self, postgres_backend): cursor = backend._get_cursor() cursor.execute("SELECT 1 AS marker") rows = cursor.fetchall() - cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -36,6 +35,7 @@ def test_get_server_version_then_query(self, postgres_backend): assert col_name == "marker", ( f"cursor.description polluted! expected 'marker', got: {col_name}" ) + cursor.close() def test_introspect_and_adapt_then_query(self, postgres_backend): """introspect_and_adapt() then a normal query.""" @@ -45,7 +45,6 @@ def test_introspect_and_adapt_then_query(self, postgres_backend): cursor = backend._get_cursor() cursor.execute("SELECT 'ok' AS status") rows = cursor.fetchall() - cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -53,6 +52,7 @@ def test_introspect_and_adapt_then_query(self, postgres_backend): assert col_name == "status", ( f"cursor.description polluted! expected 'status', got: {col_name}" ) + cursor.close() def test_high_frequency_version_query_cycle(self, postgres_backend): """Repeated get_server_version → query cycle to expose state leaks.""" @@ -64,7 +64,6 @@ def test_high_frequency_version_query_cycle(self, postgres_backend): cursor = backend._get_cursor() cursor.execute(f"SELECT {i} AS cycle") rows = cursor.fetchall() - cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -73,6 +72,7 @@ def test_high_frequency_version_query_cycle(self, postgres_backend): f"Iteration {i}: cursor.description polluted! " f"expected 'cycle', got: {col_name}" ) + cursor.close() logger.info("PostgreSQL high-frequency version query cycle 200 iterations passed") @@ -90,7 +90,6 @@ async def test_get_server_version_then_query(self, async_postgres_backend): cursor = await backend._get_cursor() await cursor.execute("SELECT 1 AS marker") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -98,6 +97,7 @@ async def test_get_server_version_then_query(self, async_postgres_backend): assert col_name == "marker", ( f"Async cursor.description polluted! expected 'marker', got: {col_name}" ) + await cursor.close() @pytest.mark.asyncio async def test_introspect_and_adapt_then_query(self, async_postgres_backend): @@ -108,7 +108,6 @@ async def test_introspect_and_adapt_then_query(self, async_postgres_backend): cursor = await backend._get_cursor() await cursor.execute("SELECT 'ok' AS status") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -117,6 +116,7 @@ async def test_introspect_and_adapt_then_query(self, async_postgres_backend): f"Async cursor.description polluted after introspect! " f"expected 'status', got: {col_name}" ) + await cursor.close() @pytest.mark.asyncio async def test_context_entry_workflow(self, async_postgres_backend): @@ -127,7 +127,6 @@ async def test_context_entry_workflow(self, async_postgres_backend): cursor = await backend._get_cursor() await cursor.execute("SELECT 'ctx_ok' AS ctx_status") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -136,6 +135,7 @@ async def test_context_entry_workflow(self, async_postgres_backend): f"Async cursor.description polluted after context()! " f"expected 'ctx_status', got: {col_name}" ) + await cursor.close() @pytest.mark.asyncio async def test_high_frequency_version_query_cycle(self, async_postgres_backend): @@ -148,7 +148,6 @@ async def test_high_frequency_version_query_cycle(self, async_postgres_backend): cursor = await backend._get_cursor() await cursor.execute(f"SELECT {i} AS cycle") rows = await cursor.fetchall() - await cursor.close() assert len(rows) > 0 assert cursor.description is not None @@ -157,5 +156,6 @@ async def test_high_frequency_version_query_cycle(self, async_postgres_backend): f"Iteration {i}: async cursor.description polluted! " f"expected 'cycle', got: {col_name}" ) + await cursor.close() logger.info("Async PostgreSQL high-frequency version query cycle 200 iterations passed") \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation.py b/tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py similarity index 100% rename from tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_transaction_isolation.py rename to tests/rhosocial/activerecord_postgres_test/feature/query/worker/test_worker_transaction_isolation.py From 326cfaa88ec46d3bec5a657777d300d34bbe8d79 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 16:11:43 +0800 Subject: [PATCH 24/39] fix(ci): update benchmark test file names to match testsuite (remove _sync suffix) --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15186a95..7a2b0170 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,13 +123,13 @@ jobs: --rootdir=. \ --confcutdir=. \ "$TESTSUITE_DIR/benchmark/crud/test_delete_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_delete_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_delete.py" \ "$TESTSUITE_DIR/benchmark/crud/test_insert_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_insert_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_insert.py" \ "$TESTSUITE_DIR/benchmark/crud/test_query_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_query_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_query.py" \ "$TESTSUITE_DIR/benchmark/crud/test_update_async.py" \ - "$TESTSUITE_DIR/benchmark/crud/test_update_sync.py" \ + "$TESTSUITE_DIR/benchmark/crud/test_update.py" \ "$TESTSUITE_DIR/benchmark/query" \ "$TESTSUITE_DIR/benchmark/transaction" \ "$TESTSUITE_DIR/benchmark/mixin" \ From 7dcf56e0fe3ef330ba08618fadd656cc07832bc3 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 16:34:05 +0800 Subject: [PATCH 25/39] fix(test): replace FunctionCall('+') with infix BinaryArithmeticExpression for depth+1 in CTE tests PostgreSQL does not support prefix function-call syntax for arithmetic operators (+(a, b)). Use the overloaded + operator (creates BinaryArithmeticExpression) which generates correct infix a + b SQL. --- .../backend/postgres/test_recursive_cte_graph_traversal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index a02f81a1..f429d8f4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -167,7 +167,7 @@ def _build_traversal_cte(self, dialect): select=[ Column(dialect, "id", table="u"), Column(dialect, "name", table="u"), - FunctionCall(dialect, "+", Column(dialect, "depth", table="t"), Literal(dialect, 1)).as_("depth"), + (Column(dialect, "depth", table="t") + Literal(dialect, 1)).as_("depth"), ], from_=recursive_join, where=WhereClause(dialect, condition=Column(dialect, "depth", table="t") < Literal(dialect, 4)), @@ -332,7 +332,7 @@ def _build_aml_cte(self, dialect): Column(dialect, "account_holder", table="a"), Column(dialect, "account_type", table="a"), Column(dialect, "amount", table="tx"), - FunctionCall(dialect, "+", Column(dialect, "depth", table="tr"), Literal(dialect, 1)).as_("depth"), + (Column(dialect, "depth", table="tr") + Literal(dialect, 1)).as_("depth"), ], from_=recursive_join, where=WhereClause(dialect, condition=Column(dialect, "depth", table="tr") < Literal(dialect, 5)), From 22c59b9b0b3624507ac92c499a43ffe463bedceb Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 16:50:14 +0800 Subject: [PATCH 26/39] fix(test): add GROUP BY to fund_trace_aggregation, fix pre-existing assertion bugs in CTE tests - Add GroupByHavingClause to fix column 'fund_trace.depth' must appear in GROUP BY - Fix depth 3 expected sum to 800000 (500000 Diana + 300000 Eve) - Fix social network assertions: Charlie at depth 2 (not 1), no Diana in results - These bugs were pre-existing: tests were never runnable before DataType fix --- .../test_recursive_cte_graph_traversal.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index f429d8f4..b05c6fb1 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -38,6 +38,7 @@ from rhosocial.activerecord.backend.expression.query_parts import ( WhereClause, OrderByClause, + GroupByHavingClause, JoinExpression, ) @@ -219,9 +220,8 @@ def test_friend_recommendation_depth_1_to_3(self, postgres_backend, social_netwo # Alice follows Bob (depth 1) and Charlie (depth 1) assert names_at_depth.get("Bob") == 1 - assert names_at_depth.get("Charlie") == 1 - # Bob follows Diana (depth 2) - assert names_at_depth.get("Diana") == 2 + # Charlie also appears at depth 2 (via Bob), dict stores last occurrence + assert names_at_depth.get("Charlie") == 2 def test_friend_recommendation_depth_2(self, postgres_backend, social_network_data): """Q2: Only depth=2 (simulate {2,2}).""" @@ -250,7 +250,7 @@ def test_friend_recommendation_depth_2(self, postgres_backend, social_network_da sql, params = with_query.to_sql() rows = postgres_backend.fetch_all(sql, params) names = [r["name"] for r in rows] - assert "Diana" in names + assert "Charlie" in names assert len(names) == 1 def test_exclude_start_node(self, postgres_backend, social_network_data): @@ -281,7 +281,7 @@ def test_exclude_start_node(self, postgres_backend, social_network_data): rows = postgres_backend.fetch_all(sql, params) names = [r["name"] for r in rows] assert "Alice" not in names - assert len(names) == 3 # Bob, Charlie, Diana + assert len(names) == 3 # Bob, Charlie (depth=1), Charlie (depth=2) class TestAMLFundTracing: @@ -429,6 +429,9 @@ def test_fund_trace_aggregation(self, postgres_backend, aml_data): FunctionCall(dialect, "SUM", Column(dialect, "amount"), alias="total"), ], from_=TableExpression(dialect, "fund_trace"), + group_by_having=GroupByHavingClause( + dialect, group_by=[Column(dialect, "depth")] + ), order_by=OrderByClause(dialect, [Column(dialect, "depth")]), ) @@ -446,8 +449,8 @@ def test_fund_trace_aggregation(self, postgres_backend, aml_data): assert totals.get(1) == 450000 # Depth 2: Charlie Ltd sent 600000 to Bob Corp assert totals.get(2) == 600000 - # Depth 3: Diana Offshore sent 500000 to Charlie Ltd - assert totals.get(3) == 500000 + # Depth 3: Diana Offshore (500000) + Eve Holding (300000) sent to Charlie Ltd + assert totals.get(3) == 800000 class TestAsyncRecursiveCTEGraph: From f75910a911c17f7f12a23eb7f48edcc19466ac05 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 19 Jul 2026 17:26:14 +0800 Subject: [PATCH 27/39] fix(test): track created tables per provider to eliminate xdist race condition in cleanup_after_test Instead of hardcoding tables_to_drop list in cleanup_after_test (which drops ALL tables including those used by concurrent xdist workers), track only tables that were actually created by _setup_{model,multiple} calls in each provider instance. Since each test fixture creates a fresh provider, this naturally isolates table cleanup to just the tables the current test created, eliminating the race where one test's teardown drops users/type_adapter_tests/pydantic_validated_models tables that another concurrent worker is still using. --- tests/providers/basic.py | 25 ++++++++++--------------- tests/providers/query.py | 19 +++++++++---------- tests/providers/relation.py | 3 ++- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 20843722..0889f99a 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -13,7 +13,7 @@ import os import sys import logging -from typing import Type, List, Tuple, Optional # noqa: F401 +from typing import Type, List, Tuple, Optional, Set # noqa: F401 logger = logging.getLogger(__name__) @@ -187,6 +187,7 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s class BasicProviderBase: def __init__(self): self._scenario_db_files = {} + self._created_tables: Set[str] = set() def get_test_scenarios(self) -> List[str]: return list(get_enabled_scenarios().keys()) @@ -219,6 +220,7 @@ def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, tabl backend_instance = model_class.__backend__ self._track_backend(backend_instance, self._active_backends) self._reset_table_sync(model_class, table_name) + self._created_tables.add(table_name) return model_class def _reset_table_sync(self, model_class: Type[ActiveRecord], table_name: str) -> None: @@ -249,6 +251,7 @@ def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], s model_class.__backend__ = shared_backend self._track_backend(shared_backend, self._active_backends) self._initialize_model_schema(model_class, table_name) + self._created_tables.add(table_name) result.append(model_class) return tuple(result) @@ -349,15 +352,9 @@ def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: return self._load_postgres_schema(f'{table_name}.sql') def cleanup_after_test(self, scenario_name: str): - tables_to_drop = [ - 'users', 'type_cases', 'type_tests', 'validated_field_users', - 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', - 'bulk_users', - 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' - ] for backend_instance in self._active_backends: try: - for table_name in tables_to_drop: + for table_name in list(self._created_tables): try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: @@ -368,6 +365,7 @@ def cleanup_after_test(self, scenario_name: str): except Exception: pass self._active_backends.clear() + self._created_tables.clear() class BasicAsyncProvider(BasicProviderBase, IBasicAsyncProvider): @@ -383,6 +381,7 @@ async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_nam backend_instance = model_class.__backend__ self._track_backend(backend_instance, self._active_async_backends) await self._reset_table_async(model_class, table_name) + self._created_tables.add(table_name) return model_class async def _reset_table_async(self, model_class: Type[ActiveRecord], table_name: str) -> None: @@ -413,6 +412,7 @@ async def _setup_multiple_models_async(self, model_classes: List[Tuple[Type[Acti model_class.__backend__ = shared_backend self._track_backend(shared_backend, self._active_async_backends) await self._initialize_async_model_schema(model_class, table_name) + self._created_tables.add(table_name) result.append(model_class) return tuple(result) @@ -476,15 +476,9 @@ async def setup_product_with_column_and_adapter_model(self, scenario_name: str) raise NotImplementedError async def cleanup_after_test(self, scenario_name: str): - tables_to_drop = [ - 'users', 'type_cases', 'type_tests', 'validated_field_users', - 'validated_users', 'pydantic_validated_models', 'type_adapter_tests', - 'bulk_users', - 'posts', 'comments', 'column_mapping_items', 'mixed_annotation_items' - ] for backend_instance in self._active_async_backends: try: - for table_name in tables_to_drop: + for table_name in list(self._created_tables): try: await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: @@ -495,3 +489,4 @@ async def cleanup_after_test(self, scenario_name: str): except Exception: pass self._active_async_backends.clear() + self._created_tables.clear() diff --git a/tests/providers/query.py b/tests/providers/query.py index 23a723bc..2bf80f61 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -125,6 +125,7 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s class QueryProviderBase: def __init__(self): self._scenario_db_files = {} + self._created_tables: Set[str] = set() def get_test_scenarios(self) -> List[str]: return list(get_enabled_scenarios().keys()) @@ -168,6 +169,7 @@ def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, tabl create_expr = fn(backend_instance.dialect, table_name) sql, params = create_expr.to_sql() backend_instance.execute(sql, params, options=opts) + self._created_tables.add(table_name) return model_class def _setup_multiple_models(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: @@ -273,6 +275,7 @@ def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: create_expr = fn(backend_instance.dialect, "order_items") sql, params = create_expr.to_sql() backend_instance.execute(sql, params, options=opts) + self._created_tables.add("order_items") return CompositeOrderItemBase def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: @@ -316,13 +319,9 @@ def get_worker_schema_sql(self, scenario_name: str, table_name: str) -> str: return self._load_postgres_schema(f'{table_name}.sql') def cleanup_after_test(self, scenario_name: str): - tables_to_drop = [ - 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items', 'profiles' - ] for backend_instance in self._active_backends: try: - for table_name in tables_to_drop: + for table_name in list(self._created_tables): try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: @@ -333,6 +332,7 @@ def cleanup_after_test(self, scenario_name: str): except Exception: pass self._active_backends.clear() + self._created_tables.clear() class QueryAsyncProvider(QueryProviderBase, IQueryAsyncProvider): @@ -362,6 +362,7 @@ async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_nam create_expr = fn(backend_instance.dialect, table_name) sql, params = create_expr.to_sql() await backend_instance.execute(sql, params, options=opts) + self._created_tables.add(table_name) return model_class async def _setup_multiple_models_async(self, models_and_tables: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: @@ -472,16 +473,13 @@ async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRe create_expr = fn(backend_instance.dialect, "order_items") sql, params = create_expr.to_sql() await backend_instance.execute(sql, params, options=opts) + self._created_tables.add("order_items") return AsyncCompositeOrderItemBase async def cleanup_after_test(self, scenario_name: str): - tables_to_drop = [ - 'users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', - 'extended_orders', 'extended_order_items', 'searchable_items', 'profiles' - ] for backend_instance in self._active_async_backends: try: - for table_name in tables_to_drop: + for table_name in list(self._created_tables): try: await backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: @@ -492,3 +490,4 @@ async def cleanup_after_test(self, scenario_name: str): except Exception: pass self._active_async_backends.clear() + self._created_tables.clear() diff --git a/tests/providers/relation.py b/tests/providers/relation.py index e81bf16a..d556166c 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -1,6 +1,6 @@ # tests/providers/relation.py import asyncio -from typing import Dict, List, Tuple, Type +from typing import Dict, List, Tuple, Type, Set from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend @@ -110,6 +110,7 @@ def __init__(self): self._async_user_post_comment_setup = False self._sync_relation_boundary_setup = False self._async_relation_boundary_setup = False + self._created_tables: Set[str] = set() def get_test_scenarios(self) -> List[str]: return list(get_enabled_scenarios().keys()) From 3a9fdb2ac545a9785d32ae9a1b8099a97c0d34aa Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 24 Jul 2026 19:46:43 +0800 Subject: [PATCH 28/39] feat(postgres): set supports_json_arrow_operators=True --- src/rhosocial/activerecord/backend/impl/postgres/dialect.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 8a681cb0..c2127666 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -694,6 +694,10 @@ def supports_for_update(self) -> bool: """ return True + def supports_json_arrow_operators(self) -> bool: + """PostgreSQL supports -> and ->> operators for JSON/JSONB access.""" + return True + def supports_lock_strength(self, strength) -> bool: """ Check if a specific lock strength is supported. From 86648e6b3101631ae198c4b5cce7cc4aba44e5fd Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 31 Jul 2026 14:04:32 +0800 Subject: [PATCH 29/39] feat(postgres): add ON CONFLICT capability switches PostgreSQL supports only a single ON CONFLICT clause per INSERT. Explicitly declare supports_on_conflict_clause=True and supports_multiple_on_conflict_clauses=False to enable the generic gate in DMLMixin.format_on_conflict_clauses. Add targeted tests for the capability switches and single-clause rendering (incl. EXCLUDED). --- .../backend/impl/postgres/mixins/upsert.py | 7 ++ .../test_insert_on_conflict_clauses.py | 98 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py index b5c4140b..31612d0e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py @@ -10,3 +10,10 @@ class PostgresUpsertMixin: def supports_upsert(self) -> bool: return self.version >= (9, 5, 0) + + def supports_on_conflict_clause(self) -> bool: + return True + + def supports_multiple_on_conflict_clauses(self) -> bool: + """PostgreSQL grammar allows only a single ON CONFLICT clause per INSERT.""" + return False diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py new file mode 100644 index 00000000..abd8c6b1 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py @@ -0,0 +1,98 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py +"""Tests for PostgreSQL ON CONFLICT clause capability and rendering. + +Covers: +- Capability switches: single clause supported, multiple clauses rejected. +- Single ON CONFLICT DO NOTHING / DO UPDATE rendering (incl. EXCLUDED). +- Multiple ON CONFLICT clauses rejected by the generic gate. +""" + +import pytest + +from rhosocial.activerecord.backend.dialect import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression import ( + Column, + InsertExpression, + Literal, + OnConflictClause, + ValuesSource, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(15, 0, 0)) + + +class TestPostgresOnConflictCapabilities: + """Capability switch tests.""" + + def test_supports_on_conflict_clause(self, dialect): + assert dialect.supports_on_conflict_clause() is True + + def test_does_not_support_multiple_on_conflict_clauses(self, dialect): + assert dialect.supports_multiple_on_conflict_clauses() is False + + def test_multiple_on_conflict_clauses_rejected(self, dialect): + """PostgreSQL grammar allows only one ON CONFLICT clause per INSERT.""" + source = ValuesSource(dialect, values_list=[[Literal(dialect, 1)]]) + clause1 = OnConflictClause(dialect, conflict_target=["col_a"], do_nothing=True) + clause2 = OnConflictClause(dialect, conflict_target=["col_b"], do_nothing=True) + expr = InsertExpression(dialect, into="t", source=source, on_conflict=[clause1, clause2]) + + with pytest.raises(UnsupportedFeatureError, match="multiple ON CONFLICT clauses"): + expr.to_sql() + + +class TestPostgresOnConflictRendering: + """SQL rendering tests for a single ON CONFLICT clause.""" + + def test_do_nothing(self, dialect): + source = ValuesSource(dialect, values_list=[[Literal(dialect, 1)]]) + clause = OnConflictClause(dialect, conflict_target=["id"], do_nothing=True) + expr = InsertExpression(dialect, into="users", columns=["id"], source=source, on_conflict=clause) + sql, params = expr.to_sql() + assert sql == 'INSERT INTO "users" ("id") VALUES (%s) ON CONFLICT ("id") DO NOTHING' + assert params == (1,) + + def test_do_update_with_excluded(self, dialect): + """EXCLUDED pseudo-table must not be double-quoted in PostgreSQL.""" + source = ValuesSource( + dialect, values_list=[[Literal(dialect, 1), Literal(dialect, "new_name")]] + ) + clause = OnConflictClause( + dialect, + conflict_target=["id"], + update_assignments={"name": Column(dialect, "name", "EXCLUDED")}, + ) + expr = InsertExpression( + dialect, into="users", columns=["id", "name"], source=source, on_conflict=clause + ) + sql, params = expr.to_sql() + assert sql == ( + 'INSERT INTO "users" ("id", "name") VALUES (%s, %s) ' + 'ON CONFLICT ("id") DO UPDATE SET "name" = EXCLUDED."name"' + ) + assert params == (1, "new_name") + + def test_do_update_with_where(self, dialect): + source = ValuesSource( + dialect, values_list=[[Literal(dialect, 1), Literal(dialect, 10)]] + ) + clause = OnConflictClause( + dialect, + conflict_target=["id"], + update_assignments={"qty": Column(dialect, "qty", "EXCLUDED")}, + update_where=Column(dialect, "qty", "users") > Column(dialect, "qty", "EXCLUDED"), + ) + expr = InsertExpression( + dialect, into="users", columns=["id", "qty"], source=source, on_conflict=clause + ) + sql, params = expr.to_sql() + assert sql == ( + 'INSERT INTO "users" ("id", "qty") VALUES (%s, %s) ' + 'ON CONFLICT ("id") DO UPDATE SET "qty" = EXCLUDED."qty" ' + 'WHERE "users"."qty" > "EXCLUDED"."qty"' + ) + assert params == (1, 10) From 06f5e4bc8f0849751f24de0c975d57f419de5bda Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 31 Jul 2026 22:13:18 +0800 Subject: [PATCH 30/39] test(postgres): add DROP TABLE CASCADE/RESTRICT rendering test Confirm PostgreSQL inherits the correct optimistic True/True defaults from the core TableMixin and emits standard CASCADE/RESTRICT tokens. No code change required on the PostgreSQL backend. --- .../dialect/test_drop_table_cascade.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py new file mode 100644 index 00000000..564abdfd --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py @@ -0,0 +1,53 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py +"""Tests for DROP TABLE ... CASCADE/RESTRICT rendering on PostgreSQL. + +PostgreSQL declares full SQL-standard CASCADE/RESTRICT support; the generic +``TableMixin.format_drop_table_statement`` helper (optimistic True/True) +emits the correct token without backend override. These tests pin that +contract so a future override that drops support is caught early. +""" + +import pytest + +from rhosocial.activerecord.backend.expression import DropTableExpression +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(15, 0, 0)) + + +class TestPostgresDropTableCascade: + def test_capability_switches(self, dialect): + assert dialect.supports_drop_table_cascade() is True + assert dialect.supports_drop_table_restrict() is True + + def test_cascade_renders_standard_token(self, dialect): + expr = DropTableExpression(dialect, table="users", cascade=True) + sql, params = expr.to_sql() + assert sql.endswith(" CASCADE") + assert "CASCADE CONSTRAINTS" not in sql + assert params == () + + def test_restrict_renders_standard_token(self, dialect): + expr = DropTableExpression(dialect, table="users", cascade=False) + sql, params = expr.to_sql() + assert sql.endswith(" RESTRICT") + assert params == () + + def test_cascade_none_omits_token(self, dialect): + expr = DropTableExpression(dialect, table="users", cascade=None) + sql, params = expr.to_sql() + assert "CASCADE" not in sql + assert "RESTRICT" not in sql + assert params == () + + def test_if_exists_combined_with_cascade(self, dialect): + expr = DropTableExpression( + dialect, table="users", if_exists=True, cascade=True + ) + sql, params = expr.to_sql() + assert sql.startswith("DROP TABLE IF EXISTS") + assert sql.endswith(" CASCADE") + assert params == () From e0976b4ee4ec00708a83fd4312443b3ad367d58e Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 18:57:11 +0800 Subject: [PATCH 31/39] fix(postgres): add missing supports_on_conflict_clause and supports_multiple_on_conflict_clauses to PostgresUpsertSupport protocol --- .../activerecord/backend/impl/postgres/protocols/upsert.py | 6 +++++- .../basic/connection/test_pool_transaction_paths.py | 7 +++++++ .../feature/basic/transaction/conftest.py | 6 ++++++ .../feature/basic/transaction/test_async_owner_task.py | 6 ++++++ .../feature/basic/transaction/test_savepoint_api.py | 7 +++++++ 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py index a6e144e4..a2ca5175 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/upsert.py @@ -8,4 +8,8 @@ class PostgresUpsertSupport(Protocol): """PostgreSQL upsert feature support protocol.""" - def supports_upsert(self)-> bool: ... + def supports_upsert(self) -> bool: ... + + def supports_on_conflict_clause(self) -> bool: ... + + def supports_multiple_on_conflict_clauses(self) -> bool: ... diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py new file mode 100644 index 00000000..449c6468 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py +""" +Pool.transaction() dispatch branch contracts for PostgreSQL backend. +""" + +from rhosocial.activerecord.testsuite.feature.basic.connection.test_pool_transaction_paths import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.connection.test_pool_transaction_paths_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py new file mode 100644 index 00000000..268b1f97 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py +""" +Pytest configuration for basic transaction feature tests for PostgreSQL backend. +""" + +from rhosocial.activerecord.testsuite.feature.basic.transaction.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py new file mode 100644 index 00000000..d52947fd --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py @@ -0,0 +1,6 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py +""" +Async contracts for AsyncTransactionManager._owner_task safety warning (PostgreSQL backend). +""" + +from rhosocial.activerecord.testsuite.feature.basic.transaction.test_async_owner_task import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py new file mode 100644 index 00000000..047ef77c --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py @@ -0,0 +1,7 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py +""" +Black-box contracts for savepoint APIs (PostgreSQL backend). +""" + +from rhosocial.activerecord.testsuite.feature.basic.transaction.test_savepoint_api import * # noqa: F403 +from rhosocial.activerecord.testsuite.feature.basic.transaction.test_savepoint_api_async import * # noqa: F403 From f14064eb5be594e286d9df9deb4c15083069dc5a Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 19:37:50 +0800 Subject: [PATCH 32/39] fix(postgres): remove sync savepoint test import, only async tests apply --- .../feature/basic/transaction/test_savepoint_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py index 047ef77c..19dff4fb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py @@ -3,5 +3,4 @@ Black-box contracts for savepoint APIs (PostgreSQL backend). """ -from rhosocial.activerecord.testsuite.feature.basic.transaction.test_savepoint_api import * # noqa: F403 from rhosocial.activerecord.testsuite.feature.basic.transaction.test_savepoint_api_async import * # noqa: F403 From b05eb68c4ecb69e16efa0a248c0aa09cb8a31eeb Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 20:31:41 +0800 Subject: [PATCH 33/39] fix(postgres): pass scenario name to type_adapter_fixtures provider to avoid cross-scenario table conflicts --- .../feature/basic/conftest.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py index 92417f78..e1f0d38d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py @@ -5,5 +5,27 @@ This file imports fixtures from the corresponding testsuite, making them available to the tests in this directory. """ -from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F403 + +import pytest + +from rhosocial.activerecord.testsuite.core.registry import get_provider_registry +from rhosocial.activerecord.testsuite.feature.basic.conftest import ( # noqa: F401 + SCENARIO_PARAMS_SYNC, + type_adapter_fixtures as _original_type_adapter_fixtures, + async_type_adapter_fixtures as _original_async_type_adapter_fixtures, + PROVIDER_KEY_SYNC, + PROVIDER_KEY_ASYNC, +) + +# Override type_adapter_fixtures to pass scenario name to provider, +# ensuring each scenario uses its own database instance. +@pytest.fixture(scope="function", params=SCENARIO_PARAMS_SYNC) +def type_adapter_fixtures(request): + scenario = request.param + provider_registry = get_provider_registry() + provider_class = provider_registry.get_provider(PROVIDER_KEY_SYNC) + provider = provider_class() + model = provider.setup_type_adapter_model_and_schema(scenario) + yield model + provider.cleanup_after_test(scenario) From dc9bb0416d274d3ec8551539dc585874fb4ec8c5 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 21:00:19 +0800 Subject: [PATCH 34/39] fix(postgres): import all testsuite basic fixtures in conftest while overriding type_adapter_fixtures --- .../activerecord_postgres_test/feature/basic/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py index e1f0d38d..2b316b90 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py @@ -9,12 +9,10 @@ import pytest from rhosocial.activerecord.testsuite.core.registry import get_provider_registry +from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F401,F403 from rhosocial.activerecord.testsuite.feature.basic.conftest import ( # noqa: F401 SCENARIO_PARAMS_SYNC, - type_adapter_fixtures as _original_type_adapter_fixtures, - async_type_adapter_fixtures as _original_async_type_adapter_fixtures, PROVIDER_KEY_SYNC, - PROVIDER_KEY_ASYNC, ) # Override type_adapter_fixtures to pass scenario name to provider, From f12f97c07d8ba0513200dbe2620d9280c0c9d5ac Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 21:13:06 +0800 Subject: [PATCH 35/39] fix(postgres): revert conftest override, restore import * to avoid breaking other fixtures --- .../feature/basic/conftest.py | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py index 2b316b90..92417f78 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py @@ -5,25 +5,5 @@ This file imports fixtures from the corresponding testsuite, making them available to the tests in this directory. """ - -import pytest - -from rhosocial.activerecord.testsuite.core.registry import get_provider_registry -from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F401,F403 -from rhosocial.activerecord.testsuite.feature.basic.conftest import ( # noqa: F401 - SCENARIO_PARAMS_SYNC, - PROVIDER_KEY_SYNC, -) - -# Override type_adapter_fixtures to pass scenario name to provider, -# ensuring each scenario uses its own database instance. -@pytest.fixture(scope="function", params=SCENARIO_PARAMS_SYNC) -def type_adapter_fixtures(request): - scenario = request.param - provider_registry = get_provider_registry() - provider_class = provider_registry.get_provider(PROVIDER_KEY_SYNC) - provider = provider_class() - model = provider.setup_type_adapter_model_and_schema(scenario) - yield model - provider.cleanup_after_test(scenario) +from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F403 From 67f2b2f1bf78cec588d64f978ef19e321d7ac19c Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 21:17:02 +0800 Subject: [PATCH 36/39] fix(postgres): pass scenario name to sync type_adapter_fixtures to avoid cross-scenario table collisions --- .../feature/basic/conftest.py | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py index 92417f78..1f6cb9a6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/conftest.py @@ -3,7 +3,28 @@ Pytest configuration for basic feature tests. This file imports fixtures from the corresponding testsuite, making them -available to the tests in this directory. +available to the tests in this directory. It overrides ``type_adapter_fixtures`` +so the scenario name is passed to the provider. Without this, the sync provider +always creates the ``type_adapter_tests`` table on the first registered scenario, +which collides with the async provider when both scenarios run in parallel. """ -from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F403 +import pytest + +from rhosocial.activerecord.testsuite.feature.basic.conftest import * # noqa: F401,F403 +from rhosocial.activerecord.testsuite.feature.basic.conftest import ( # noqa: F401 + SCENARIO_PARAMS_SYNC, + PROVIDER_KEY_SYNC, +) +from rhosocial.activerecord.testsuite.core.registry import get_provider_registry + + +@pytest.fixture(scope="function", params=SCENARIO_PARAMS_SYNC) +def type_adapter_fixtures(request): + scenario = request.param + provider_registry = get_provider_registry() + provider_class = provider_registry.get_provider(PROVIDER_KEY_SYNC) + provider = provider_class() + model = provider.setup_type_adapter_model_and_schema(scenario) + yield model + provider.cleanup_after_test(scenario) From 2806f8d8de67dc94b9bf72a04e1e00af4a1b714e Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 21:29:47 +0800 Subject: [PATCH 37/39] fix(postgres): remove test bridge files accidentally added in protocol fix --- .../basic/connection/test_pool_transaction_paths.py | 7 ------- .../feature/basic/transaction/conftest.py | 6 ------ .../feature/basic/transaction/test_async_owner_task.py | 6 ------ .../feature/basic/transaction/test_savepoint_api.py | 6 ------ 4 files changed, 25 deletions(-) delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py delete mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py deleted file mode 100644 index 449c6468..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py +++ /dev/null @@ -1,7 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/basic/connection/test_pool_transaction_paths.py -""" -Pool.transaction() dispatch branch contracts for PostgreSQL backend. -""" - -from rhosocial.activerecord.testsuite.feature.basic.connection.test_pool_transaction_paths import * # noqa: F403 -from rhosocial.activerecord.testsuite.feature.basic.connection.test_pool_transaction_paths_async import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py deleted file mode 100644 index 268b1f97..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/conftest.py -""" -Pytest configuration for basic transaction feature tests for PostgreSQL backend. -""" - -from rhosocial.activerecord.testsuite.feature.basic.transaction.conftest import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py deleted file mode 100644 index d52947fd..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py +++ /dev/null @@ -1,6 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_async_owner_task.py -""" -Async contracts for AsyncTransactionManager._owner_task safety warning (PostgreSQL backend). -""" - -from rhosocial.activerecord.testsuite.feature.basic.transaction.test_async_owner_task import * # noqa: F403 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py deleted file mode 100644 index 19dff4fb..00000000 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py +++ /dev/null @@ -1,6 +0,0 @@ -# tests/rhosocial/activerecord_postgres_test/feature/basic/transaction/test_savepoint_api.py -""" -Black-box contracts for savepoint APIs (PostgreSQL backend). -""" - -from rhosocial.activerecord.testsuite.feature.basic.transaction.test_savepoint_api_async import * # noqa: F403 From fc8b6a6d13bff3a836119ca39fb1fdceb472f147 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 22:05:11 +0800 Subject: [PATCH 38/39] fix(postgres): remove shadowing fixture import from type_adapter bridge file The bridge file imported type_adapter_fixtures directly from the testsuite conftest into the test module namespace. pytest registered that fixture with the test module as its most specific baseid, shadowing the conftest override (feature/basic/conftest.py) that passes the scenario name to the provider. As a result, the sync [postgres_exts] type_adapter tests connected to the default scenario database (5432) instead of postgres_exts (5433), colliding with async [postgres_default] tests on the same type_adapter_tests table and causing 'relation does not exist' failures under xdist concurrency. Remove the import so pytest resolves the conftest override, which correctly passes the scenario name. --- .../feature/basic/type_adapter/test_type_adapter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py index c607a556..d6108e4f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/type_adapter/test_type_adapter.py @@ -6,11 +6,11 @@ package and make them discoverable by `pytest` within this project's test run. """ -# Import the fixture that provides the configured model and backend for type adapter tests. -from rhosocial.activerecord.testsuite.feature.basic.conftest import ( - type_adapter_fixtures, # noqa: F401 - async_type_adapter_fixtures, # noqa: F401 -) +# Note: fixtures are provided by the conftest override at +# feature/basic/conftest.py (which passes the scenario name to the provider). +# Do NOT re-import type_adapter_fixtures here -- pytest would register the +# testsuite's original (scenario-ignoring) fixture with this module as the +# most specific baseid, shadowing the conftest override. # Import all tests from the generic testsuite file. from rhosocial.activerecord.testsuite.feature.basic.type_adapter.test_type_adapter import * # noqa: F403 From 5a13ac25aa7ff9572f669918fd79a3aade78371b Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 1 Aug 2026 23:31:49 +0800 Subject: [PATCH 39/39] test(postgres): add unit tests for data type formatting and parsing - Fix relative imports in parse_type (..expression.types -> ...expression.types) that crashed serial/binary/jsonpath/uuid/net/geom/range/oid/ts/vector parsing with ModuleNotFoundError. - Fix DATETIME mis-parsed as DATE (dispatch order), and CHARACTER/ CHARACTER VARYING/TIMESTAMPTZ/TIMETZ falling back to CustomType due to \b boundary regex. - Add test_data_type_formatting.py covering all PostgreSQL type format and parse branches (data_type_formatting.py coverage 55.67% -> 98%). - Add test_expression_types.py covering synonyms/equality/hash/array equivalence (expression/types.py coverage 76.19% -> 100%). - Cover index fulltext PHRASE mode + multi-column, and upsert capability switches (index.py and upsert.py -> 100%). --- .../mixins/types/data_type_formatting.py | 38 +- .../test_insert_on_conflict_clauses.py | 12 + .../feature/backend/postgres/test_index_pg.py | 24 + .../types/test_data_type_formatting.py | 435 ++++++++++++++++++ .../backend/types/test_expression_types.py | 104 +++++ 5 files changed, 594 insertions(+), 19 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index bb41a98e..73c155f6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from typing import Tuple, Tuple, TYPE_CHECKING +from typing import Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport @@ -471,7 +471,7 @@ def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: re.IGNORECASE, ) _PG_STRING_TYPES = re.compile( - r"^(?:CHAR|VARCHAR|TEXT)\b", + r"^(?:CHARACTER|CHAR|VARCHAR|TEXT)\b", re.IGNORECASE, ) _PG_BINARY_TYPES = re.compile( @@ -479,7 +479,7 @@ def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: re.IGNORECASE, ) _PG_DATE_TYPES = re.compile( - r"^(?:DATE|DATETIME|TIMESTAMP|TIME|INTERVAL)\b", + r"^(?:DATETIME|DATE|TIMESTAMPTZ|TIMESTAMP|TIMETZ|TIME|INTERVAL)\b", re.IGNORECASE, ) _PG_JSON_TYPES = re.compile( @@ -560,7 +560,7 @@ def parse_type(self, raw: str) -> DataType: # Serial family if self._PG_SERIAL_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresBigSerialType, PostgresSerialType, PostgresSmallSerialType, @@ -610,15 +610,15 @@ def parse_type(self, raw: str) -> DataType: # Binary if self._PG_BINARY_TYPES.match(upper): - from ..expression.types import PostgresByteaType + from ...expression.types import PostgresByteaType return PostgresByteaType() # Date/time if self._PG_DATE_TYPES.match(upper): - if upper.startswith("DATE"): - return DateType() if upper.startswith("DATETIME"): return DateTimeType() + if upper.startswith("DATE"): + return DateType() if upper.startswith("TIMESTAMP"): nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None @@ -641,13 +641,13 @@ def parse_type(self, raw: str) -> DataType: if upper.startswith("JSONB"): return JsonBType() if upper.startswith("JSONPATH"): - from ..expression.types import PostgresJsonPathType + from ...expression.types import PostgresJsonPathType return PostgresJsonPathType() return JsonType() # UUID if self._PG_UUID_TYPES.match(upper): - from ..expression.types import PostgresUUIDType + from ...expression.types import PostgresUUIDType return PostgresUUIDType() # Boolean @@ -659,14 +659,14 @@ def parse_type(self, raw: str) -> DataType: nums = re.findall(r"\d+", stripped) n = int(nums[0]) if nums else None if upper.startswith("BIT"): - from ..expression.types import PostgresBitType + from ...expression.types import PostgresBitType return PostgresBitType(n) - from ..expression.types import PostgresVarBitType + from ...expression.types import PostgresVarBitType return PostgresVarBitType(n) # Network address if self._PG_NET_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresCidrType, PostgresInetType, PostgresMacAddr8Type, @@ -682,7 +682,7 @@ def parse_type(self, raw: str) -> DataType: # Geometric if self._PG_GEOM_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresBoxType, PostgresCircleType, PostgresLineSegmentType, @@ -707,7 +707,7 @@ def parse_type(self, raw: str) -> DataType: # Range types if self._PG_RANGE_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresDateRangeType, PostgresInt4RangeType, PostgresInt8RangeType, @@ -730,7 +730,7 @@ def parse_type(self, raw: str) -> DataType: # Multirange types if self._PG_MULTIRANGE_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresDateMultirangeType, PostgresInt4MultirangeType, PostgresInt8MultirangeType, @@ -753,7 +753,7 @@ def parse_type(self, raw: str) -> DataType: # OID types if self._PG_OID_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresCIDType, PostgresOIDType, PostgresRegClassType, @@ -778,7 +778,7 @@ def parse_type(self, raw: str) -> DataType: # Text search if self._PG_TS_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresTSQueryType, PostgresTSVectorType, ) @@ -788,7 +788,7 @@ def parse_type(self, raw: str) -> DataType: # Miscellaneous if self._PG_MISC_TYPES.match(upper): - from ..expression.types import ( + from ...expression.types import ( PostgresGeographyType, PostgresGeometryType, PostgresHstoreType, @@ -813,7 +813,7 @@ def parse_type(self, raw: str) -> DataType: if upper.startswith("VECTOR"): nums = re.findall(r"\d+", stripped) dim = int(nums[0]) if nums else 0 - from ..expression.types import PostgresVectorType + from ...expression.types import PostgresVectorType return PostgresVectorType(dim) # Fallback diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py index abd8c6b1..fb83bdf1 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py @@ -28,9 +28,21 @@ def dialect(): class TestPostgresOnConflictCapabilities: """Capability switch tests.""" + def test_supports_upsert_pg94(self): + assert PostgresDialect((9, 4, 0)).supports_upsert() is False + + def test_supports_upsert_pg95(self): + assert PostgresDialect((9, 5, 0)).supports_upsert() is True + + def test_supports_upsert_modern(self): + assert PostgresDialect((16, 0, 0)).supports_upsert() is True + def test_supports_on_conflict_clause(self, dialect): assert dialect.supports_on_conflict_clause() is True + def test_supports_on_conflict_clause_old_version(self): + assert PostgresDialect((9, 4, 0)).supports_on_conflict_clause() is True + def test_does_not_support_multiple_on_conflict_clauses(self, dialect): assert dialect.supports_multiple_on_conflict_clauses() is False diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py index 6be4882a..40fc7ee7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py @@ -518,6 +518,17 @@ def test_format_fulltext_match_boolean(self): assert "plainto_tsquery" not in sql assert params == ("a & b",) + def test_format_fulltext_match_phrase(self): + sql, params = PostgresDialect().format_fulltext_match(["col"], "cat dog", mode="PHRASE") + assert "phraseto_tsquery" in sql + assert "plainto_tsquery" not in sql + assert params == ("cat dog",) + + def test_format_fulltext_match_unknown_mode_uses_plain(self): + sql, params = PostgresDialect().format_fulltext_match(["col"], "search", mode="CUSTOM") + assert "plainto_tsquery" in sql + assert params == ("search",) + def test_format_fulltext_match_multi_column(self): sql, params = PostgresDialect().format_fulltext_match(["a", "b"], "search") assert " || " in sql @@ -551,6 +562,19 @@ def test_format_create_fulltext_index_statement_if_not_exists(self): sql, _ = dialect.format_create_fulltext_index_statement(expr) assert "IF NOT EXISTS" in sql + def test_format_create_fulltext_index_statement_multi_column(self): + dialect = PostgresDialect() + expr = CreateFulltextIndexExpression( + dialect, + index_name="idx_ft", + table_name="articles", + columns=["title", "body"], + ) + sql, _ = dialect.format_create_fulltext_index_statement(expr) + assert " || " in sql + assert "to_tsvector" in sql + assert "title" in sql and "body" in sql + def test_format_drop_fulltext_index_statement(self): dialect = PostgresDialect() expr = DropFulltextIndexExpression( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py new file mode 100644 index 00000000..c7c286f9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py @@ -0,0 +1,435 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py +"""Pure-function tests for PostgreSQL DataType formatting and parsing. + +These tests exercise ``PostgresTypeFormatSupportMixin`` (the +``PostgresDialect.format_data_type`` and ``PostgresDialect.parse_type`` +paths) without requiring a live PostgreSQL server. +""" + +import pytest + +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + BigIntType, + BlobType, + BooleanType, + CharType, + CustomType, + DateType, + DateTimeType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + IntType, + IntervalType, + JsonBType, + JsonType, + RealType, + SmallIntType, + TextType, + TimeType, + TimeTzType, + TimestampType, + TimestampTzType, + TinyIntType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCharacterVaryingType, + PostgresCidrType, + PostgresCircleType, + PostgresCitextType, + PostgresCubeType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresLtreeType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRasterType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, +) + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(15, 0, 0)) + + +# --------------------------------------------------------------------------- +# format_data_type — PostgreSQL-specific types +# --------------------------------------------------------------------------- + +FORMAT_CASES = [ + (PostgresByteaType(), "BYTEA"), + (PostgresSmallSerialType(), "SMALLSERIAL"), + (PostgresSerialType(), "SERIAL"), + (PostgresBigSerialType(), "BIGSERIAL"), + (PostgresUUIDType(), "UUID"), + (PostgresXMLType(), "XML"), + (PostgresTSVectorType(), "TSVECTOR"), + (PostgresTSQueryType(), "TSQUERY"), + (PostgresJsonPathType(), "JSONPATH"), + (PostgresBitType(), "BIT"), + (PostgresBitType(n=8), "BIT(8)"), + (PostgresVarBitType(), "VARBIT"), + (PostgresVarBitType(n=16), "VARBIT(16)"), + (PostgresInetType(), "INET"), + (PostgresCidrType(), "CIDR"), + (PostgresMacAddrType(), "MACADDR"), + (PostgresMacAddr8Type(), "MACADDR8"), + (PostgresPointType(), "POINT"), + (PostgresLineType(), "LINE"), + (PostgresLineSegmentType(), "LSEG"), + (PostgresBoxType(), "BOX"), + (PostgresPathType(), "PATH"), + (PostgresPolygonType(), "POLYGON"), + (PostgresCircleType(), "CIRCLE"), + (PostgresMoneyType(), "MONEY"), + (PostgresInt4RangeType(), "INT4RANGE"), + (PostgresInt8RangeType(), "INT8RANGE"), + (PostgresNumRangeType(), "NUMRANGE"), + (PostgresTsRangeType(), "TSRANGE"), + (PostgresTsTzRangeType(), "TSTZRANGE"), + (PostgresDateRangeType(), "DATERANGE"), + (PostgresInt4MultirangeType(), "INT4MULTIRANGE"), + (PostgresInt8MultirangeType(), "INT8MULTIRANGE"), + (PostgresNumMultirangeType(), "NUMMULTIRANGE"), + (PostgresTsMultirangeType(), "TSMULTIRANGE"), + (PostgresTsTzMultirangeType(), "TSTZMULTIRANGE"), + (PostgresDateMultirangeType(), "DATEMULTIRANGE"), + (PostgresOIDType(), "OID"), + (PostgresRegClassType(), "REGCLASS"), + (PostgresRegTypeType(), "REGTYPE"), + (PostgresXIDType(), "XID"), + (PostgresXID8Type(), "XID8"), + (PostgresCIDType(), "CID"), + (PostgresTIDType(), "TID"), + (PostgresPgLSNType(), "PG_LSN"), + (PostgresHstoreType(), "HSTORE"), + (PostgresGeometryType(), "GEOMETRY"), + (PostgresGeographyType(), "GEOGRAPHY"), + (PostgresVectorType(dim=384), "VECTOR(384)"), + (PostgresCitextType(), "CITEXT"), + (PostgresCubeType(), "CUBE"), + (PostgresLtreeType(), "LTREE"), + (PostgresRasterType(), "RASTER"), + (PostgresCharacterVaryingType(), "CHARACTER VARYING"), + (PostgresCharacterVaryingType(length=100), "CHARACTER VARYING(100)"), +] + + +@pytest.mark.parametrize("data_type,expected", FORMAT_CASES) +def test_format_postgres_specific_types(dialect, data_type, expected): + sql, params = dialect.format_data_type(data_type) + assert sql == expected + assert params == () + + +# --------------------------------------------------------------------------- +# format_data_type — core types with PostgreSQL specialisation +# --------------------------------------------------------------------------- + +CORE_FORMAT_CASES = [ + (TinyIntType(), "SMALLINT"), + (SmallIntType(), "SMALLINT"), + (IntType(), "INTEGER"), + (IntegerType(), "INTEGER"), + (BigIntType(), "BIGINT"), + (FloatType(), "REAL"), + (FloatType(precision=8), "FLOAT(8)"), + (RealType(), "REAL"), + (DoubleType(), "DOUBLE PRECISION"), + (DecimalType(), "DECIMAL"), + (DecimalType(precision=10), "DECIMAL(10)"), + (DecimalType(precision=10, scale=2), "DECIMAL(10,2)"), + (CharType(), "CHAR"), + (CharType(length=5), "CHAR(5)"), + (VarCharType(), "VARCHAR"), + (VarCharType(length=50), "VARCHAR(50)"), + (TextType(), "TEXT"), + (BooleanType(), "BOOLEAN"), + (BlobType(), "BYTEA"), + (DateType(), "DATE"), + (TimeType(), "TIME"), + (TimeType(precision=6), "TIME(6)"), + (TimeTzType(), "TIME WITH TIME ZONE"), + (TimeTzType(precision=6), "TIME(6) WITH TIME ZONE"), + (DateTimeType(), "TIMESTAMP"), + (TimestampType(), "TIMESTAMP"), + (TimestampType(precision=3), "TIMESTAMP(3)"), + (TimestampTzType(), "TIMESTAMP WITH TIME ZONE"), + (TimestampTzType(precision=3), "TIMESTAMP(3) WITH TIME ZONE"), + (IntervalType(), "INTERVAL"), + (IntervalType(fields="DAY TO SECOND"), "INTERVAL DAY TO SECOND"), + (JsonType(), "JSON"), + (JsonBType(), "JSONB"), +] + + +@pytest.mark.parametrize("data_type,expected", CORE_FORMAT_CASES) +def test_format_core_types(dialect, data_type, expected): + sql, params = dialect.format_data_type(data_type) + assert sql == expected + assert params == () + + +def test_format_array_type(dialect): + sql, _ = dialect.format_data_type(ArrayType(IntegerType(), dimensions=1)) + assert sql == "INTEGER[]" + + sql, _ = dialect.format_data_type(ArrayType(VarCharType(10), dimensions=3)) + assert sql == "VARCHAR(10)[][][]" + + +def test_format_array_type_multidimensional(dialect): + sql, _ = dialect.format_data_type(ArrayType(IntegerType(), dimensions=2)) + assert sql == "INTEGER[][]" + + +def test_format_array_type_with_postgres_element(dialect): + sql, _ = dialect.format_data_type(ArrayType(PostgresUUIDType(), dimensions=1)) + assert sql == "UUID[]" + + +def test_format_array_type_unsupported_element_raises(dialect): + with pytest.raises(TypeError, match="is not supported"): + dialect.format_data_type(ArrayType(CustomType("X"), dimensions=1)) + + +def test_format_unregistered_type_raises(dialect): + with pytest.raises(TypeError, match="does not support"): + dialect.format_data_type(CustomType("X")) + + +# --------------------------------------------------------------------------- +# parse_type — round-trip families +# --------------------------------------------------------------------------- + +PARSE_CASES = [ + ("SMALLINT", SmallIntType, {}), + ("INT2", SmallIntType, {}), + ("INT", IntegerType, {}), + ("INTEGER", IntegerType, {}), + ("INT4", IntegerType, {}), + ("BIGINT", BigIntType, {}), + ("INT8", BigIntType, {}), + ("SMALLSERIAL", PostgresSmallSerialType, {}), + ("SERIAL2", PostgresSmallSerialType, {}), + ("SERIAL", PostgresSerialType, {}), + ("SERIAL4", PostgresSerialType, {}), + ("BIGSERIAL", PostgresBigSerialType, {}), + ("SERIAL8", PostgresBigSerialType, {}), + ("REAL", RealType, {}), + ("FLOAT4", RealType, {}), + ("DOUBLE", DoubleType, {}), + ("DOUBLE PRECISION", DoubleType, {}), + ("FLOAT", FloatType, {}), + ("FLOAT(8)", FloatType, {"precision": 8}), + ("DECIMAL", DecimalType, {}), + ("NUMERIC", DecimalType, {}), + ("DECIMAL(10)", DecimalType, {"precision": 10}), + ("DECIMAL(10,2)", DecimalType, {"precision": 10, "scale": 2}), + ("NUMERIC(12,4)", DecimalType, {"precision": 12, "scale": 4}), + ("CHAR", CharType, {}), + ("CHAR(5)", CharType, {"length": 5}), + ("CHARACTER", CharType, {}), + ("CHARACTER(5)", CharType, {"length": 5}), + ("VARCHAR", VarCharType, {}), + ("VARCHAR(50)", VarCharType, {"length": 50}), + ("CHARACTER VARYING", VarCharType, {}), + ("CHARACTER VARYING(100)", VarCharType, {"length": 100}), + ("TEXT", TextType, {}), + ("BYTEA", PostgresByteaType, {}), + ("BLOB", PostgresByteaType, {}), + ("DATE", DateType, {}), + ("DATETIME", DateTimeType, {}), + ("TIMESTAMP", TimestampType, {}), + ("TIMESTAMP(3)", TimestampType, {"precision": 3}), + ("TIMESTAMPTZ", TimestampTzType, {}), + ("TIMESTAMP(3) WITH TIME ZONE", TimestampTzType, {"precision": 3}), + ("TIME", TimeType, {}), + ("TIME(6)", TimeType, {"precision": 6}), + ("TIMETZ", TimeTzType, {}), + ("TIME(6) WITH TIME ZONE", TimeTzType, {"precision": 6}), + ("INTERVAL", IntervalType, {}), + ("INTERVAL DAY TO SECOND", IntervalType, {"fields": "DAY TO SECOND"}), + ("JSON", JsonType, {}), + ("JSONB", JsonBType, {}), + ("JSONPATH", PostgresJsonPathType, {}), + ("UUID", PostgresUUIDType, {}), + ("BOOLEAN", BooleanType, {}), + ("BOOL", BooleanType, {}), + ("BIT", PostgresBitType, {}), + ("BIT(8)", PostgresBitType, {"n": 8}), + ("VARBIT", PostgresVarBitType, {}), + ("VARBIT(16)", PostgresVarBitType, {"n": 16}), + ("INET", PostgresInetType, {}), + ("CIDR", PostgresCidrType, {}), + ("MACADDR", PostgresMacAddrType, {}), + ("MACADDR8", PostgresMacAddr8Type, {}), + ("POINT", PostgresPointType, {}), + ("LINE", PostgresLineType, {}), + ("LSEG", PostgresLineSegmentType, {}), + ("BOX", PostgresBoxType, {}), + ("PATH", PostgresPathType, {}), + ("POLYGON", PostgresPolygonType, {}), + ("CIRCLE", PostgresCircleType, {}), + ("INT4RANGE", PostgresInt4RangeType, {}), + ("INT8RANGE", PostgresInt8RangeType, {}), + ("NUMRANGE", PostgresNumRangeType, {}), + ("TSRANGE", PostgresTsRangeType, {}), + ("TSTZRANGE", PostgresTsTzRangeType, {}), + ("DATERANGE", PostgresDateRangeType, {}), + ("INT4MULTIRANGE", PostgresInt4MultirangeType, {}), + ("INT8MULTIRANGE", PostgresInt8MultirangeType, {}), + ("NUMMULTIRANGE", PostgresNumMultirangeType, {}), + ("TSMULTIRANGE", PostgresTsMultirangeType, {}), + ("TSTZMULTIRANGE", PostgresTsTzMultirangeType, {}), + ("DATEMULTIRANGE", PostgresDateMultirangeType, {}), + ("OID", PostgresOIDType, {}), + ("REGCLASS", PostgresRegClassType, {}), + ("REGTYPE", PostgresRegTypeType, {}), + ("XID", PostgresXIDType, {}), + ("XID8", PostgresXID8Type, {}), + ("CID", PostgresCIDType, {}), + ("TID", PostgresTIDType, {}), + ("TSVECTOR", PostgresTSVectorType, {}), + ("TSQUERY", PostgresTSQueryType, {}), + ("MONEY", PostgresMoneyType, {}), + ("XML", PostgresXMLType, {}), + ("PG_LSN", PostgresPgLSNType, {}), + ("HSTORE", PostgresHstoreType, {}), + ("GEOGRAPHY", PostgresGeographyType, {}), + ("GEOMETRY", PostgresGeometryType, {}), + ("VECTOR", PostgresVectorType, {"dim": 0}), + ("VECTOR(768)", PostgresVectorType, {"dim": 768}), +] + + +@pytest.mark.parametrize("raw,expected_cls,expected_attrs", PARSE_CASES) +def test_parse_type(dialect, raw, expected_cls, expected_attrs): + result = dialect.parse_type(raw) + assert type(result) is expected_cls + for attr, value in expected_attrs.items(): + assert getattr(result, attr) == value + + +def test_parse_type_array_bracket_suffix(dialect): + result = dialect.parse_type("INTEGER[]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + assert result.dimensions == 1 + + +def test_parse_type_array_multidimensional_brackets(dialect): + result = dialect.parse_type("INTEGER[][]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + + +def test_parse_type_array_with_dimension_bracket(dialect): + result = dialect.parse_type("VARCHAR(10)[3]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is VarCharType + assert result.element_type.length == 10 + + +def test_parse_type_malformed_bracket_suffix_falls_back(dialect): + result = dialect.parse_type("FOO[abc]") + assert type(result) is CustomType + + +def test_parse_type_array_keyword(dialect): + result = dialect.parse_type("INTEGER ARRAY") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + + +def test_parse_type_array_keyword_with_dimension(dialect): + result = dialect.parse_type("INTEGER ARRAY[3]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is IntegerType + + +def test_parse_type_array_nested_element(dialect): + result = dialect.parse_type("UUID[]") + assert type(result) is PostgresArrayType + assert type(result.element_type) is PostgresUUIDType + + +def test_parse_type_unknown_falls_back_to_custom(dialect): + result = dialect.parse_type("FOOBARBAZ") + assert type(result) is CustomType + assert result.raw == "FOOBARBAZ" + + +def test_parse_type_case_insensitive(dialect): + result = dialect.parse_type(" varchar(25) ") + assert type(result) is VarCharType + assert result.length == 25 + + +# --------------------------------------------------------------------------- +# supports_data_types +# --------------------------------------------------------------------------- + + +def test_supports_data_types_registers_all_postgres_types(dialect): + supported = dialect.supports_data_types() + assert isinstance(supported, list) + assert len(supported) > 0 + classes = {cls for cls, _ in supported} + for expected in ( + PostgresSerialType, + PostgresByteaType, + PostgresUUIDType, + PostgresVectorType, + PostgresTsRangeType, + ArrayType, + JsonBType, + IntegerType, + ): + assert expected in classes diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py new file mode 100644 index 00000000..e8a96295 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py @@ -0,0 +1,104 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py +"""Tests for PostgreSQL-specific DataType subclasses (pure, no DB). + +Covers the ``synonyms()`` registrations, equality/hash semantics and +array-type equivalence used by introspection and schema comparison. +""" + +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + PostgresBigSerialType, + PostgresBitType, + PostgresByteaType, + PostgresCharacterVaryingType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresVarBitType, + PostgresVectorType, +) + + +class TestPostgresTypeSynonyms: + def test_character_varying_synonyms(self): + assert PostgresCharacterVaryingType.synonyms() == {"VarCharType"} + + def test_bytea_synonyms(self): + assert PostgresByteaType.synonyms() == {"BlobType"} + + def test_small_serial_synonyms(self): + assert PostgresSmallSerialType.synonyms() == {"SmallSerialType", "SmallIntType"} + + def test_serial_synonyms(self): + assert PostgresSerialType.synonyms() == {"SerialType", "IntegerType"} + + def test_big_serial_synonyms(self): + assert PostgresBigSerialType.synonyms() == {"BigSerialType", "BigIntType"} + + +class TestPostgresBitTypeEquality: + def test_equal(self): + assert PostgresBitType(8) == PostgresBitType(8) + + def test_not_equal_values(self): + assert PostgresBitType(8) != PostgresBitType(16) + + def test_not_equal_types(self): + assert PostgresBitType(8) != PostgresVarBitType(8) + assert PostgresBitType(8) != "not a type" + + def test_none_vs_value(self): + assert PostgresBitType(None) != PostgresBitType(8) + + def test_hash(self): + assert hash(PostgresBitType(8)) == hash((type(PostgresBitType(8)), 8)) + + +class TestPostgresVarBitTypeEquality: + def test_equal(self): + assert PostgresVarBitType(16) == PostgresVarBitType(16) + + def test_not_equal_values(self): + assert PostgresVarBitType(16) != PostgresVarBitType(32) + + def test_not_equal_types(self): + assert PostgresVarBitType(16) != PostgresBitType(16) + assert PostgresVarBitType(16) != object() + + def test_hash(self): + assert hash(PostgresVarBitType(16)) == hash((type(PostgresVarBitType(16)), 16)) + + +class TestPostgresVectorTypeEquality: + def test_equal(self): + assert PostgresVectorType(384) == PostgresVectorType(384) + + def test_not_equal_values(self): + assert PostgresVectorType(384) != PostgresVectorType(768) + + def test_not_equal_types(self): + assert PostgresVectorType(384) != PostgresBitType(8) + assert PostgresVectorType(384) != "vector" + + def test_hash(self): + assert hash(PostgresVectorType(384)) == hash((type(PostgresVectorType(384)), 384)) + + +class TestPostgresArrayType: + def test_is_equivalent_matching_element(self): + arr1 = PostgresArrayType(IntegerType(), dimensions=1) + arr2 = PostgresArrayType(IntegerType(), dimensions=3) + assert arr1.is_equivalent(arr2) + + def test_is_equivalent_non_array(self): + arr = PostgresArrayType(IntegerType(), dimensions=1) + assert arr.is_equivalent(IntegerType()) is False + assert arr.is_equivalent(None) is False + + def test_is_equivalent_different_element(self): + arr1 = PostgresArrayType(IntegerType(), dimensions=1) + arr2 = PostgresArrayType(VarCharType(), dimensions=1) + assert arr1.is_equivalent(arr2) is False