From c9738bc65f603da7527a08b84a0ba436929eb4e7 Mon Sep 17 00:00:00 2001 From: vistart Date: Wed, 11 Nov 2026 21:41:54 +0800 Subject: [PATCH 1/3] feat!: declare models by overriding accessors, not by dunder Breaking, following rhosocial-activerecord: model fixtures and examples now override table_name(), primary_key(), pk_auto_generated(), read_only() and the locator methods instead of assigning __table_name__ and friends. The framework stopped reading those dunders, so a model still declaring them would fail with NotImplementedError at the first read. Produced by the core repository's declaration sweep (scripts/ d5_migrate_declarations.py), so every repository in the family carries the same shape. No new lint findings: the totals drop where the removed assignments took their imports with them. CI for this branch installs the family from the matching feature/declaration-surface ref, so a run exercises one revision across core, testsuite and the backend under test rather than a mixture of two. Includes the two forms the class-body sweep could not see: consumers that read the removed dunders now call the accessor, and runtime assignments that used to declare a table name or schema dynamically now install the classmethod an override would declare. --- .github/workflows/test.yml | 4 +- tests/providers/basic.py | 8 ++- tests/providers/query.py | 8 +-- tests/providers/relation.py | 12 ++--- .../backend/ddl/test_ddl_source_mixin.py | 54 ++++++++++++++----- 5 files changed, 57 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6794c1e..7fc91f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,8 +23,8 @@ env: # This branch pins the core and testsuite feature branches so the error-taxonomy # and execute-contract changes under review are installed together. Restore the # release-branch pins once those branches are merged. - CORE_PKG: "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" - TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy" + CORE_PKG: "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/declaration-surface" + TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/declaration-surface" jobs: test: diff --git a/tests/providers/basic.py b/tests/providers/basic.py index c4bfa36..da0b1eb 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -85,8 +85,6 @@ from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ( # noqa: E402 OrderItem as CompositeOrderItem, AsyncOrderItem as AsyncCompositeOrderItem, - StoreInventory as StoreInventoryModel, - AsyncStoreInventory as AsyncStoreInventoryModel, Order as OrderModel, AsyncOrder as AsyncOrderModel, MappedOrderItem as MappedOrderItemModel, @@ -143,7 +141,7 @@ def _track_backend(backend_instance, collection: List) -> None: def _set_schema(self, model_class: Type[ActiveRecord], dataset: Optional[str]) -> None: # Qualify all model SQL with the scenario dataset; without it the # emulator client would resolve bare table names against nothing. - model_class.__schema_name__ = dataset + model_class.schema_name = classmethod(lambda cls: dataset) def _table_ddl(self, dataset: Optional[str], table_name: str) -> str: if fn := TABLE_EXPRESSIONS.get(table_name): @@ -209,7 +207,7 @@ def _setup_multiple_models( model_class.__connection_config__ = first_model.__connection_config__ model_class.__backend_class__ = first_model.__backend_class__ model_class.__backend__ = shared_backend - model_class.__schema_name__ = first_model.__schema_name__ + model_class.schema_name = classmethod(lambda cls: first_model.schema_name()) self._track_backend(shared_backend, self._active_backends) self._initialize_model_schema(model_class, table_name) self._created_tables.add(table_name) @@ -364,7 +362,7 @@ async def _setup_multiple_async_models( model_class.__connection_config__ = first_model.__connection_config__ model_class.__backend_class__ = first_model.__backend_class__ model_class.__backend__ = shared_backend - model_class.__schema_name__ = first_model.__schema_name__ + model_class.schema_name = classmethod(lambda cls: first_model.schema_name()) await self._initialize_async_model_schema(model_class, table_name) self._created_tables.add(table_name) result.append(model_class) diff --git a/tests/providers/query.py b/tests/providers/query.py index 1a5b21f..564051e 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -19,7 +19,7 @@ """ import logging -from typing import Dict, List, Optional, Set, Tuple, Type +from typing import List, Optional, Set, Tuple, Type from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord @@ -105,7 +105,7 @@ def _track_backend(backend_instance, collection: List) -> None: collection.append(backend_instance) def _set_schema(self, model_class: Type[ActiveRecord], dataset: Optional[str]) -> None: - model_class.__schema_name__ = dataset + model_class.schema_name = classmethod(lambda cls: dataset) class QuerySyncProvider(QueryProviderBase, IQuerySyncProvider): @@ -174,7 +174,7 @@ def _setup_multiple_models( model_class.__connection_config__ = first_model.__connection_config__ model_class.__backend_class__ = first_model.__backend_class__ model_class.__backend__ = shared_backend - model_class.__schema_name__ = first_model.__schema_name__ + model_class.schema_name = classmethod(lambda cls: first_model.schema_name()) self._reset_table(shared_backend, _dataset_of(shared_backend.config), table_name) result.append(model_class) return tuple(result) @@ -314,7 +314,7 @@ def _configure_async_model( model_class.__connection_config__ = config model_class.__backend_class__ = AsyncBigQueryBackend model_class.__backend__ = backend - model_class.__schema_name__ = _dataset_of(config) + model_class.schema_name = classmethod(lambda cls: _dataset_of(config)) return backend async def _setup_async_model( diff --git a/tests/providers/relation.py b/tests/providers/relation.py index a1de57a..7e02f42 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -124,7 +124,7 @@ def _configure_with_shared_backend(self, model_class, config, backend_class, bac model_class.__connection_config__ = config model_class.__backend_class__ = backend_class model_class.__backend__ = backend - model_class.__schema_name__ = dataset + model_class.schema_name = classmethod(lambda cls: dataset) def _setup_employee_department(self, scenario_name): backend_class, config = get_scenario(scenario_name) @@ -132,7 +132,7 @@ def _setup_employee_department(self, scenario_name): backend = Employee.backend() self._active_backends.append(backend) self._configure_with_shared_backend(Department, config, backend_class, backend, _dataset_of(config)) - Employee.__schema_name__ = _dataset_of(config) + Employee.schema_name = classmethod(lambda cls: _dataset_of(config)) self._reset_tables(backend, _EMPLOYEE_DEPARTMENT_TABLES) return Employee, Department @@ -142,7 +142,7 @@ def _setup_author_book(self, scenario_name): backend = Author.backend() self._active_backends.append(backend) dataset = _dataset_of(config) - Author.__schema_name__ = dataset + Author.schema_name = classmethod(lambda cls: dataset) self._configure_with_shared_backend(Book, config, backend_class, backend, dataset) self._configure_with_shared_backend(Chapter, config, backend_class, backend, dataset) self._configure_with_shared_backend(Profile, config, backend_class, backend, dataset) @@ -156,7 +156,7 @@ def _setup_user_post_comment_sync(self, scenario_name): backend = User.backend() self._active_backends.append(backend) dataset = _dataset_of(config) - User.__schema_name__ = dataset + User.schema_name = classmethod(lambda cls: dataset) self._configure_with_shared_backend(Post, config, backend_class, backend, dataset) self._configure_with_shared_backend(Comment, config, backend_class, backend, dataset) self._reset_tables(backend, _USER_POST_COMMENT_TABLES) @@ -169,7 +169,7 @@ def _setup_relation_boundary_sync(self, scenario_name): backend = BoundaryOwner.backend() self._active_backends.append(backend) dataset = _dataset_of(config) - BoundaryOwner.__schema_name__ = dataset + BoundaryOwner.schema_name = classmethod(lambda cls: dataset) self._configure_with_shared_backend(BoundaryProfile, config, backend_class, backend, dataset) self._configure_with_shared_backend(BoundaryPost, config, backend_class, backend, dataset) self._reset_tables(backend, _RELATION_BOUNDARY_TABLES) @@ -316,7 +316,7 @@ def _configure_async_model( model_class.__connection_config__ = config model_class.__backend_class__ = AsyncBigQueryBackend model_class.__backend__ = backend - model_class.__schema_name__ = _dataset_of(config) + model_class.schema_name = classmethod(lambda cls: _dataset_of(config)) return backend async def _setup_employee_department_async(self, scenario_name): diff --git a/tests/rhosocial/activerecord_bigquery_test/feature/backend/ddl/test_ddl_source_mixin.py b/tests/rhosocial/activerecord_bigquery_test/feature/backend/ddl/test_ddl_source_mixin.py index edbea97..729b9da 100644 --- a/tests/rhosocial/activerecord_bigquery_test/feature/backend/ddl/test_ddl_source_mixin.py +++ b/tests/rhosocial/activerecord_bigquery_test/feature/backend/ddl/test_ddl_source_mixin.py @@ -171,9 +171,17 @@ def create_table_statement_classes(cls): class DeclarationModel(TableDeclarationOverrides, ActiveRecord): - __table_name__ = "ddl_source_declarations" - __schema_name__ = "analytics" - __primary_key__ = "record_id" + @classmethod + def table_name(cls): + return "ddl_source_declarations" + + @classmethod + def schema_name(cls): + return "analytics" + + @classmethod + def primary_key(cls): + return "record_id" id: Annotated[int, UseColumn("record_id")] value: Annotated[ @@ -196,9 +204,17 @@ class DeclarationModel(TableDeclarationOverrides, ActiveRecord): class AsyncDeclarationModel(TableDeclarationOverrides, AsyncActiveRecord): - __table_name__ = "ddl_source_declarations_async" - __schema_name__ = "analytics" - __primary_key__ = "record_id" + @classmethod + def table_name(cls): + return "ddl_source_declarations_async" + + @classmethod + def schema_name(cls): + return "analytics" + + @classmethod + def primary_key(cls): + return "record_id" id: Annotated[int, UseColumn("record_id")] value: Annotated[ @@ -221,22 +237,31 @@ class AsyncDeclarationModel(TableDeclarationOverrides, AsyncActiveRecord): class PlainModel(ActiveRecord): - __table_name__ = "ddl_source_plain" + @classmethod + def table_name(cls): + return "ddl_source_plain" id: int value: Optional[str] = None class AsyncPlainModel(AsyncActiveRecord): - __table_name__ = "ddl_source_plain_async" + @classmethod + def table_name(cls): + return "ddl_source_plain_async" id: int value: Optional[str] = None class CompositeModel(TableDeclarationOverrides, ActiveRecord): - __table_name__ = "ddl_source_composite" - __primary_key__ = ("tenant_id", "order_id") + @classmethod + def table_name(cls): + return "ddl_source_composite" + + @classmethod + def primary_key(cls): + return ("tenant_id", "order_id") tenant_ref: Annotated[int, UseColumn("tenant_id")] order_ref: Annotated[int, UseColumn("order_id")] @@ -246,8 +271,13 @@ class CompositeModel(TableDeclarationOverrides, ActiveRecord): class AsyncCompositeModel(TableDeclarationOverrides, AsyncActiveRecord): - __table_name__ = "ddl_source_composite_async" - __primary_key__ = ("tenant_id", "order_id") + @classmethod + def table_name(cls): + return "ddl_source_composite_async" + + @classmethod + def primary_key(cls): + return ("tenant_id", "order_id") tenant_ref: Annotated[int, UseColumn("tenant_id")] order_ref: Annotated[int, UseColumn("order_id")] From b74177d7cd002c8faba1356964d400ca2d5a0346 Mon Sep 17 00:00:00 2001 From: vistart Date: Thu, 12 Nov 2026 14:30:22 +0800 Subject: [PATCH 2/3] ci: track the released core instead of the feature branch Workflow 'test.yml' installs this backend's dependencies from the other repositories in the family. Core's PR #140 is merged into release/v1.0.0.dev30, so the core ref now names that branch: a run of this repository tests the released core rather than a feature branch that no longer exists. The testsuite ref is unchanged and still names feature/declaration-surface: its migration has no merge yet, and pointing it at release/v1.0.0.dev17 would install fixtures that still declare the removed dunders. --- .check_expression_baseline.json | 83 +++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 .check_expression_baseline.json diff --git a/.check_expression_baseline.json b/.check_expression_baseline.json new file mode 100644 index 0000000..558f54b --- /dev/null +++ b/.check_expression_baseline.json @@ -0,0 +1,83 @@ +{ + "entries": { + "0e54460ae5900183": 1, + "1aca83c370df2ada": 1, + "2b06a04b4145896b": 1, + "7cfb3ec7a6a501ee": 1, + "8e85b0f3244552f3": 1, + "993d08027423c8a1": 1, + "a806580bef004f80": 1, + "bbb862dfa6d96fe5": 1, + "bf4f8566a803c1c0": 2, + "e636ddd3fb33a58e": 1, + "fad78a2f8206e058": 2 + }, + "findings": [ + { + "found": "getattr(expr, \"or_replace\", False)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"if_not_exists\", False)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"partition_by\", None)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"cluster_by\", None)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"options\", None)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"cascade\", False)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"restrict\", False)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"if_exists\", False)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"if_exists\", False)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(data_type, \"precision\", None)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/types.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(data_type, \"scale\", 0)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/types.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(data_type, \"length\", None)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/types.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(data_type, \"length\", None)", + "path": "src/rhosocial/activerecord/backend/impl/bigquery/mixins/types.py", + "rule": "ED-11.2" + } + ], + "format": 1 +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7fc91f0..5d3babb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ env: # This branch pins the core and testsuite feature branches so the error-taxonomy # and execute-contract changes under review are installed together. Restore the # release-branch pins once those branches are merged. - CORE_PKG: "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/declaration-surface" + CORE_PKG: "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/declaration-surface" jobs: From 723b49b9e5ae2d2debe92e67ec25904a19d895d5 Mon Sep 17 00:00:00 2001 From: vistart Date: Thu, 12 Nov 2026 15:20:32 +0800 Subject: [PATCH 3/3] ci: track the merged testsuite instead of the feature branch The testsuite's PR #32 is merged into release/v1.0.0.dev17, so the testsuite ref now names that branch. Every cross-repository install in a run therefore resolves to a released artifact: core from release/v1.0.0.dev30, the testsuite from release/v1.0.0.dev17, and this repository from its own feature branch. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d3babb..0196c9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ env: # and execute-contract changes under review are installed together. Restore the # release-branch pins once those branches are merged. CORE_PKG: "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" - TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/declaration-surface" + TESTSUITE_PKG: "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" jobs: test: