From ace57bebc1b3b8cd23a51978fc45e19f8b2e6de2 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 1 Dec 2025 11:27:51 +0800 Subject: [PATCH 01/11] chore: bump version to 1.0.0.dev2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f85c78b0..4cebfded 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.dev1" +version = "1.0.0.dev2" description = "postgres backend implementation for rhosocial-activerecord, providing a robust and optimized postgres database support." readme = "README.md" license = { file = "LICENSE" } From b3fe01c3a5fefd8f26cc1d3781443531274057a9 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 1 Dec 2025 17:25:05 +0800 Subject: [PATCH 02/11] ci: use git+https for dependencies in CI Updated the CI workflow to install rhosocial-activerecord and rhosocial-activerecord-testsuite directly from their git repositories, ensuring that the latest development versions are used in tests. Also removed the rhosocial-activerecord from pyproject.toml to avoid conflicts. --- .github/workflows/test.yml | 9 +++------ pyproject.toml | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 000b957b..33a9b997 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,13 +65,10 @@ jobs: pip install -r requirements.txt pip install -r requirements-dev.txt - - name: Install activerecord from git + - name: Install activerecord and testsuite from git run: | - pip install rhosocial-activerecord==1.0.0.dev13 - - - name: Install testsuite package from git - run: | - pip install rhosocial-activerecord-testsuite==1.0.0.dev3 + pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev14 + pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev4 - name: Create postgres scenario config run: | diff --git a/pyproject.toml b/pyproject.toml index 4cebfded..88eb821a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "rhosocial-activerecord>=0.9.0,<2.0.0", "psycopg>=3.2.12", "psycopg-binary>=3.2.12" ] From d154bb986a9fc8c7e9429d155352e298b4e871c2 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 4 Dec 2025 09:59:24 +0800 Subject: [PATCH 03/11] feat: Implement PostgreSQL provider for type adapter tests - Adds the ype_adapter_tests.sql schema file for PostgreSQL. - Adds the bridge test file est_type_adapter.py to integrate the generic type adapter tests from python-activerecord-testsuite. - Implements the setup_type_adapter_model_and_schema and get_yes_no_adapter methods in ests/providers/basic.py for the PostgreSQL backend. --- tests/providers/basic.py | 13 ++++++++++++- .../feature/basic/schema/type_adapter_tests.sql | 11 +++++++++++ .../feature/basic/test_type_adapter.py | 15 +++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 909ebef5..929f0674 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -14,6 +14,7 @@ from typing import Type, List from rhosocial.activerecord.model import ActiveRecord +from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter from rhosocial.activerecord.testsuite.feature.basic.interfaces import IBasicProvider # ...and the scenarios are defined specifically for this backend. from .scenarios import get_enabled_scenarios, get_scenario @@ -90,6 +91,16 @@ def setup_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ValidatedUser return self._setup_model(ValidatedUser, scenario_name, "validated_users") + def setup_type_adapter_model_and_schema(self, scenario_name: str) -> Type[ActiveRecord]: + """Sets up the database for the `TypeAdapterTest` model tests.""" + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import TypeAdapterTest + return self._setup_model(TypeAdapterTest, scenario_name, "type_adapter_tests") + + def get_yes_no_adapter(self) -> 'BaseSQLTypeAdapter': + """Returns an instance of the YesOrNoBooleanAdapter.""" + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import YesOrNoBooleanAdapter + return YesOrNoBooleanAdapter() + 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. @@ -107,7 +118,7 @@ def cleanup_after_test(self, scenario_name: str): for backend_instance in self._active_backends: try: # Drop all tables that might have been created for basic tests - for table_name in ['users', 'type_cases', 'type_tests', 'validated_field_users', 'validated_users']: + for table_name in ['users', 'type_cases', 'type_tests', 'validated_field_users', 'validated_users', 'type_adapter_tests']: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql new file mode 100644 index 00000000..28e34c3f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS type_adapter_tests ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + optional_name VARCHAR(255), + optional_age INTEGER, + last_login TIMESTAMP, + is_premium BOOLEAN, + unsupported_union VARCHAR(255), + custom_bool VARCHAR(3), + optional_custom_bool VARCHAR(3) +); \ No newline at end of file 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 new file mode 100644 index 00000000..3adffc2b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_type_adapter.py @@ -0,0 +1,15 @@ +""" +This is a "bridge" file for the basic features test group, specifically for +type adapter tests. + +Its purpose is to import the generic tests from the `rhosocial-activerecord-testsuite` +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, +) + +# Import all tests from the generic testsuite file. +from rhosocial.activerecord.testsuite.feature.basic.test_type_adapter import * \ No newline at end of file From eca8dbc8c8a6583761bbad931d35ebd6ca296aaf Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 4 Dec 2025 15:10:12 +0800 Subject: [PATCH 04/11] docs(postgres): add comprehensive testing guide Introduces `testing.md` for the PostgreSQL backend, detailing its testing setup, integration with the shared test suite, and backend-specific testing practices. Adapts content from the existing MySQL testing guide, ensuring consistency in documentation style and structure. Provides clear instructions on PostgreSQL-specific dependencies (`psycopg`), scenario configuration (`postgres_scenarios.yaml`), and Docker setup. Emphasizes the capability declaration process at backend instantiation and the importance of comprehensive test imports for compatibility. Includes examples for PostgreSQL-specific features like JSONB operators and troubleshooting tips. --- .gemini/testing.md | 203 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 .gemini/testing.md diff --git a/.gemini/testing.md b/.gemini/testing.md new file mode 100644 index 00000000..42328085 --- /dev/null +++ b/.gemini/testing.md @@ -0,0 +1,203 @@ +> **Important**: This document is a supplement to the main **Testing Architecture and Execution Guide** ([local](../../python-activerecord/.gemini/testing.md) | [github](https://github.com/Rhosocial/python-activerecord/blob/main/.gemini/testing.md)). Please read the core documentation first to understand the overall testing architecture, provider patterns, and capability negotiation. This guide focuses only on details specific to the PostgreSQL backend. + +# PostgreSQL Backend Testing Guide + +## 1. Overview + +This guide provides instructions for setting up and running tests for the `rhosocial-activerecord-postgres` backend, covering PostgreSQL-specific configuration, testing patterns, and troubleshooting. + +## 2. PostgreSQL Test Environment Setup + +### 2.1. Dependencies + +In addition to core testing dependencies, the PostgreSQL backend requires `psycopg`. For easier installation, `psycopg-binary` is also included. + +```toml +# pyproject.toml +[project] +dependencies = [ + "psycopg>=3.2.12", + "psycopg-binary>=3.2.12" +] + +[project.optional-dependencies] +test = [ + # ... other dependencies + "rhosocial-activerecord-testsuite", +] +``` + +### 2.2. Test Scenario Configuration + +All database configurations for testing are managed through **test scenarios**. The primary method for defining these scenarios is the `tests/config/postgres_scenarios.yaml` file. This file is git-ignored, so you must create it locally. + +The test framework, specifically `config_manager.py` and `providers/scenarios.py`, reads this file to understand which database environments are available for testing. + +**Example `tests/config/postgres_scenarios.yaml`:** + +This example shows how to configure connections for different versions of PostgreSQL. You should adapt the host, port, and credential details to your local environment. + +```yaml +scenarios: + # Example for a PostgreSQL 16 server + postgres_16: + host: "localhost" + port: 5432 + username: "test_user" + password: "your_password" # Use ${PG_PASSWORD} to load from env var + database: "test_activerecord_pg16" + sslmode: "prefer" + + # Example for a PostgreSQL 15 server (commented out by default) + # postgres_15: + # host: "localhost" + # port: 5433 + # username: "test_user" + # password: "your_password" + # database: "test_activerecord_pg15" +``` + +Alternatively, for CI/CD environments, you can configure scenarios using environment variables, but the YAML file is recommended for local development. + +### 2.3. Setting Up Test Databases with Docker + +For a consistent and isolated testing environment, you can use Docker to run PostgreSQL instances. + +```bash +# Start a PostgreSQL 16 container for the 'postgres_16' scenario +docker run -d \ + --name postgres-test-16 \ + -e POSTGRES_USER=test_user \ + -e POSTGRES_PASSWORD=your_password \ + -e POSTGRES_DB=test_activerecord_pg16 \ + -p 5432:5432 \ + postgres:16 +``` + +## 3. Testing Against the Shared Test Suite + +Integrating the PostgreSQL backend with `rhosocial-activerecord-testsuite` involves implementing a set of provider classes that adapt the generic tests to the PostgreSQL environment. + +### 3.1. Implement Provider Interfaces by Feature + +For each feature category defined in the test suite (e.g., `basic`, `query`), a corresponding provider must be implemented in the `tests/providers/` directory. + +Each provider is responsible for: +1. **Setting up the database connection** for its specific test scenario. +2. **Creating the necessary schema**. This involves converting SQLite-based schemas from the test suite to PostgreSQL-compatible SQL (e.g., changing `AUTOINCREMENT` to `SERIAL` or `IDENTITY`). +3. **Configuring and returning ActiveRecord models**. +4. **Handling cleanup** (teardown). + +The `providers/registry.py` file discovers and registers these provider implementations. + +### 3.2. Import and Execute Shared Tests + +With providers in place, tests are triggered by files in `tests/rhosocial/activerecord_postgres_test/feature/`. + +To ensure maximum compatibility, **it is strongly recommended to import and run the entire suite of tests without omissions.** This provides a comprehensive validation of the backend's implementation. + +These test files import test classes from `rhosocial-activerecord-testsuite`, and pytest fixtures invoke the providers to run the tests against the PostgreSQL backend. + +```bash +# Ensure the test suite is installed +pip install rhosocial-activerecord-testsuite +``` + +## 4. Capability Declaration + +Capabilities are declared once, during the **backend's instantiation phase**, not during testing. The `PostgreSQLBackend` class's `_initialize_capabilities` method is called on creation to build a `DatabaseCapabilities` object based on the database version. + +**Example: Declaring Capabilities in `PostgreSQLBackend`:** +```python +# src/rhosocial/activerecord/backend/impl/postgres/backend.py +from rhosocial.activerecord.backend.capabilities import ( + DatabaseCapabilities, + CTECapability, + JSONCapability +) + +class PostgreSQLBackend(StorageBackend): + def _initialize_capabilities(self): + """Initializes and returns the backend's capability descriptor.""" + capabilities = DatabaseCapabilities() + version = self.get_server_version() # Returns a tuple like (16, 1, 0) + + # PostgreSQL has robust JSONB support + if version >= (9, 4, 0): + capabilities.add_json_operation([ + JSONCapability.BASIC_JSON, + JSONCapability.JSON_EXTRACT, + JSONCapability.JSONB, # PostgreSQL specific + ]) + + # RETURNING clause is well-supported + capabilities.add_dml_returning(True) + + return capabilities +``` +The test suite automatically uses this information to skip unsupported tests. For backend-specific tests, you are responsible for managing capability checks. + +## 5. Writing PostgreSQL-Specific Tests + +Dedicated tests are needed for PostgreSQL-specific features. These can be organized in a separate directory or interspersed within the `tests/rhosocial/activerecord_postgres_test/feature/` directories. + +### Example: Testing JSONB Contains Operator + +```python +# tests/feature/query/test_postgres_jsonb.py +def test_postgres_jsonb_contains_operator(pg_backend): + """Tests PostgreSQL's JSONB '@>' contains operator.""" + pg_backend.execute("CREATE TABLE users (id SERIAL, data JSONB)") + + class User(ActiveRecord): + # ... + + User(data={'tags': ['python', 'orm']}).save() + + # Use the @> operator for JSONB containment + found_user = User.where("data @> %s", [{'tags': ['python']}]).first() + + assert found_user is not None +``` + +## 6. Troubleshooting + +* **Issue**: `psycopg.OperationalError: connection failed` + * **Solution**: + 1. Check if your PostgreSQL service is running (e.g., via `docker ps`). + 2. Confirm that the details in your `postgres_scenarios.yaml` match your running database instance. + 3. Try connecting manually (e.g., with `psql`) to verify credentials and permissions. + +* **Issue**: PostgreSQL syntax errors during shared tests. + * **Solution**: Check the schema conversion logic in your providers. It may need to be updated to handle more SQLite-to-PostgreSQL syntax differences (e.g., `AUTOINCREMENT` to `SERIAL`). + +* **Issue**: Tests skipped due to unsupported capabilities. + * **Solution**: Check the `_initialize_capabilities` method in your backend implementation. Ensure it correctly declares all supported features based on the PostgreSQL version. + +## 7. Quick Command Reference + +**Crucial Prerequisite**: Before running any tests, `PYTHONPATH` **must** be set. + +```bash +# Set PYTHONPATH (Linux/macOS) +export PYTHONPATH=src + +# Set PYTHONPATH (Windows PowerShell) +$env:PYTHONPATH="src" + +# --- + +# --- Test Execution --- + +# Run all PostgreSQL backend tests +pytest tests/ + +# Run tests for a specific feature +pytest tests/rhosocial/activerecord_postgres_test/feature/basic/ + +# Run specific tests by name +pytest -k "jsonb" + +# View coverage +pytest --cov=rhosocial.activerecord.backend.impl.postgres tests/ +``` From f392c0ec767bed19e54ffa9ee626150b2fb9c183 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 4 Dec 2025 21:11:18 +0800 Subject: [PATCH 05/11] fix(postgres): Adjust schema for timezone-aware datetime in tests Corrects the schema definition for the ype_adapter_tests table in the PostgreSQL backend. The last_login column is now properly defined to store timezone-aware datetime values, resolving the est_optional_datetime_conversion failures. This ensures consistent handling of datetime types between application and PostgreSQL database. --- .../feature/basic/schema/type_adapter_tests.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql index 28e34c3f..aaf0b8a9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/type_adapter_tests.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS type_adapter_tests ( name VARCHAR(255) NOT NULL, optional_name VARCHAR(255), optional_age INTEGER, - last_login TIMESTAMP, + last_login TIMESTAMP WITH TIME ZONE, is_premium BOOLEAN, unsupported_union VARCHAR(255), custom_bool VARCHAR(3), From 5d733b2fcfbe0d61e4da78e60ce7bd6e6e1e5598 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 5 Dec 2025 18:06:47 +0800 Subject: [PATCH 06/11] feat(postgres): Implement support for annotated adapter query tests Adds implementation for `setup_annotated_query_fixtures` in `tests/providers/query.py` for the PostgreSQL backend. Creates `searchable_items.sql` schema adapted for PostgreSQL. Introduces `test_annotated_adapter_queries.py` bridge file to run the new generic tests from the testsuite. This enables testing of `Annotated` type adapters in query scenarios for the PostgreSQL backend. --- tests/providers/query.py | 7 +++++++ .../feature/query/schema/searchable_items.sql | 9 +++++++++ .../query/test_annotated_adapter_queries.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py diff --git a/tests/providers/query.py b/tests/providers/query.py index 958f010b..a8459944 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -144,6 +144,13 @@ 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 _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. diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql new file mode 100644 index 00000000..2d149a4e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql @@ -0,0 +1,9 @@ +-- tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql + +CREATE TABLE "searchable_items" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR(255), + "tags" TEXT, + "created_at" TIMESTAMPTZ, + "updated_at" TIMESTAMPTZ +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py new file mode 100644 index 00000000..03993742 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py @@ -0,0 +1,14 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py +""" +This is a "bridge" file for the query features test group, specifically for +tests related to Annotated type adapters. + +Its purpose is to import the generic tests from the `rhosocial-activerecord-testsuite` +package and make them discoverable by `pytest` within this project's test run. +""" + +from rhosocial.activerecord.testsuite.feature.query.conftest import ( + annotated_query_fixtures, +) + +from rhosocial.activerecord.testsuite.feature.query.test_annotated_adapter_queries import * From 5fe1b160f808d2b981d7b5570c3064bc893a1690 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 9 Dec 2025 23:06:06 +0800 Subject: [PATCH 07/11] feat(postgres): Add support for new mapping and type adapter tests This commit brings the PostgreSQL backend up to date with the latest testing requirements from the core testsuite, specifically for the new field-to-column mapping and type adapter features. - Updated `tests/providers/basic.py` and `tests/providers/query.py` to implement the new required fixture setup methods (`setup_mapped_models`, `setup_mixed_models`, `setup_annotated_query_fixtures`). - Added the necessary test "bridge" files (e.g., `test_field_column_mapping.py`, `test_annotated_adapter_queries.py`) to integrate the generic tests from the testsuite. - Created PostgreSQL-specific versions of the schema files (`posts.sql`, `comments.sql`, etc.) required by the new test cases. - Patched the `insert` and `async insert` methods in the `PostgresBackend` to accept the `column_mapping` argument and correctly handle `SERIAL` primary keys by omitting them from the INSERT statement when their value is None. This resolves the `NotNullViolation` errors. With these changes, all tests in the `feature/basic` and `feature/query` directories now pass for the PostgreSQL backend. --- .../backend/impl/postgres/backend.py | 12 +++++++ tests/providers/basic.py | 34 +++++++++++++++++-- tests/providers/query.py | 16 ++++++++- .../basic/schema/column_mapping_items.sql | 6 ++++ .../feature/basic/schema/comments.sql | 11 ++++++ .../basic/schema/mixed_annotation_items.sql | 8 +++++ .../feature/basic/schema/posts.sql | 11 ++++++ .../feature/basic/test_basic_mapped_models.py | 18 ++++++++++ .../basic/test_field_column_mapping.py | 18 ++++++++++ .../feature/query/schema/searchable_items.sql | 6 ++-- .../query/test_annotated_adapter_queries.py | 2 +- .../feature/query/test_query_mapped_models.py | 25 ++++++++++++++ 12 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/schema/column_mapping_items.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/schema/comments.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/schema/mixed_annotation_items.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/schema/posts.sql create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/query/test_query_mapped_models.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend.py index 81722133..649bb001 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend.py @@ -422,6 +422,7 @@ def insert(self, data: Dict, returning: Optional[Union[bool, List[str], ReturningOptions]] = None, column_adapters: Optional[Dict] = None, + column_mapping: Optional[Dict[str, str]] = None, auto_commit: Optional[bool] = True, primary_key: Optional[str] = None) -> QueryResult: """Insert record and force returning the primary key. @@ -429,10 +430,15 @@ def insert(self, Overrides the base implementation to ignore the incorrect `returning=False` passed from the base model class. """ + # psycopg3 doesn't handle None for SERIAL columns, so we pop the pk if it's None. + if primary_key and data.get(primary_key) is None: + data.pop(primary_key, None) + # Force returning the primary key, overriding the faulty value from the base class final_returning = [primary_key] if primary_key else None return super().insert( table, data, returning=final_returning, column_adapters=column_adapters, + column_mapping=column_mapping, auto_commit=auto_commit, primary_key=primary_key ) @property @@ -617,6 +623,7 @@ async def insert(self, data: Dict, returning: Optional[Union[bool, List[str], ReturningOptions]] = None, column_adapters: Optional[Dict] = None, + column_mapping: Optional[Dict[str, str]] = None, auto_commit: Optional[bool] = True, primary_key: Optional[str] = None) -> QueryResult: """Insert record and force returning the primary key asynchronously. @@ -624,10 +631,15 @@ async def insert(self, Overrides the base implementation to ignore the incorrect `returning=False` passed from the base model class. """ + # psycopg3 doesn't handle None for SERIAL columns, so we pop the pk if it's None. + if primary_key and data.get(primary_key) is None: + data.pop(primary_key, None) + # Force returning the primary key, overriding the faulty value from the base class final_returning = [primary_key] if primary_key else None return await super().insert( table, data, returning=final_returning, column_adapters=column_adapters, + column_mapping=column_mapping, auto_commit=auto_commit, primary_key=primary_key ) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 929f0674..730011fd 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -11,7 +11,7 @@ 3. Cleaning up any resources after a test runs. """ import os -from typing import Type, List +from typing import Type, List, Tuple from rhosocial.activerecord.model import ActiveRecord from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter @@ -64,6 +64,14 @@ def _setup_model(self, model_class: Type[ActiveRecord], scenario_name: str, tabl return model_class + def _setup_multiple_models(self, model_classes: List[Tuple[Type[ActiveRecord], str]], scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + """Helper to set up multiple related models for a test.""" + result = [] + for model_class, table_name in model_classes: + configured_model = self._setup_model(model_class, scenario_name, table_name) + result.append(configured_model) + return tuple(result) + # --- Implementation of the IBasicProvider interface --- def setup_user_model(self, scenario_name: str) -> Type[ActiveRecord]: @@ -91,6 +99,23 @@ def setup_validated_user_model(self, scenario_name: str) -> Type[ActiveRecord]: from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ValidatedUser return self._setup_model(ValidatedUser, scenario_name, "validated_users") + def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + """Sets up the database for MappedUser, MappedPost, and MappedComment models.""" + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import MappedUser, MappedPost, MappedComment + return self._setup_multiple_models([ + (MappedUser, "users"), + (MappedPost, "posts"), + (MappedComment, "comments") + ], scenario_name) + + def setup_mixed_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], ...]: + """Sets up the database for ColumnMappingModel and MixedAnnotationModel.""" + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import ColumnMappingModel, MixedAnnotationModel + return self._setup_multiple_models([ + (ColumnMappingModel, "column_mapping_items"), + (MixedAnnotationModel, "mixed_annotation_items") + ], scenario_name) + def setup_type_adapter_model_and_schema(self, scenario_name: str) -> Type[ActiveRecord]: """Sets up the database for the `TypeAdapterTest` model tests.""" from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import TypeAdapterTest @@ -115,10 +140,15 @@ 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. """ + tables_to_drop = [ + 'users', 'type_cases', 'type_tests', 'validated_field_users', + 'validated_users', 'type_adapter_tests', 'posts', 'comments', + 'column_mapping_items', 'mixed_annotation_items' + ] for backend_instance in self._active_backends: try: # Drop all tables that might have been created for basic tests - for table_name in ['users', 'type_cases', 'type_tests', 'validated_field_users', 'validated_users', 'type_adapter_tests']: + for table_name in tables_to_drop: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: diff --git a/tests/providers/query.py b/tests/providers/query.py index a8459944..beeeaff6 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -151,6 +151,16 @@ def setup_annotated_query_fixtures(self, scenario_name: str) -> Tuple[Type[Activ (SearchableItem, "searchable_items"), ], scenario_name) + def setup_mapped_models(self, scenario_name: str) -> Tuple[Type[ActiveRecord], Type[ActiveRecord], Type[ActiveRecord]]: + """Sets up the database for MappedUser, MappedPost, and MappedComment models.""" + from rhosocial.activerecord.testsuite.feature.basic.fixtures.models import MappedUser, MappedPost, MappedComment + return self._setup_multiple_models([ + (MappedUser, "users"), + (MappedPost, "posts"), + (MappedComment, "comments") + ], scenario_name) + + 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. @@ -165,11 +175,15 @@ 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 ['users', 'orders', 'order_items', 'posts', 'comments', 'json_users', 'nodes', 'extended_orders', 'extended_order_items']: + for table_name in tables_to_drop: try: backend_instance.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE') except Exception: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/column_mapping_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/column_mapping_items.sql new file mode 100644 index 00000000..9ee802ee --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/column_mapping_items.sql @@ -0,0 +1,6 @@ +CREATE TABLE "column_mapping_items" ( + "id" SERIAL PRIMARY KEY, + "name" VARCHAR(255) NOT NULL, + "item_total" INT NOT NULL, + "remarks" INT +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/comments.sql b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/comments.sql new file mode 100644 index 00000000..18a1eb1b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/comments.sql @@ -0,0 +1,11 @@ +CREATE TABLE "comments" ( + "id" SERIAL PRIMARY KEY, + "post_ref" INT NOT NULL, + "author" INT NOT NULL, + "text" TEXT NOT NULL, + "created_at" TIMESTAMP NOT NULL, + "updated_at" TIMESTAMP, + "approved" BOOLEAN DEFAULT false, + FOREIGN KEY ("post_ref") REFERENCES "posts"("id") ON DELETE CASCADE, + FOREIGN KEY ("author") REFERENCES "users"("id") ON DELETE CASCADE +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/mixed_annotation_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/mixed_annotation_items.sql new file mode 100644 index 00000000..e824b264 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/mixed_annotation_items.sql @@ -0,0 +1,8 @@ +CREATE TABLE "mixed_annotation_items" ( + "id" INT PRIMARY KEY, + "name" VARCHAR(255) NOT NULL, + "tags" TEXT, + "meta" TEXT, + "description" TEXT, + "status" TEXT +); diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/posts.sql b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/posts.sql new file mode 100644 index 00000000..a57be435 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/schema/posts.sql @@ -0,0 +1,11 @@ +CREATE TABLE "posts" ( + "id" SERIAL PRIMARY KEY, + "author" INT NOT NULL, + "title" VARCHAR(255) NOT NULL, + "content" TEXT, + "published_at" TIMESTAMP, + "published" BOOLEAN DEFAULT false, + "created_at" TIMESTAMP, + "updated_at" TIMESTAMP, + FOREIGN KEY ("author") REFERENCES "users"("id") ON DELETE CASCADE +); 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 new file mode 100644 index 00000000..ef61fd01 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py @@ -0,0 +1,18 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/test_basic_mapped_models.py +""" +This module serves as a bridge to execute the generic basic mapped models tests +defined in the `rhosocial-activerecord-testsuite` package against the PostgreSQL backend. + +The tests are dynamically parametrized with PostgreSQL-specific fixtures. +""" + +# IMPORTANT: +# The following imports are essential for the test suite to discover and run the tests. +# The fixtures are defined in the `conftest.py` of the testsuite and are implemented +# by the provider in the current backend (`tests/providers/basic.py`). + +# The `basic_model_fixtures` is a fixture that provides the necessary setup for the tests. +from rhosocial.activerecord.testsuite.feature.basic.conftest import mapped_models_fixtures + +# The `TestBasicMappedModels` class contains the actual test cases. +from rhosocial.activerecord.testsuite.feature.basic.test_example_basic_fixtures import * 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 new file mode 100644 index 00000000..35f27e39 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py @@ -0,0 +1,18 @@ +# tests/rhosocial/activerecord_postgres_test/feature/basic/test_field_column_mapping.py +""" +This module serves as a bridge to execute the generic field-to-column mapping tests +defined in the `rhosocial-activerecord-testsuite` package against the PostgreSQL backend. + +The tests are dynamically parametrized with PostgreSQL-specific fixtures. +""" + +# IMPORTANT: +# The following imports are essential for the test suite to discover and run the tests. +# The fixtures are defined in the `conftest.py` of the testsuite and are implemented +# by the provider in the current backend (`tests/providers/basic.py`). + +# The `mapped_models_fixtures` is a fixture that provides the necessary setup for the tests. +from rhosocial.activerecord.testsuite.feature.basic.conftest import mapped_models_fixtures + +# The `TestMappedModels` class contains the actual test cases. +from rhosocial.activerecord.testsuite.feature.basic.test_field_column_mapping import TestMappedModels diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql b/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql index 2d149a4e..4a3b59bc 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/schema/searchable_items.sql @@ -4,6 +4,6 @@ CREATE TABLE "searchable_items" ( "id" SERIAL PRIMARY KEY, "name" VARCHAR(255), "tags" TEXT, - "created_at" TIMESTAMPTZ, - "updated_at" TIMESTAMPTZ -); + "created_at" TIMESTAMP, + "updated_at" TIMESTAMP +); \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py index 03993742..aca81338 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_annotated_adapter_queries.py @@ -11,4 +11,4 @@ annotated_query_fixtures, ) -from rhosocial.activerecord.testsuite.feature.query.test_annotated_adapter_queries import * +from rhosocial.activerecord.testsuite.feature.query.test_annotated_adapter_queries import * \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_mapped_models.py b/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_mapped_models.py new file mode 100644 index 00000000..eb3856bb --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/test_query_mapped_models.py @@ -0,0 +1,25 @@ +# tests/rhosocial/activerecord_postgres_test/feature/query/test_query_mapped_models.py +# File path: tests/rhosocial/activerecord_postgres_test/feature/query/test_query_mapped_models.py + +""" +This bridge file imports the `mapped_models_fixtures` from the testsuite's query feature +conftest and then wildcard imports all test cases related to mapped models +from the testsuite. + +IMPORTANT: +- Do NOT add any test logic directly in this file. +- This file is solely responsible for wiring up the testsuite's generic tests + with the backend's specific fixtures and configuration. +""" +import pytest +from rhosocial.activerecord.testsuite.feature.query.conftest import mapped_models_fixtures + +# Wildcard import all test cases from the testsuite's test file. +from rhosocial.activerecord.testsuite.feature.query.test_example_query_fixtures import * + +# Define markers to categorize these tests +pytest.mark.query_mapped_models = pytest.mark.mark(name="query_mapped_models", description="Tests for models with custom column name mappings in query feature.") + +# You can optionally add a fixture here if needed to ensure specific setup/teardown +# for ALL query_mapped_models tests in this backend, but generally this should be handled +# by the provider and the testsuite's conftest. From ea8215e55f36154272c158689f8c7a12994d799f Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 10 Dec 2025 14:01:05 +0800 Subject: [PATCH 08/11] test(backend-postgres): add column mapping tests and update dependencies Adds low-level backend tests for the column name mapping feature, targeting both the synchronous and asynchronous PostgreSQL backends. - Introduces `test_column_mapping_backend.py` and `test_async_column_mapping_backend.py` to validate `column_mapping` and `column_adapters` functionality directly against the backend. Additionally, this commit modernizes the project's dependencies: - Replaces separate `psycopg` and `psycopg-binary` packages with the consolidated `psycopg[binary]` dependency. - Makes the `rhosocial-activerecord` core library an explicit dependency in `pyproject.toml` and `requirements.txt` for clarity and better dependency management. --- pyproject.toml | 4 +- requirements.txt | 4 +- .../test_async_column_mapping_backend.py | 136 ++++++++++++++++++ .../backend/test_column_mapping_backend.py | 136 ++++++++++++++++++ 4 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_async_column_mapping_backend.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_column_mapping_backend.py diff --git a/pyproject.toml b/pyproject.toml index 88eb821a..093e2cef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,8 +35,8 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "psycopg>=3.2.12", - "psycopg-binary>=3.2.12" + "psycopg[binary]", + "rhosocial-activerecord" ] [project.optional-dependencies] diff --git a/requirements.txt b/requirements.txt index 49c9eb0e..e90bb622 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,2 @@ -psycopg>=3.2.12 -psycopg-binary -psycopg-pool +psycopg[binary] rhosocial-activerecord \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_async_column_mapping_backend.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_async_column_mapping_backend.py new file mode 100644 index 00000000..c72e1f9d --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_async_column_mapping_backend.py @@ -0,0 +1,136 @@ +# /Users/vistart/PycharmProjects/rhosocial/python-activerecord-postgres/tests/rhosocial/activerecord_postgres_test/feature/backend/test_async_column_mapping_backend.py +import pytest +import pytest_asyncio +from datetime import datetime +import uuid + +from rhosocial.activerecord.backend.type_adapter import UUIDAdapter, BooleanAdapter + + +@pytest_asyncio.fixture +async def setup_mapped_users_table(async_postgres_backend): + """Async fixture to create and drop a 'mapped_users' table for PostgreSQL.""" + await async_postgres_backend.execute("DROP TABLE IF EXISTS mapped_users") + await async_postgres_backend.execute(""" + CREATE TABLE mapped_users ( + user_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP NOT NULL, + user_uuid UUID, + is_active BOOLEAN + ) + """) + yield + await async_postgres_backend.execute("DROP TABLE IF EXISTS mapped_users") + + +@pytest.mark.asyncio +async def test_async_insert_and_returning_with_mapping(async_postgres_backend, setup_mapped_users_table): + """ + Tests that async execute() with an INSERT and RETURNING clause correctly applies column_mapping. + """ + backend = async_postgres_backend + now = datetime.now() + test_uuid = uuid.uuid4() + + column_to_field_mapping = { + "user_id": "pk", + "name": "full_name", + "user_uuid": "uuid", + "is_active": "active", + "created_at": "creation_time" + } + + sql = "INSERT INTO mapped_users (name, email, created_at, user_uuid, is_active) VALUES (%s, %s, %s, %s, %s)" + params = ("Async PG User", "asyncpg@example.com", now, test_uuid, True) + + result = await backend.execute( + sql=sql, + params=params, + returning=True, + column_mapping=column_to_field_mapping + ) + + assert result.data is not None + assert len(result.data) == 1 + returned_row = result.data[0] + + assert "pk" in returned_row + assert "full_name" in returned_row + assert "uuid" in returned_row + assert "active" in returned_row + assert returned_row["pk"] == 1 + assert returned_row["full_name"] == "Async PG User" + assert returned_row["uuid"] == test_uuid + assert returned_row["active"] is True + + +@pytest.mark.asyncio +async def test_async_update_with_backend(async_postgres_backend, setup_mapped_users_table): + """ + Tests that an async update operation via execute() works correctly for PostgreSQL. + """ + backend = async_postgres_backend + + await backend.execute("INSERT INTO mapped_users (name, email, created_at, is_active) VALUES (%s, %s, %s, %s)", + ("Async Jane PG", "async.jane.pg@example.com", datetime.now(), True)) + + sql = "UPDATE mapped_users SET name = %s WHERE user_id = %s" + params = ("Async Jane Smith PG", 1) + result = await backend.execute(sql, params) + + assert result.affected_rows == 1 + + fetch_result = await backend.execute("SELECT name FROM mapped_users WHERE user_id = 1") + fetched_row = fetch_result.data[0] if fetch_result.data else None + assert fetched_row is not None + assert fetched_row["name"] == "Async Jane Smith PG" + + +@pytest.mark.asyncio +async def test_async_fetch_with_combined_mapping_and_adapters(async_postgres_backend, setup_mapped_users_table): + """ + Tests that async execute() correctly applies both column_mapping and column_adapters for PostgreSQL. + """ + backend = async_postgres_backend + now = datetime.now() + test_uuid = uuid.uuid4() + + column_to_field_mapping = { + "user_id": "pk", + "name": "full_name", + "user_uuid": "uuid", + "is_active": "active" + } + + column_adapters = { + "user_uuid": (UUIDAdapter(), uuid.UUID), + "is_active": (BooleanAdapter(), bool) + } + + await backend.execute( + "INSERT INTO mapped_users (name, email, created_at, user_uuid, is_active) VALUES (%s, %s, %s, %s, %s)", + ("Async PG Combined", "asyncpgcombined@example.com", now, test_uuid, True) + ) + + result = await backend.execute( + "SELECT * FROM mapped_users WHERE user_id = 1", + column_mapping=column_to_field_mapping, + column_adapters=column_adapters + ) + + fetched_row = result.data[0] if result.data else None + assert fetched_row is not None + + # 1. Assert keys are the MAPPED FIELD NAMES + assert "full_name" in fetched_row + assert "uuid" in fetched_row + assert "active" in fetched_row + + # 2. Assert values are the CORRECT PYTHON TYPES + assert fetched_row["full_name"] == "Async PG Combined" + assert isinstance(fetched_row["uuid"], uuid.UUID) + assert fetched_row["uuid"] == test_uuid + assert isinstance(fetched_row["active"], bool) + assert fetched_row["active"] is True diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_column_mapping_backend.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_column_mapping_backend.py new file mode 100644 index 00000000..eecc7ddd --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_column_mapping_backend.py @@ -0,0 +1,136 @@ +# /Users/vistart/PycharmProjects/rhosocial/python-activerecord-postgres/tests/rhosocial/activerecord_postgres_test/feature/backend/test_column_mapping_backend.py +import pytest +from datetime import datetime +import uuid + +from rhosocial.activerecord.backend.type_adapter import UUIDAdapter, BooleanAdapter + + +@pytest.fixture +def setup_mapped_users_table(postgres_backend): + """Fixture to create and drop a 'mapped_users' table for PostgreSQL.""" + postgres_backend.execute("DROP TABLE IF EXISTS mapped_users") + postgres_backend.execute(""" + CREATE TABLE mapped_users ( + user_id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP NOT NULL, + user_uuid UUID, + is_active BOOLEAN + ) + """) + yield + postgres_backend.execute("DROP TABLE IF EXISTS mapped_users") + + +def test_insert_and_returning_with_mapping(postgres_backend, setup_mapped_users_table): + """ + Tests that execute() with an INSERT and RETURNING clause correctly applies column_mapping. + """ + backend = postgres_backend + now = datetime.now() + test_uuid = uuid.uuid4() + + column_to_field_mapping = { + "user_id": "pk", + "name": "full_name", + "user_uuid": "uuid", + "is_active": "active", + "created_at": "creation_time" + } + + sql = "INSERT INTO mapped_users (name, email, created_at, user_uuid, is_active) VALUES (%s, %s, %s, %s, %s)" + params = ("Postgres User", "pg@example.com", now, test_uuid, True) + + result = backend.execute( + sql=sql, + params=params, + returning=True, + column_mapping=column_to_field_mapping + ) + + assert result.data is not None + assert len(result.data) == 1 + returned_row = result.data[0] + + assert "pk" in returned_row + assert "full_name" in returned_row + assert "uuid" in returned_row + assert "active" in returned_row + assert returned_row["pk"] == 1 + assert returned_row["full_name"] == "Postgres User" + assert returned_row["uuid"] == test_uuid + assert returned_row["active"] is True + + +def test_update_with_backend(postgres_backend, setup_mapped_users_table): + """ + Tests that an update operation via execute() works correctly for PostgreSQL. + """ + backend = postgres_backend + + backend.execute("INSERT INTO mapped_users (name, email, created_at, is_active) VALUES (%s, %s, %s, %s)", + ("Jane PG", "jane.pg@example.com", datetime.now(), True)) + + sql = "UPDATE mapped_users SET name = %s WHERE user_id = %s" + params = ("Jane Smith PG", 1) + result = backend.execute(sql, params) + + assert result.affected_rows == 1 + + fetch_result = backend.execute("SELECT name FROM mapped_users WHERE user_id = 1") + fetched_row = fetch_result.data[0] if fetch_result.data else None + assert fetched_row is not None + assert fetched_row["name"] == "Jane Smith PG" + + +def test_fetch_with_combined_mapping_and_adapters(postgres_backend, setup_mapped_users_table): + """ + Tests that execute() correctly applies both column_mapping and column_adapters for PostgreSQL. + PostgreSQL's driver (psycopg) often handles common types like UUID and bool automatically, + so this test primarily validates that the adapter mechanism works if an adapter is provided, + and that mapping is applied correctly. + """ + backend = postgres_backend + now = datetime.now() + test_uuid = uuid.uuid4() + + column_to_field_mapping = { + "user_id": "pk", + "name": "full_name", + "user_uuid": "uuid", + "is_active": "active" + } + + # Even if the driver handles it, we can test that providing an adapter still works. + column_adapters = { + "user_uuid": (UUIDAdapter(), uuid.UUID), + "is_active": (BooleanAdapter(), bool) + } + + backend.execute( + "INSERT INTO mapped_users (name, email, created_at, user_uuid, is_active) VALUES (%s, %s, %s, %s, %s)", + ("PG Combined", "pgcombined@example.com", now, test_uuid, True) + ) + + result = backend.execute( + "SELECT * FROM mapped_users WHERE user_id = 1", + column_mapping=column_to_field_mapping, + column_adapters=column_adapters + ) + + fetched_row = result.data[0] if result.data else None + assert fetched_row is not None + + # 1. Assert keys are the MAPPED FIELD NAMES + assert "full_name" in fetched_row + assert "uuid" in fetched_row + assert "active" in fetched_row + + # 2. Assert values are the CORRECT PYTHON TYPES + assert fetched_row["full_name"] == "PG Combined" + assert isinstance(fetched_row["uuid"], uuid.UUID) + assert fetched_row["uuid"] == test_uuid + assert isinstance(fetched_row["active"], bool) + assert fetched_row["active"] is True From 965836beb871ed1ecc5c9c6f077ed5b4d3595d9a Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 11 Dec 2025 12:15:24 +0800 Subject: [PATCH 09/11] feat(postgres): Add changelog fragments for field adapter and column mapping support [skip ci] --- changelog.d/2.added.md | 1 + changelog.d/2.fixed.md | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog.d/2.added.md create mode 100644 changelog.d/2.fixed.md diff --git a/changelog.d/2.added.md b/changelog.d/2.added.md new file mode 100644 index 00000000..f330dd2b --- /dev/null +++ b/changelog.d/2.added.md @@ -0,0 +1 @@ +Added support for new mapping and type adapter tests, implemented annotated adapter query tests and PostgreSQL provider for type adapter tests. Added column mapping tests and a comprehensive testing guide. \ No newline at end of file diff --git a/changelog.d/2.fixed.md b/changelog.d/2.fixed.md new file mode 100644 index 00000000..73849a93 --- /dev/null +++ b/changelog.d/2.fixed.md @@ -0,0 +1 @@ +Adjusted schema for timezone-aware datetime in tests. \ No newline at end of file From a5befb48138899330b6da3d8ce6e79b31b1511b5 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 11 Dec 2025 13:14:13 +0800 Subject: [PATCH 10/11] chore(ci): Update activerecord and testsuite installation sources to PyPI --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33a9b997..a24f0620 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,8 +67,8 @@ jobs: - name: Install activerecord and testsuite from git run: | - pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev14 - pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev4 + pip install rhosocial-activerecord==1.0.0.dev14 + pip install rhosocial-activerecord-testsuite==1.0.0.dev4 - name: Create postgres scenario config run: | From d6c0d0163ab4d851cab2239566b7d5b4dc772966 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 11 Dec 2025 13:23:07 +0800 Subject: [PATCH 11/11] docs: update CHANGELOG for 1.0.0.dev2 [skip ci] --- CHANGELOG.md | 13 +++++++++++++ changelog.d/2.added.md | 1 - changelog.d/2.fixed.md | 1 - 3 files changed, 13 insertions(+), 2 deletions(-) delete mode 100644 changelog.d/2.added.md delete mode 100644 changelog.d/2.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f51efa2..ceb174a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [1.0.0.dev2] - 2025-12-11 + +### Added + +- Added support for new mapping and type adapter tests, implemented annotated adapter query tests and PostgreSQL provider for type adapter tests. Added column mapping tests and a comprehensive testing guide. ([#2](https://github.com/rhosocial/python-activerecord-postgres/issues/2)) + + + +### Fixed + +- Adjusted schema for timezone-aware datetime in tests. ([#2](https://github.com/rhosocial/python-activerecord-postgres/issues/2)) + + ## [1.0.0.dev1] - 2025-11-29 ### Added diff --git a/changelog.d/2.added.md b/changelog.d/2.added.md deleted file mode 100644 index f330dd2b..00000000 --- a/changelog.d/2.added.md +++ /dev/null @@ -1 +0,0 @@ -Added support for new mapping and type adapter tests, implemented annotated adapter query tests and PostgreSQL provider for type adapter tests. Added column mapping tests and a comprehensive testing guide. \ No newline at end of file diff --git a/changelog.d/2.fixed.md b/changelog.d/2.fixed.md deleted file mode 100644 index 73849a93..00000000 --- a/changelog.d/2.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Adjusted schema for timezone-aware datetime in tests. \ No newline at end of file