diff --git a/.claude/architecture.md b/.claude/architecture.md index 32b4ebd3..8e563d02 100644 --- a/.claude/architecture.md +++ b/.claude/architecture.md @@ -104,26 +104,25 @@ Clear distinction between Backend and ActiveRecord, avoiding tight coupling betw **Implementation**: The `SQLDialectBase` (and its concrete implementations like `SQLiteDialect`) provides dedicated methods for this: - `format_identifier(self, identifier: str) -> str`: Formats and quotes identifiers. -- `format_string_literal(self, value: str) -> str`: Formats and quotes string literals. +- `format_literal(self, value: Any) -> str`: Formats and quotes string literals. All query building components (e.g., mixins, `SQLExpression` subclasses) are designed to route identifier and literal formatting through these dialect methods, ensuring a single, centralized point of control for SQL syntax generation. ```python -# In backend/dialect.py +# In backend/dialect/base.py class SQLDialectBase(ABC): @abstractmethod def format_identifier(self, identifier: str) -> str: """Format identifier (table name, column name).""" pass - @abstractmethod - def format_string_literal(self, value: str) -> str: - """Format string literal.""" + def format_literal(self, value: Any) -> str: + """Format a Python value as a SQL literal.""" pass # Usage in query builder (conceptual) # formatted_column = self.model_class.backend().dialect.format_identifier(column_name) -# formatted_string = self.model_class.backend().dialect.format_string_literal(string_value) +# formatted_string = self.model_class.backend().dialect.format_literal(string_value) ``` ## Package Architecture @@ -290,7 +289,10 @@ class ActiveRecord( ```python # backend/base.py class StorageBackend( - # ...composed from LoggingMixin, CapabilityMixin, TypeAdaptionMixin, SQLBuildingMixin, etc. + # ...composed from LoggingMixin, TypeAdaptionMixin, SQLBuildingMixin, + # ReturningClauseMixin, ResultProcessingMixin, SQLOperationsMixin, ExecutionMixin, + # BatchExecutionMixin, ExecutionHooksMixin, ConnectionMixin, + # TransactionManagementMixin. There is no `CapabilityMixin`. ABC ): """ @@ -404,14 +406,14 @@ class Article(TimestampMixin, SoftDeleteMixin, ActiveRecord): ```python # The project's query builder is ActiveQuery, composed from specialized mixins. -from rhosocial.activerecord.query import ActiveQuery, BaseQueryMixin, AggregateQueryMixin, CTEQueryMixin, JoinQueryMixin, RangeQueryMixin, RelationalQueryMixin +from rhosocial.activerecord.query import ActiveQuery, BaseQueryMixin, AggregateQueryMixin, CTEQuery, JoinQueryMixin, RangeQueryMixin, RelationalQueryMixin class ActiveQuery( - CTEQueryMixin, + CTEQuery, JoinQueryMixin, RelationalQueryMixin, RangeQueryMixin, - # BaseQueryMixin and AggregateQueryMixin are inherited through CTEQueryMixin + # BaseQueryMixin and AggregateQueryMixin are inherited through CTEQuery ): """ Complete ActiveQuery implementation, combining all query mixins. @@ -495,7 +497,7 @@ def create_backend(backend_type: str, **config) -> StorageBackend: backends = { 'sqlite': SQLiteBackend, 'mysql': MySQLBackend, - 'postgresql': PostgreSQLBackend, + 'postgres': PostgresBackend, } backend_class = backends.get(backend_type) @@ -511,8 +513,10 @@ def create_backend(backend_type: str, **config) -> StorageBackend: ``` BaseModel (Pydantic) - └── IActiveRecord (Interface - implemented by BaseActiveRecord) - └── BaseActiveRecord (Core implementation, implements IActiveRecord) + └── BaseActiveRecord (Core CRUD implementation, a BaseModel subclass) + # NOTE: BaseActiveRecord does NOT inherit from IActiveRecord. + # IActiveRecord is a Protocol the QUERY mixins implement + # (e.g. `class QueryMixin(IActiveRecord)` in interface/query.py). # ActiveRecord is composed from these mixins and BaseActiveRecord └── QueryMixin └── RelationManagementMixin @@ -530,7 +534,7 @@ StorageBackendBase (ABC) └── StorageBackend (ABC) └── SQLiteBackend # Concrete implementation # └── MySQLBackend (if implemented) - # └── PostgreSQLBackend (if implemented) + # └── PostgresBackend (if implemented) StorageBackendBase (ABC) # Composed from mixins for async operations @@ -546,7 +550,7 @@ StorageBackendBase (ABC) ```python # 1. User configures model from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend class User(ActiveRecord): __table_name__ = "users" @@ -589,8 +593,10 @@ The project maintains **minimal core dependencies** by design: ```python # Minimal core dependencies - Pydantic only dependencies = [ - "pydantic>=2.0.0", # Data validation and model definition - "typing_extensions>=4.0.0", # Backported typing features for Python 3.8 + "pydantic==2.10.6; python_version < '3.9'", # Data validation and model definition + "pydantic>=2.12.0; python_version >= '3.9'", + "typing-extensions>=4.12.0; python_version < '3.9'", # Backported typing for 3.8 + "backports.zoneinfo; python_version < '3.9'", ] ``` @@ -599,9 +605,11 @@ dependencies = [ ### Optional Dependencies ```python +# The core ships NO driver extras. Each extra points at the separate backend +# distribution, which is what pulls the driver in transitively. extras_require = { - "mysql": ["mysql-connector-python>=8.0.0"], # MySQL backend - "postgresql": ["psycopg[binary]>=3.2.13"], # PostgreSQL backend + "mysql": ["rhosocial-activerecord-mysql>=1.0.0,<2.0.0"], + "postgres": ["rhosocial-activerecord-postgres>=1.0.0,<2.0.0"], "dev": ["pytest", "black", "mypy"], # Development tools } ``` @@ -616,14 +624,14 @@ def discover_backends(): # Check for installed backends (each uses only native drivers) try: - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend backends['mysql'] = MySQLBackend # Uses mysql-connector-python directly except ImportError: pass try: - from rhosocial.activerecord.backend.impl.postgresql import PostgreSQLBackend - backends['postgresql'] = PostgreSQLBackend # Uses psycopg 3 directly + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend + backends['postgres'] = PostgresBackend # Uses psycopg 3 directly except ImportError: pass @@ -635,16 +643,18 @@ def discover_backends(): ### 1. Custom Fields ```python -class EncryptedField(Field): - """Custom encrypted field type.""" - - def __set__(self, instance, value): - encrypted = encrypt(value) - super().__set__(instance, encrypted) - - def __get__(self, instance, owner): - value = super().__get__(instance, owner) - return decrypt(value) if value else None +# There is no `Field` base class to subclass. `base/fields.py` holds only +# annotation markers (DDLAnnotation, UseColumn, UseAdapter, UseSqlType, +# UseIndex, UseConstraint, UseColumnAttributes, UseComment, UseGeneratedColumn, +# DerivedField). For transparent encryption use a pydantic field_validator, or +# a SQLTypeAdapter for the column type. +class EncryptedModel(ActiveRecord): + settings: str + + @field_validator("settings", mode="before") + @classmethod + def _decrypt(cls, v): + return decrypt(v) if v else None ``` ### 2. Custom Validators @@ -681,14 +691,16 @@ class User(UserQueryMixin, ActiveRecord): ### 4. Event Hooks ```python -class User(ActiveRecord): - def before_save(self): - """Called before saving.""" - self.updated_at = datetime.now() +# `before_save` / `after_save` are NEVER called -- no such hook exists and +# nothing dispatches to them, so code written this way is silently dead. +# Register against the ModelEvent enum instead. +from rhosocial.activerecord.interface.base import ModelEvent + +user = User(name="Alice") +user.on(ModelEvent.AFTER_INSERT, lambda rec: cache.invalidate(f"user:{rec.id}")) - def after_save(self): - """Called after saving.""" - cache.invalidate(f"user:{self.id}") +# Events: BEFORE_VALIDATE, AFTER_VALIDATE, BEFORE_INSERT, AFTER_INSERT, +# BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE. ``` ## Performance Optimization @@ -791,17 +803,18 @@ class ThreadSafeDict(Dict[K, V]): ### Exception Hierarchy ```python -class ActiveRecordError(Exception): +# backend/errors.py -- there is NO `ActiveRecordError` class. `DatabaseError` +# is the actual root, and everything below derives from it. +class DatabaseError(Exception): """Base exception for all ActiveRecord errors.""" -class DatabaseError(ActiveRecordError): - """Database operation errors.""" - -class ValidationError(ActiveRecordError): - """Data validation errors.""" - -class RecordNotFound(DatabaseError): - """Record not found in database.""" +class ConnectionError(DatabaseError): ... +class TransactionError(DatabaseError): ... +class QueryError(DatabaseError): ... +class ValidationError(DatabaseError): ... +class IntegrityError(DatabaseError): ... +class LockError(DatabaseError): ... +class RecordNotFound(DatabaseError): ... ``` ### Error Propagation diff --git a/.claude/backend_development.md b/.claude/backend_development.md index aa452352..9eb5e353 100644 --- a/.claude/backend_development.md +++ b/.claude/backend_development.md @@ -32,7 +32,7 @@ rhosocial-activerecord-{backend}/ │ └── impl/ │ └── {backend}/ │ ├── __init__.py -│ ├── backend.py # Main backend implementation +│ ├── backend/ # sync + async backend classes │ ├── adapters.py # Backend-specific type adapters │ ├── config.py # Connection configuration │ ├── dialect.py # SQL dialect handling @@ -114,7 +114,7 @@ Expression.to_sql() -> Dialect.format_*() -> SQL string and parameters - `operators.py`: SQL operations (binary, unary, arithmetic expressions) - `predicates.py`: SQL predicate expressions (WHERE clause conditions) - `query_parts.py`: SQL query clauses (WHERE, GROUP BY, HAVING, ORDER BY, etc.) -- `statements.py`: DML/DQL/DDL statements (SELECT, INSERT, UPDATE, DELETE, etc.) +- `statements/`: DML/DQL/DDL statements (a package, not a module) (SELECT, INSERT, UPDATE, DELETE, etc.) - `functions.py`: Standalone factory functions for creating SQL expressions - `aggregates.py`: SQL aggregation expressions and functions - `advanced_functions.py`: Advanced SQL functions (CASE, CAST, EXISTS, window functions) @@ -149,8 +149,9 @@ Every backend must implement the `StorageBackend` abstract base class. This is t ```python # backend.py from typing import Any, Dict, List, Optional, Tuple, Type -from ...base import StorageBackend, ConnectionConfig -from ...type_adapter import SQLTypeAdapter +from rhosocial.activerecord.backend.base import StorageBackend +from rhosocial.activerecord.backend.config import ConnectionConfig +from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter class MyBackend(StorageBackend): """Implementation of MyDatabase backend using native driver.""" @@ -175,7 +176,8 @@ class MyBackend(StorageBackend): """Check if connection is valid.""" pass - def execute(self, sql: str, params: Optional[Tuple] = None, returning: Optional[Union[bool, List[str], ReturningOptions]] = None, column_adapters: Optional[Dict[str, Tuple[SQLTypeAdapter, Type]]] = None) -> QueryResult: + def execute(self, sql: str, params: Optional[Tuple] = None, *, + options: Optional[ExecutionOptions] = None) -> QueryResult: """Executes a SQL query.""" pass @@ -197,10 +199,6 @@ class MyBackend(StorageBackend): """ pass - def _initialize_capabilities(self) -> DatabaseCapabilities: - """Declare all features supported by this backend.""" - pass - def _handle_error(self, error: Exception) -> None: """Catch driver-specific exceptions and raise standard errors.""" pass @@ -241,10 +239,10 @@ Define an immutable configuration class for your backend. # config.py from dataclasses import dataclass, field from typing import Optional, Dict, Any -from rhosocial.activerecord.backend.config import BaseConfig +from rhosocial.activerecord.backend.config import ConnectionConfig -@dataclass(frozen=True) -class MyDatabaseConfig(BaseConfig): +@dataclass +class MyDatabaseConfig(ConnectionConfig): host: str = "localhost" port: int = 5432 database: str @@ -366,34 +364,20 @@ Every dialect inherits from: ```python # dialect.py -from rhosocial.activerecord.backend.dialect import ( - SQLDialect, ReturningClauseHandler, TypeMapping -) -from typing import Dict, List, Optional +from rhosocial.activerecord.backend.dialect.base import SQLDialectBase +from typing import Optional, Tuple -class MyDatabaseDialect(SQLDialect): +class MyDatabaseDialect(SQLDialectBase): def __init__(self): super().__init__() self.returning_handler = MyDatabaseReturningHandler() - def quote_identifier(self, identifier: str) -> str: return f'"{identifier}"' - def get_placeholder(self, name: str = None) -> str: return "?" - - def format_limit_offset(self, sql: str, limit: Optional[int], offset: Optional[int]) -> str: - if limit is not None: sql += f" LIMIT {limit}" - if offset is not None: sql += f" OFFSET {offset}" - return sql - - def get_type_mappings(self) -> Dict[str, TypeMapping]: - return {"INTEGER": TypeMapping("INTEGER"), "TEXT": TypeMapping("TEXT"), ...} + def get_parameter_placeholder(self, position: int = 0) -> str: + return "?" -class MyDatabaseReturningHandler(ReturningClauseHandler): - @property - def is_supported(self) -> bool: return True - - def format_clause(self, columns: Optional[List[str]] = None) -> str: - cols = "*" if not columns else ', '.join(self.dialect.quote_identifier(c) for c in columns) - return f"RETURNING {cols}" +# Identifier quoting is inherited from SQLDialectBase.format_identifier; +# LIMIT/OFFSET come from the DQL mixins; type mapping is not a dialect +# method but a set of SQLTypeAdapter registrations on the backend. ``` ### 4. The Type Adaptation System @@ -565,7 +549,7 @@ This checklist summarizes all the required and recommended steps for creating a - [ ] Implement `get_default_adapter_suggestions` to define the conversion strategy. - [ ] Implement `Transaction Management`. - [ ] Implement `Error Handling` to map driver exceptions. -- [ ] Implement `Feature Detection` to declare capabilities. +- [ ] Implement the `supports_*` capability switches your dialect needs. - [ ] If providing an `AsyncStorageBackend`, ensure it has **Functional Equivalence** with the sync version. ### Required Tests diff --git a/.claude/code_style.md b/.claude/code_style.md index 048b351d..8dc35fcd 100644 --- a/.claude/code_style.md +++ b/.claude/code_style.md @@ -56,7 +56,7 @@ from pydantic import BaseModel, Field # Local application imports from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend import StorageBackend +from rhosocial.activerecord.backend.base import StorageBackend # Relative imports (within same package) from .base import BaseActiveRecord @@ -65,7 +65,7 @@ from ..interface import IActiveRecord ### Line Length and Wrapping -- Maximum line length: 100 characters +- Maximum line length: 120 characters (ruff `line-length = 120`; black formats to 100, but 100 is not the limit) - Wrap long lines using parentheses: ```python @@ -291,7 +291,7 @@ from typing import annotations import third_party -from rhosocial.activerecord import local +from rhosocial.activerecord.model import ActiveRecord from . import relative # Constants @@ -414,12 +414,12 @@ raise DatabaseError( """Test module for specific feature.""" import pytest -from rhosocial.activerecord import Model +from rhosocial.activerecord.model import ActiveRecord # Fixtures at top @pytest.fixture def sample_model(): - return Model(name="test") + return ActiveRecord(name="test") # Test classes for organization class TestCRUDOperations: @@ -603,13 +603,12 @@ def transaction(backend: StorageBackend): ### Mixin Classes ```python -class TimestampMixin: - """Add timestamp fields to models.""" - created_at: datetime = Field(default_factory=datetime.now) - updated_at: Optional[datetime] = None - - def before_save(self): - self.updated_at = datetime.now() +# TimestampMixin declares NO fields: it hooks +# ModelEvent.BEFORE_INSERT / BEFORE_UPDATE and requires the model to +# declare the fields itself, named per __created_at_field__ / +# __updated_at_field__. Use DefaultTimestampMixin to get them declared +# for you. There is no before_save() hook -- lifecycle is registered +# with instance.on(ModelEvent.X, handler). ``` ## Expression-Dialect System Coding Standards @@ -687,7 +686,7 @@ class MyExpression(mixins.ArithmeticMixin, mixins.ComparisonMixin, bases.SQLValu def to_sql(self) -> 'bases.SQLQueryAndParams': # Delegate formatting to dialect - formatted_value = self.dialect.format_string_literal(self.value) + formatted_value = self.dialect.format_literal(self.value) return formatted_value, (self.value,) ``` @@ -705,7 +704,7 @@ def format_identifier(self, identifier: str) -> str: - [ ] Path comment at file start matches actual location - [ ] All functions/classes have docstrings - [ ] Type hints on all function signatures -- [ ] No line exceeds 100 characters +- [ ] No line exceeds 120 characters - [ ] Imports sorted and grouped correctly - [ ] No commented-out code - [ ] Exception messages are informative diff --git a/.claude/commands/gen-model.md b/.claude/commands/gen-model.md index 87631693..25a8b692 100644 --- a/.claude/commands/gen-model.md +++ b/.claude/commands/gen-model.md @@ -58,7 +58,7 @@ CREATE TABLE users ( 2. Show the SQL migration to create the table 3. Remind the user to configure the backend: ```python - from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend + from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend User.configure(SQLiteBackend("sqlite:///app.db")) ``` diff --git a/.claude/commands/validate-model.md b/.claude/commands/validate-model.md index 5e95c977..e7266c89 100644 --- a/.claude/commands/validate-model.md +++ b/.claude/commands/validate-model.md @@ -66,7 +66,7 @@ user = User(name="John") # Will raise "No backend configured" user.save() # CORRECT -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend User.configure(SQLiteBackend("sqlite:///app.db")) user = User(name="John") user.save() diff --git a/.claude/testing.md b/.claude/testing.md index 3f16238b..7360363f 100644 --- a/.claude/testing.md +++ b/.claude/testing.md @@ -21,6 +21,8 @@ This backend CANNOT be used with free-threaded Python builds. Free-threaded test ```toml dependencies = [ "psycopg[binary]>=3.2.13", + "rhosocial-activerecord>=1.0.0.dev0,<2.0.0", + "rhosocial-activerecord>=1.0.0.dev0,<2.0.0", "rhosocial-activerecord" ] ``` @@ -30,8 +32,8 @@ dependencies = [ ```bash # Activate virtual environment and set PYTHONPATH cd /mnt/i/GitHubRepositories/rhosocial/python-activerecord-postgres -source .venv/bin/activate -export PYTHONPATH=src +source .venv3.14-ubuntu26.04/bin/activate +export PYTHONPATH=src:tests # Run tests pytest @@ -57,5 +59,5 @@ markers = [ ## Reference -- [Core testing guide](../python-activerecord/.claude/testing.md) -- [PostgreSQL backend development](../python-activerecord/.claude/backend_development.md) \ No newline at end of file +- [Core testing guide](../../python-activerecord/.claude/testing.md) +- [PostgreSQL backend development](../../../python-activerecord/.claude/backend_development.md) \ No newline at end of file diff --git a/.claude/version_control.md b/.claude/version_control.md index b162bb9f..77488742 100644 --- a/.claude/version_control.md +++ b/.claude/version_control.md @@ -901,9 +901,9 @@ git push origin --delete release/v1.2.0 # Optional - Require pull request reviews (minimum 1 approval) - **Require status checks to pass**: - - `test-with-coverage` (Python 3.14 with coverage ≥90%) - - `test-other-versions` (Python 3.8-3.13 compatibility) - - `test-free-threaded` (Python 3.13t, 3.14t) + - `test` (Python 3.14 with coverage ≥90%) + - `test` (Python 3.8-3.13 compatibility) + - `test` (Python 3.13t, 3.14t) - **Require branches to be up to date before merge** - **Require linear history** (no merge commits) - Include administrators in restrictions @@ -2743,55 +2743,44 @@ git push origin maint/1.2.x v1.2.6 ### Capability Declaration System -Backends MUST declare their supported capabilities using the `DatabaseCapabilities` system: +There is **no** `rhosocial.activerecord.backend.capabilities` module, and no +`DatabaseCapabilities` / `CapabilityCategory` / `CTECapability` classes, and no +`_initialize_capabilities()` hook. Capabilities are declared the ordinary way: +the dialect implements a Protocol from +`rhosocial.activerecord.backend.dialect.protocols` and answers a +`supports_*` switch. ```python -# Backend capability declaration example -from rhosocial.activerecord.backend.capabilities import ( - DatabaseCapabilities, - CapabilityCategory, - CTECapability, - WindowFunctionCapability, -) - -class MySQLBackend(StorageBackend): - def _initialize_capabilities(self): - """Declare backend capabilities based on server version.""" - capabilities = DatabaseCapabilities() - version = self.get_server_version() - - # CTEs supported from MySQL 8.0+ - if version >= (8, 0, 0): - capabilities.add_cte([ - CTECapability.BASIC_CTE, - CTECapability.RECURSIVE_CTE, - ]) - - # Window functions from MySQL 8.0+ - if version >= (8, 0, 0): - capabilities.add_window_function(ALL_WINDOW_FUNCTIONS) - - return capabilities +# mixins/cte.py on the MySQL backend -- the real mechanism +from rhosocial.activerecord.backend.dialect.protocols import CTESupport + +class MySQLCTEMixin(CTESupport): + def supports_basic_cte(self) -> bool: + return self.version >= (8, 0, 0) + + def supports_recursive_cte(self) -> bool: + return self.version >= (8, 0, 0) ``` +`SQLDialectBase` is adapted to the live server on connect +(`backend.introspect_and_adapt()`), which is what populates `self.version` the +gates compare against. A `version` of `None` means "not adapted", and every +gate then raises `DialectNotAdaptedException`. + ### Capability-Driven Test Execution -Tests automatically skip when required capabilities are unavailable: +Tests branch on the same `supports_*` switches rather than on a decorator: ```python -from rhosocial.activerecord.backend.capabilities import ( - CapabilityCategory, - CTECapability, -) -from rhosocial.activerecord.testsuite.utils import requires_capability - -@requires_capability(CapabilityCategory.CTE, CTECapability.RECURSIVE_CTE) -def test_recursive_cte(tree_fixtures): - """Test requires recursive CTE support.""" - Node = tree_fixtures[0] - # Test implementation +dialect = backend.dialect +if dialect.supports_recursive_cte(): + ... # exercise the recursive-CTE path ``` +Note that `supports_*` names are not uniform: some take an argument (for +example `supports_collation_name(name)`, `supports_json_table()`), and many +take none. + **Capability Version Tracking**: - New capabilities added in MINOR versions @@ -3021,7 +3010,7 @@ rhosocial-activerecord-{backend}/ **Backend Class**: `{Backend}Backend` -- Examples: `MySQLBackend`, `PostgreSQLBackend` +- Examples: `MySQLBackend`, `PostgresBackend` (note: not `PostgreSQLBackend`) #### Interface Compliance @@ -3030,7 +3019,7 @@ All backends MUST implement: 1. **StorageBackend Interface**: ```python - from rhosocial.activerecord.backend import StorageBackend + from rhosocial.activerecord.backend.base import StorageBackend class MyBackend(StorageBackend): def connect(self) -> None: ... @@ -3045,20 +3034,24 @@ All backends MUST implement: 2. **Capability Declaration**: + There is no `_initialize_capabilities()` and no `DatabaseCapabilities` + object. Declare capabilities by implementing the `supports_*` switches on + the dialect: + ```python - def _initialize_capabilities(self) -> DatabaseCapabilities: - """Declare backend capabilities.""" - capabilities = DatabaseCapabilities() - # Add supported capabilities based on version/config - return capabilities + def supports_basic_cte(self) -> bool: + return self.version >= (8, 0, 0) ``` 3. **Test Provider Implementation**: ```python - from rhosocial.activerecord.testsuite.core import IProvider + # There is no `IProvider`. The testsuite defines per-category interfaces: + # IBasicProvider, IQueryProvider, IRelationProvider, IEventsProvider, + # IMixinsProvider (see tests/providers/registry.py). + from rhosocial.activerecord.testsuite.core import IBasicProvider - class MyBackendProvider(IProvider): + class MyBackendProvider(IBasicProvider): def setup_fixtures(self, scenario: str) -> Tuple[Type[ActiveRecord], ...]: # Setup models and schemas pass @@ -3070,36 +3063,31 @@ All backends MUST implement: #### Capability Declaration Requirements -Backends MUST accurately declare capabilities: +Backends MUST answer the `supports_*` switches truthfully, against the real +`self.version` populated by `introspect_and_adapt()`: ```python -def _initialize_capabilities(self): - capabilities = DatabaseCapabilities() - version = self.get_server_version() - - # Example: MySQL 8.0+ features - if version >= (8, 0, 0): - capabilities.add_cte([ - CTECapability.BASIC_CTE, - CTECapability.RECURSIVE_CTE, - ]) - capabilities.add_window_function(ALL_WINDOW_FUNCTIONS) - - # JSON operations - if version >= (5, 7, 0): - capabilities.add_json([ - JSONCapability.JSON_EXTRACT, - JSONCapability.JSON_SET, - ]) - - return capabilities +# mixins/cte.py +def supports_basic_cte(self) -> bool: + return self.version >= (8, 0, 0) + +# mixins/window.py +def supports_window_functions(self) -> bool: + return self.version >= (8, 0, 0) + +# mixins/json.py -- note the patch level: 5.7.0 is NOT enough +def supports_json_type(self) -> bool: + return self.version >= (5, 7, 8) ``` **Capability Testing**: -- Backend must pass all tests for declared capabilities -- Tests automatically skip for unsupported capabilities -- False capability declarations fail during CI +- Tests branch on the same switches the renderer consults +- A gate that claims a feature the server lacks fails at DDL time, not + silently +- The version ladder used to exercise gates is a *lower bound*: probe below + the oldest real release too, or a capability that is really "available + everywhere" gets misreported as gated at the floor #### Version Management for Extensions @@ -3177,7 +3165,7 @@ Forks creating independent implementations have full autonomy but should: ```python # Still use rhosocial.activerecord namespace - from rhosocial.activerecord.backend import StorageBackend + from rhosocial.activerecord.backend.base import StorageBackend ``` 2. **Document Compatibility**: diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index b0b920fc..abc4ec9f 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -1,4 +1,22 @@ # .github/workflows/changelog-check.yml +# +# Enforces the rhosocial changelog fragment rules. +# +# feature/* or fix/* -> release/* | maint/*| main +# - a feature/fix branch must not target main directly +# - the PR must add at least one changelog.d/..md fragment +# - every added fragment's must equal the PR number +# - fragments already present in changelog.d/ from previously merged +# branches are ignored; only files ADDED by this PR are considered +# +# release/* -> main +# - CHANGELOG.md must have changed (content is not inspected) +# - changelog.d/ must contain no fragments at the merge commit +# +# Escape hatches (all three retained): a [trivial] marker in the PR title, +# the skip-changelog label, or a "docs: update CHANGELOG" title. An escape +# hatch waives the "at least one fragment" requirement only -- the +# number-must-match-PR rule is always enforced on fragments that were added. name: Changelog Fragment Check on: @@ -9,12 +27,9 @@ on: - 'maint/**' push: branches: + - main - 'release/v**' - 'maint/**' - paths-ignore: - - 'docs/**' - - '**.md' - - '.github/workflows/**' permissions: contents: read @@ -28,95 +43,199 @@ jobs: with: fetch-depth: 0 - - name: Check for changelog fragment + - name: Validate changelog fragments env: - PR_TITLE: ${{ github.event.pull_request.title || '' }} EVENT_NAME: ${{ github.event_name }} + HEAD_REF: ${{ github.head_ref || '' }} + BASE_REF: ${{ github.base_ref || '' }} + PR_NUMBER: ${{ github.event.pull_request.number || '' }} + PR_TITLE: ${{ github.event.pull_request.title || '' }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} run: | - # For push events, check if there are uncommitted changelog fragments - if [[ "$EVENT_NAME" == "push" ]]; then - echo "Push event detected - checking for changelog fragments in last commit" + set -uo pipefail + + # Towncrier fragment types. Advisory only: a project may legitimately + # configure a different set in pyproject.toml [tool.towncrier.type]. + KNOWN_TYPES="added changed deprecated removed fixed performance security docs internal" + + # Files that live in changelog.d/ but are not fragments. + is_boilerplate() { + case "${1##*/}" in + README.md|template.md|.gitkeep) return 0 ;; + *) return 1 ;; + esac + } + + # Validate against (empty = don't care). + # Prints the fragment type on success. + validate_fragment() { + local path="$1" expected="$2" + local base num type + + base="${path##*/}" + + if [[ ! "$base" =~ ^([0-9]+)\.([a-zA-Z]+)\.md$ ]]; then + echo "::error file=$path::fragment must be named ..md, got '$base'" + return 1 + fi + num="${BASH_REMATCH[1]}" + type="${BASH_REMATCH[2]}" - # Check if last commit already contains a changelog fragment - LAST_COMMIT_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD) + if [[ -n "$expected" && "$num" != "$expected" ]]; then + echo "::error file=$path::fragment number '$num' does not match PR #$expected" + echo " Rename it to changelog.d/$expected.$type.md, or open a PR for #$num." + return 1 + fi - if echo "$LAST_COMMIT_FILES" | grep -q "^changelog.d/[0-9].*\.md$"; then - echo "✓ Changelog fragment found in last commit" - exit 0 + if [[ " $KNOWN_TYPES " != *" $type "* ]]; then + echo "::warning file=$path::'$type' is not a standard towncrier type" >&2 fi - # Check if last commit is a version bump or changelog build - LAST_MSG=$(git log -1 --pretty=%B) - if [[ "$LAST_MSG" =~ ^chore:\ bump\ version ]] || [[ "$LAST_MSG" =~ ^docs:\ update\ CHANGELOG ]]; then - echo "✓ Version bump or changelog update commit, fragment not required" - exit 0 + echo "$type" + return 0 + } + + is_exempt() { + [[ "$PR_TITLE" == *"[trivial]"* ]] && return 0 + [[ ",$PR_LABELS," == *",skip-changelog,"* ]] && return 0 + [[ "$PR_TITLE" == "docs: update CHANGELOG"* ]] && return 0 + return 1 + } + + # ------------------------------------------------------------------ + # release/* -> main : CHANGELOG must change, changelog.d/ must be empty + # ------------------------------------------------------------------ + check_release_merge() { + local scope="$1" changed remaining failed=0 + + changed=$(git diff --name-only "origin/main...HEAD" -- CHANGELOG.md) + if [[ -z "$changed" ]]; then + echo "::error::CHANGELOG.md was not updated by this $scope." + echo " A release merge must regenerate the changelog (e.g. \`towncrier build --yes --keep\`)" + echo " before the fragments under changelog.d/ are discarded." + failed=1 + else + echo "OK: CHANGELOG.md updated" fi - # Check if there are any source code changes (excluding tests/docs) - if echo "$LAST_COMMIT_FILES" | grep -qE "^src/.*\.py$"; then - echo "⚠ Warning: Source code changes without changelog fragment" - echo "Consider adding a changelog fragment for traceability" - echo "Changed files: $LAST_COMMIT_FILES" - # Warning only, don't fail the build - exit 0 + # Checked against the merge result, not the diff: leftover fragments + # from earlier in the release cycle must be gone by this point. + remaining=$(git ls-tree --name-only HEAD changelog.d/ \ + | while read -r f; do + if ! is_boilerplate "$f" \ + && [[ "${f##*/}" =~ ^[0-9]+\.[a-zA-Z]+\.md$ ]]; then + echo "$f" + fi + done) + if [[ -n "$remaining" ]]; then + echo "::error::changelog.d/ still contains fragments after the release merge:" + echo "$remaining" | while read -r f; do echo " - $f"; done + echo " Delete them; their content is already folded into CHANGELOG.md." + failed=1 + else + echo "OK: changelog.d/ is clear of fragments" fi - echo "✓ No source code changes requiring fragment" + return $failed + } + + # ------------------------------------------------------------------ + # push event: no PR number available, so only structural checks + # ------------------------------------------------------------------ + if [[ "$EVENT_NAME" == "push" ]]; then + if [[ "$GITHUB_REF_NAME" == "main" ]]; then + touched=$(git diff-tree --no-commit-id --name-only -r HEAD) + if ! echo "$touched" | grep -qE '^(CHANGELOG\.md|changelog\.d/|src/)'; then + echo "OK: no changelog-relevant changes in this push" + exit 0 + fi + check_release_merge "push to main" || exit 1 + exit $? + fi + echo "OK: push to $GITHUB_REF_NAME (fragment numbering is validated on the PR)" exit 0 fi - # For pull_request events, use the original logic + # ------------------------------------------------------------------ + # pull_request + # ------------------------------------------------------------------ + echo "head=$HEAD_REF base=$BASE_REF pr=#$PR_NUMBER" - # Fetch the base branch explicitly to ensure it's available for comparison - git fetch origin ${{ github.base_ref }} - - # Compare the head of the PR (current HEAD) with the base branch's remote tracking ref - # This compares the changes introduced by the PR branch relative to its base. - # Use --diff-filter=A to only consider added files for fragment check - CHANGED_FILES=$(git diff --name-only --diff-filter=A origin/${{ github.base_ref }}...HEAD) - - # Debugging: Print the diff command and its output - echo "Executing: git diff --name-only --diff-filter=A origin/${{ github.base_ref }}...HEAD" - echo "Changed files (added only): $CHANGED_FILES" - - # --- NEW EXEMPTION FOR CHANGELOG BUILD PRS --- - # Check if PR title indicates a changelog update (e.g., "docs: update CHANGELOG") - # This exempts PRs that are specifically for building the changelog (which remove fragments) - if [[ "$PR_TITLE" =~ ^docs:\ update\ CHANGELOG ]]; then - echo "✓ PR title indicates changelog update, fragment check skipped" + if [[ "$HEAD_REF" == release/* && "$BASE_REF" == "main" ]]; then + git fetch origin main --quiet + echo "== release merge into main ==" + check_release_merge "release merge" || exit 1 exit 0 fi - # --- END NEW EXEMPTION --- - # Check if any changelog fragment was added - if echo "$CHANGED_FILES" | grep -q "^changelog.d/[0-9].*\.md$"; then - echo "✓ Changelog fragment found" - exit 0 + # A feature/fix branch never merges straight into main; it targets a + # release or maintenance branch. Opening a second PR at main from the + # same branch is a duplicate, and it also invalidates the fragment + # numbering, which is bound to a single PR number. + if [[ ("$HEAD_REF" == feature/* || "$HEAD_REF" == fix/* || "$HEAD_REF" == bugfix/* \ + || "$HEAD_REF" == test/* || "$HEAD_REF" == docs/* || "$HEAD_REF" == chore/*) \ + && "$BASE_REF" == "main" ]]; then + echo "::error::'$HEAD_REF' must not target \`main\` directly." + echo "" + echo "Per the rhosocial merge rules:" + echo " feature/* fix/* bugfix/* test/* docs/* chore/* -> release/* | maint/*" + echo " release/* -> main" + echo "" + echo "Retarget this PR at the active release branch, or close it if" + echo "another PR from '$HEAD_REF' already targets the release branch." + echo "The changelog fragment is numbered for one PR only, so a duplicate" + echo "PR at main would also fail the fragment numbering check." + echo "" + git branch -r --list "origin/release/*" | sed 's#.*origin/##' | sed 's/^/ /' + exit 1 fi - # Check if this is an internal change (exempt from fragment requirement) - if echo "$CHANGED_FILES" | grep -qE "^(tests/|docs/|\.github/)"; then - echo "✓ Internal change, fragment not required" - exit 0 + git fetch origin "$BASE_REF" --quiet + + # Fragments ADDED by this PR. Fragments that were already in + # changelog.d/ (carried in by previously merged branches) are not in + # this list and are therefore ignored. + added=$(git diff --name-only --diff-filter=A "origin/$BASE_REF...HEAD" -- 'changelog.d/*.md') + + failed=0 + types="" + if [[ -n "$added" ]]; then + while read -r f; do + [[ -z "$f" ]] && continue + is_boilerplate "$f" && continue + t=$(validate_fragment "$f" "$PR_NUMBER") || failed=1 + [[ -n "$t" ]] && types="$types $t" + done <<< "$added" fi - # Check if PR is marked as trivial - if [[ "$PR_TITLE" =~ \[trivial\] ]]; then - echo "✓ PR marked as trivial, fragment not required" - exit 0 + # The number-must-match rule is enforced even for exempt PRs; only + # the presence requirement is waived. + if [[ "$failed" -ne 0 ]]; then + echo "" + echo "✗ Fragment numbering does not match PR #$PR_NUMBER" + exit 1 + fi + + if [[ -z "$added" ]]; then + if is_exempt; then + echo "OK: no fragment required (exempt)" + exit 0 + fi + if git diff --name-only "origin/$BASE_REF...HEAD" \ + | grep -qE '^(tests/|docs/|\.github/)' \ + && ! git diff --name-only "origin/$BASE_REF...HEAD" | grep -qE '^src/'; then + echo "OK: internal-only change, no fragment required" + exit 0 + fi + echo "::error::No changelog fragment added by this PR." + echo " Add at least one changelog.d/$PR_NUMBER..md" + echo "" + echo "Types: added | changed | deprecated | removed | fixed |" + echo " performance | security | docs | internal" + echo "" + echo "Or waive it: add [trivial] to the PR title, apply the" + echo "skip-changelog label, or title the PR 'docs: update CHANGELOG'." + exit 1 fi - echo "✗ Missing changelog fragment" - echo "Please add a changelog fragment in changelog.d/" - echo "Example: changelog.d/${{ github.event.number }}.fixed.md" - echo "" - echo "Fragment types:" - echo " - added.md - New features" - echo " - fixed.md - Bug fixes" - echo " - changed.md - Behavior changes" - echo " - deprecated.md - Deprecations" - echo " - removed.md - Removed features" - echo " - security.md - Security fixes" - echo "" - echo "If this PR does not need a changelog entry, add [trivial] to the title." - exit 1 + echo "OK: fragment(s) added for PR #$PR_NUMBER ->$types" diff --git a/README.md b/README.md index 5fa4d1e2..96365207 100644 --- a/README.md +++ b/README.md @@ -5,289 +5,211 @@ [![Tests](https://github.com/rhosocial/python-activerecord-postgres/actions/workflows/test.yml/badge.svg)](https://github.com/rhosocial/python-activerecord-postgres/actions) [![Coverage Status](https://codecov.io/gh/rhosocial/python-activerecord-postgres/branch/main/graph/badge.svg)](https://app.codecov.io/gh/rhosocial/python-activerecord-postgres/tree/main) [![Apache 2.0 License](https://img.shields.io/github/license/rhosocial/python-activerecord-postgres.svg)](https://github.com/rhosocial/python-activerecord-postgres/blob/main/LICENSE) -[![Powered by vistart](https://img.shields.io/badge/Powered_by-vistart-blue.svg)](https://github.com/vistart)
rhosocial ActiveRecord Logo

PostgreSQL Backend for rhosocial-activerecord

-

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

+

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

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

Built with ❤️ by the rhosocial team

-

GitHub · Documentation · PyPI

-
diff --git a/changelog.d/60.fixed.md b/changelog.d/60.fixed.md new file mode 100644 index 00000000..da73f2de --- /dev/null +++ b/changelog.d/60.fixed.md @@ -0,0 +1 @@ +Forward SSL/TLS connection parameters to the driver. `PostgresBackend.connect()` and `AsyncPostgresBackend.connect()` read `ssl_ca`/`ssl_cert`/`ssl_key`/`ssl_mode`, but `PostgresConnectionConfig` inherits only the libpq-named `PostgresSSLMixin` (`sslmode`/`sslcert`/`sslkey`/`sslrootcert`/`sslcrl`/`sslcompression`) and not the generic `SSLMixin`, so every `hasattr()` check was `False` and no TLS setting ever reached `psycopg.connect()`. Connections therefore ran unencrypted even with `sslmode="verify-full"`, and an unreadable `sslrootcert` was silently ignored. The shared `PostgresConnectionConfig.get_ssl_connection_params()` now collects the configured libpq keywords once for both the sync and async paths, and the kwargs extraction lists in both backends accept the libpq names instead of the generic ones. diff --git a/docs/en_US/ddl/materialized_view.md b/docs/en_US/ddl/materialized_view.md index 4da9e612..6ad5253f 100644 --- a/docs/en_US/ddl/materialized_view.md +++ b/docs/en_US/ddl/materialized_view.md @@ -25,7 +25,7 @@ usable; they simply do not carry `schema` / `if_not_exists`. ```python from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression, TableExpression -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateMaterializedViewExpression, ) @@ -86,7 +86,7 @@ standard heap relation options, enumerated by [`PostgresStorageParameter`](../../api/storage_parameters.md): ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter PostgresStorageParameter.FILLFACTOR.value # 'fillfactor' PostgresStorageParameter.FILLFACTOR.value_type # PostgresStorageParameterValueType.INT diff --git a/docs/en_US/installation_and_configuration/README.md b/docs/en_US/installation_and_configuration/README.md index 47d47b87..a6d4d34a 100644 --- a/docs/en_US/installation_and_configuration/README.md +++ b/docs/en_US/installation_and_configuration/README.md @@ -17,10 +17,8 @@ pip install rhosocial-activerecord-postgres ``` ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", diff --git a/docs/en_US/installation_and_configuration/backend_group.md b/docs/en_US/installation_and_configuration/backend_group.md index 5fa0b2f3..4c05b9c0 100644 --- a/docs/en_US/installation_and_configuration/backend_group.md +++ b/docs/en_US/installation_and_configuration/backend_group.md @@ -6,7 +6,8 @@ This document describes how to use `BackendGroup` and `BackendManager` with the ```python from rhosocial.activerecord.connection import BackendGroup -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.model import ActiveRecord diff --git a/docs/en_US/installation_and_configuration/configuration.md b/docs/en_US/installation_and_configuration/configuration.md index cf1c95c7..8404d30c 100644 --- a/docs/en_US/installation_and_configuration/configuration.md +++ b/docs/en_US/installation_and_configuration/configuration.md @@ -3,7 +3,7 @@ ## Basic Configuration ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", @@ -49,7 +49,7 @@ For security, use environment variables: ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host=os.getenv("PG_HOST", "localhost"), diff --git a/docs/en_US/installation_and_configuration/installation.md b/docs/en_US/installation_and_configuration/installation.md index 2e4cd4d9..5835a6f9 100644 --- a/docs/en_US/installation_and_configuration/installation.md +++ b/docs/en_US/installation_and_configuration/installation.md @@ -27,7 +27,7 @@ pip install rhosocial-activerecord-postgres[test,dev,docs] ## Verifying Installation ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend print("PostgreSQL backend installed successfully!") ``` diff --git a/docs/en_US/installation_and_configuration/pool.md b/docs/en_US/installation_and_configuration/pool.md index e88356db..e0082fa3 100644 --- a/docs/en_US/installation_and_configuration/pool.md +++ b/docs/en_US/installation_and_configuration/pool.md @@ -5,7 +5,7 @@ The PostgreSQL backend uses a "connect-on-use" pattern by default: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) # Connection is NOT established yet @@ -55,7 +55,7 @@ pip install rhosocial-activerecord-postgres[pooling] ``` ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = PostgresConnectionConfig( # ... basic config ... diff --git a/docs/en_US/introduction/relationship.md b/docs/en_US/introduction/relationship.md index 82795e8b..f4595946 100644 --- a/docs/en_US/introduction/relationship.md +++ b/docs/en_US/introduction/relationship.md @@ -24,7 +24,7 @@ rhosocial-activerecord-postgres (Backend) The PostgreSQL backend registers itself with the core library through the standard backend interface: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # Configure model with PostgreSQL backend User.configure( diff --git a/docs/en_US/plugins/README.md b/docs/en_US/plugins/README.md index 50d0e137..87f523d3 100644 --- a/docs/en_US/plugins/README.md +++ b/docs/en_US/plugins/README.md @@ -17,7 +17,7 @@ The backend automatically detects server capabilities and installed extensions v ### Basic Usage ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend( host='localhost', diff --git a/docs/en_US/postgres_specific_features/dialect.md b/docs/en_US/postgres_specific_features/dialect.md index f6c95709..68279df6 100644 --- a/docs/en_US/postgres_specific_features/dialect.md +++ b/docs/en_US/postgres_specific_features/dialect.md @@ -8,8 +8,8 @@ PostgreSQL uses the `::` operator for type casting, which is PostgreSQL-specific ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import INTEGER, NUMERIC, MONEY +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import INTEGER, NUMERIC, MONEY dialect = PostgresDialect() @@ -32,8 +32,8 @@ PostgreSQL supports chained type conversions: ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import MONEY, NUMERIC, FLOAT8 +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC, FLOAT8 dialect = PostgresDialect() col = Column(dialect, "amount") @@ -63,7 +63,7 @@ When using incompatible type conversions, the system will emit a warning to guid For convenience, PostgreSQL type constants are provided: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # Numeric types SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/en_US/postgres_specific_features/field_types.md b/docs/en_US/postgres_specific_features/field_types.md index faf6cc32..109aee70 100644 --- a/docs/en_US/postgres_specific_features/field_types.md +++ b/docs/en_US/postgres_specific_features/field_types.md @@ -5,7 +5,7 @@ For convenience, PostgreSQL type constants are provided. These constants are string values that can be used in type casting: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # Numeric types SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/en_US/postgres_specific_features/introspection.md b/docs/en_US/postgres_specific_features/introspection.md index 9ed2bc3b..78c5fee2 100644 --- a/docs/en_US/postgres_specific_features/introspection.md +++ b/docs/en_US/postgres_specific_features/introspection.md @@ -19,9 +19,8 @@ The PostgreSQL introspection system is accessible via `backend.introspector` and ### Accessing the Introspector ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend - -backend = PostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +backend = PostgresBackend( host="localhost", port=5432, database="mydb", @@ -254,9 +253,8 @@ for idx in indexes: The async backend provides identical introspection methods with the same names as the sync version: ```python -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend - -backend = AsyncPostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +backend = AsyncPostgresBackend( host="localhost", port=5432, database="mydb", diff --git a/docs/en_US/postgres_specific_features/property_graph_query.md b/docs/en_US/postgres_specific_features/property_graph_query.md index 4d46e690..37412a3b 100644 --- a/docs/en_US/postgres_specific_features/property_graph_query.md +++ b/docs/en_US/postgres_specific_features/property_graph_query.md @@ -35,7 +35,7 @@ feature. ## Capability Detection ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect dialect = PostgresDialect(version=(19, 0, 4)) assert dialect.supports_graph_match() is False diff --git a/docs/en_US/postgres_specific_features/protocol_support.md b/docs/en_US/postgres_specific_features/protocol_support.md index d1286a35..7798dfdd 100644 --- a/docs/en_US/postgres_specific_features/protocol_support.md +++ b/docs/en_US/postgres_specific_features/protocol_support.md @@ -60,7 +60,7 @@ PostgreSQL provides comprehensive support for SQL set operations: **Example:** ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect dialect = PostgresDialect() assert dialect.supports_union() # True @@ -86,7 +86,7 @@ PostgreSQL supports TRUNCATE TABLE with several database-specific features: **Example:** ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.expression.statements import TruncateExpression # Basic truncate @@ -521,7 +521,7 @@ After calling `introspect_and_adapt()`, the following is cached: The framework provides a convenient method for version-aware feature detection: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # Connect and introspect (one-time) backend = PostgresBackend(...) @@ -661,7 +661,7 @@ backend.introspect_and_adapt() ### Checking Protocol Support ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.dialect.protocols import ( SetOperationSupport, TruncateSupport, CTESupport ) @@ -683,7 +683,7 @@ assert dialect.supports_merge_statement() # False (requires ≥ 15) ### Version-Specific Feature Detection ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect # Old PostgreSQL version old_dialect = PostgresDialect(version=(8, 3, 0)) diff --git a/docs/en_US/scenarios/parallel_workers.md b/docs/en_US/scenarios/parallel_workers.md index cfe2f873..1400c991 100644 --- a/docs/en_US/scenarios/parallel_workers.md +++ b/docs/en_US/scenarios/parallel_workers.md @@ -22,8 +22,8 @@ In scenarios such as data processing, task queues, and batch imports, developers `rhosocial-activerecord` follows the core design principle of **one ActiveRecord class bound to one connection**: -- **Sync**: `Post.configure(config, PostgreSQLBackend)` → writes to `Post.__backend__` -- **Async**: `await Post.configure(config, AsyncPostgreSQLBackend)` → writes to `Post.__backend__` +- **Sync**: `Post.configure(config, PostgresBackend)` → writes to `Post.__backend__` +- **Async**: `await Post.configure(config, AsyncPostgresBackend)` → writes to `Post.__backend__` The configuration writes to a **class-level attribute**, which is still unsafe for multi-threading. PostgreSQL differs from other databases in the following ways: @@ -59,19 +59,20 @@ Multi-processing is the recommended approach for parallel worker scenarios. Each ```python import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import Comment, Post, User def worker(post_ids: list[int]): # 1. After process starts, configure connection within process - each process establishes independent TCP connection - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - User.configure(config, PostgreSQLBackend) + User.configure(config, PostgresBackend) Post.__backend__ = User.backend() Comment.__backend__ = User.backend() @@ -103,16 +104,17 @@ if __name__ == "__main__": ```python import asyncio import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import AsyncComment, AsyncPost, AsyncUser async def async_worker_main(post_ids: list[int]): # 1. Configure async connection within process (psycopg native async, TCP network I/O) - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - await AsyncUser.configure(config, AsyncPostgreSQLBackend) + await AsyncUser.configure(config, AsyncPostgresBackend) AsyncPost.__backend__ = AsyncUser.backend() AsyncComment.__backend__ = AsyncUser.backend() @@ -161,7 +163,7 @@ Runnable timing comparison demo at [exp1_basic_multiprocess.py](../../examples/c ### 3.1 Single Connection Model Constraints -`rhosocial-activerecord`'s async PostgreSQL backend (`AsyncPostgreSQLBackend`) is based on `psycopg` version 3's async interface, **each ActiveRecord class binds to one connection**. This differs from connection pool solutions: +`rhosocial-activerecord`'s async PostgreSQL backend (`AsyncPostgresBackend`) is based on `psycopg` version 3's async interface, **each ActiveRecord class binds to one connection**. This differs from connection pool solutions: | Feature | Single Connection ORM (This Project) | Connection Pool Solution | | --- | --- | --- | @@ -397,7 +399,7 @@ When a system contains two distinctly different workloads, it is recommended to | Workload Type | Characteristics | Suitable Deployment | | --- | --- | --- | | Web API Service | Short requests, high concurrency, response time sensitive | FastAPI / Django + asyncio + connection pool | -| Data Analysis Batch Processing | Long running, large data volume, CPU intensive | Standalone script + multiprocessing + PostgreSQLBackend | +| Data Analysis Batch Processing | Long running, large data volume, CPU intensive | Standalone script + multiprocessing + PostgresBackend | | Task Queue Consumer | Regular polling, independent tasks, easy horizontal scaling | Standalone worker process pool | ### 5.2 Recommended Architecture @@ -408,7 +410,7 @@ User requests ──→ Web application (asyncio + connection pool) └──→ Task queue (PostgreSQL database table / Redis) │ └──→ Background worker process pool - (Each process independent PostgreSQLBackend sync connection) + (Each process independent PostgresBackend sync connection) ``` Web application receives requests and writes to task queue; worker process pool executes time-consuming tasks. @@ -518,7 +520,7 @@ async def async_worker_task(user_id, conn_params): async def async_worker_task(user_id, conn_params): # Only pass connection parameters (serializable), create new instance in child process config = conn_params['config_kwargs'] - await Model.configure(config, AsyncPostgreSQLBackend) + await Model.configure(config, AsyncPostgresBackend) user = await Model.find_one(user_id) await Model.backend().disconnect() ``` @@ -550,8 +552,8 @@ Sync test coverage is higher; async test coverage has uncovered code paths due t ### 8.3 Production Recommendations -1. **Sync Workers Preferred**: In multi-process Worker scenarios, sync backend (`PostgreSQLBackend`) is the more stable choice -2. **Async for Single Process**: Async backend (`AsyncPostgreSQLBackend`) works well in single-process sequential execution scenarios +1. **Sync Workers Preferred**: In multi-process Worker scenarios, sync backend (`PostgresBackend`) is the more stable choice +2. **Async for Single Process**: Async backend (`AsyncPostgresBackend`) works well in single-process sequential execution scenarios 3. **Avoid Cross-Process Async Instance Passing**: Only pass serializable connection parameters, create new async backend instances in child processes ### 8.4 FOR UPDATE Capability Detection Experience @@ -751,7 +753,7 @@ In FastAPI + PostgreSQL + async backend scenario: ```python # database.py - Connection pool manager from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend # Global connection pool (created at application startup) _pool: AsyncBackendPool = None diff --git a/docs/en_US/testing/configuration.md b/docs/en_US/testing/configuration.md index 686f9c17..a5dbd6e8 100644 --- a/docs/en_US/testing/configuration.md +++ b/docs/en_US/testing/configuration.md @@ -10,9 +10,8 @@ The `dummy` backend is recommended for unit tests as it does not require a real ```python from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.dummy import DummyBackend, DummyConnectionConfig - - +from rhosocial.activerecord.backend.impl.dummy.backend import DummyBackend +rhosocial.activerecord.backend.config import ConnectionConfig class User(ActiveRecord): name: str email: str @@ -25,7 +24,7 @@ class User(ActiveRecord): # Configure Dummy backend -config = DummyConnectionConfig() +config = ConnectionConfig() User.configure(config, DummyBackend) ``` @@ -34,7 +33,8 @@ User.configure(config, DummyBackend) For tests requiring real database behavior, use the SQLite backend: ```python -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend, SQLiteConnectionConfig +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.config import SQLiteConnectionConfig class User(ActiveRecord): @@ -59,9 +59,8 @@ For complete PostgreSQL behavior testing, use the PostgreSQL backend: ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig class User(ActiveRecord): name: str email: str @@ -74,26 +73,25 @@ class User(ActiveRecord): # Read configuration from environment variables -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host=os.environ.get('PG_HOST', 'localhost'), port=int(os.environ.get('PG_PORT', 5432)), database=os.environ.get('PG_DATABASE', 'test'), username=os.environ.get('PG_USER', 'postgres'), password=os.environ.get('PG_PASSWORD', ''), ) -User.configure(config, PostgreSQLBackend) +User.configure(config, PostgresBackend) ``` ## Test Fixtures ```python import pytest -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig @pytest.fixture def postgres_config(): - return PostgreSQLConnectionConfig( + return PostgresConnectionConfig( host='localhost', port=5432, database='test', @@ -104,7 +102,7 @@ def postgres_config(): @pytest.fixture def postgres_backend(postgres_config): - backend = PostgreSQLBackend(connection_config=postgres_config) + backend = PostgresBackend(connection_config=postgres_config) backend.connect() yield backend backend.disconnect() diff --git a/docs/en_US/transaction_support/README.md b/docs/en_US/transaction_support/README.md index 23b9ee02..76e07752 100644 --- a/docs/en_US/transaction_support/README.md +++ b/docs/en_US/transaction_support/README.md @@ -12,7 +12,7 @@ PostgreSQL provides robust transaction support with advanced features like savep ## Quick Start ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) tm = backend.transaction_manager diff --git a/docs/en_US/transaction_support/deferrable.md b/docs/en_US/transaction_support/deferrable.md index 6fe60d0b..b31ab365 100644 --- a/docs/en_US/transaction_support/deferrable.md +++ b/docs/en_US/transaction_support/deferrable.md @@ -9,7 +9,7 @@ Deferrable constraints are checked at transaction commit time, not at statement ## Usage ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/en_US/transaction_support/isolation_level.md b/docs/en_US/transaction_support/isolation_level.md index e35dda8f..7c95ee41 100644 --- a/docs/en_US/transaction_support/isolation_level.md +++ b/docs/en_US/transaction_support/isolation_level.md @@ -11,7 +11,7 @@ ## Setting Isolation Level ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/en_US/troubleshooting/connection.md b/docs/en_US/troubleshooting/connection.md index 7de866e0..2b50058a 100644 --- a/docs/en_US/troubleshooting/connection.md +++ b/docs/en_US/troubleshooting/connection.md @@ -62,7 +62,7 @@ psycopg.OperationalError: connection timeout expired ### Solutions ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', connect_timeout=30, # Increase timeout ) @@ -82,7 +82,7 @@ psycopg.OperationalError: SSL connection failed ### Solutions ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', sslmode='require', # Options: disable, prefer, require, verify-ca, verify-full sslrootcert='/path/to/ca.crt', # For verify-ca or verify-full @@ -256,7 +256,7 @@ SET GLOBAL idle_in_transaction_session_timeout = '10min'; For high-concurrency scenarios, configure connection pooling: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='mydb', # Connection pool settings diff --git a/docs/en_US/troubleshooting/performance.md b/docs/en_US/troubleshooting/performance.md index 23dcba5f..05f65218 100644 --- a/docs/en_US/troubleshooting/performance.md +++ b/docs/en_US/troubleshooting/performance.md @@ -20,10 +20,10 @@ SELECT pg_reload_conf(); ### Using EXPLAIN ANALYZE ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - -backend = PostgreSQLBackend( - connection_config=PostgreSQLConnectionConfig( +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +backend = PostgresBackend( + connection_config=PostgresConnectionConfig( host='localhost', database='myapp', username='user', @@ -87,7 +87,7 @@ VACUUM ANALYZE users; For high-concurrency applications, use connection pooling: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='myapp', username='user', diff --git a/docs/en_US/type_adapters/README.md b/docs/en_US/type_adapters/README.md index 7a5bf990..e09361ee 100644 --- a/docs/en_US/type_adapters/README.md +++ b/docs/en_US/type_adapters/README.md @@ -107,8 +107,10 @@ class User(ActiveRecord): For PostgreSQL-specific types with registered adapters, use the corresponding wrapper classes: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresMoney, Point, PostgresTsVector +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresMoney, + Point, + PostgresTsVector, ) class Order(ActiveRecord): @@ -136,9 +138,8 @@ backend.execute( **Method 2: Use PostgresRange wrapper (recommended for ActiveRecord models)** ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresRange, PostgresRangeAdapter -) +from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresRangeAdapter # Manually register the adapter backend.adapter_registry.register(PostgresRangeAdapter(), PostgresRange, str) @@ -152,10 +153,15 @@ class Order(ActiveRecord): For types with str→str mapping conflicts, you must explicitly register the adapter: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter, - PostgresJsonPath, PostgresJsonPathAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, + PostgresJsonPath, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, + PostgresJsonPathAdapter, ) # Method 1: Global registration on backend @@ -175,9 +181,13 @@ result = backend.execute( ```python from rhosocial.activerecord import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, ) class Document(ActiveRecord): diff --git a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py index d228055c..dce14a01 100644 --- a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py +++ b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py @@ -40,7 +40,7 @@ "python-activerecord-postgres", "src")) from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py index 076f25c0..ea3104ea 100644 --- a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py +++ b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py @@ -38,7 +38,7 @@ "python-activerecord-postgres", "src")) from rhosocial.activerecord.connection.pool import PoolConfig, BackendPool -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/docs/examples/chapter_04_ddl/ddl.py b/docs/examples/chapter_04_ddl/ddl.py index 7e0d6d2b..c446a1f8 100644 --- a/docs/examples/chapter_04_ddl/ddl.py +++ b/docs/examples/chapter_04_ddl/ddl.py @@ -10,7 +10,7 @@ from typing import ClassVar from rhosocial.activerecord.model import ActiveRecord from rhosocial.activerecord.base import FieldProxy -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.expression import ( ColumnDefinition, CreateTableExpression, @@ -39,7 +39,7 @@ def table_name(cls) -> str: def main(): # Configure with PostgreSQL backend - from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="db-dev-1-n.rho.im", diff --git a/docs/examples/chapter_05_worker_isolation/config.py b/docs/examples/chapter_05_worker_isolation/config.py index 29a01103..a3472d42 100644 --- a/docs/examples/chapter_05_worker_isolation/config.py +++ b/docs/examples/chapter_05_worker_isolation/config.py @@ -7,7 +7,7 @@ import yaml from typing import Optional # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig diff --git a/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py b/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py index a26a556a..90a11666 100644 --- a/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py +++ b/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py @@ -57,7 +57,7 @@ def worker_task_shared_connection(worker_id: int, num_operations: int, config_di WARNING: This will cause problems! """ - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig for model in ALL_MODELS: @@ -161,7 +161,7 @@ def run_experiment(config_dict: Dict[str, Any], num_workers: int, ops_per_worker # Configure globally ONCE (this is the anti-pattern) print("Step 1: Configuring ActiveRecord globally (shared connection)...") - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig(**config_dict) diff --git a/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py b/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py index a7967284..ef5200b1 100644 --- a/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py +++ b/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py @@ -79,7 +79,7 @@ def worker_task_isolated_connection( This is the correct approach for process-based parallelism. """ - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, Order, Post, Comment, ALL_MODELS # noqa: F401 @@ -211,7 +211,7 @@ def run_experiment(config_dict: Dict[str, Any], num_workers: int, ops_per_worker # Setup initial database state (in main process) print("Step 1: Setting up database...") - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, ALL_MODELS from config import setup_database diff --git a/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py b/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py index bb89f5af..a6b0ada6 100644 --- a/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py +++ b/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py @@ -93,7 +93,7 @@ def worker_task( use_isolated: If True, use isolated connection (correct pattern) If False, use shared connection (anti-pattern) """ - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, Order, Post, Comment, ALL_MODELS # noqa: F401 @@ -234,7 +234,7 @@ def run_endurance_test( stats = ExperimentStats() # Setup database - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, ALL_MODELS from config import setup_database diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py b/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py index 5748eeee..7f7d53ba 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py @@ -8,7 +8,7 @@ 3. Hardcoded defaults (localhost:5432/test_db/postgres/) Public interface: - load_config(scenario=None) → PostgreSQLConnectionConfig + load_config(scenario=None) → PostgresConnectionConfig list_scenarios() → list[str] """ @@ -24,7 +24,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # ─── Default preferred scenarios (by priority) ───────────────────────────────────── @@ -52,7 +52,7 @@ def load_config(scenario: Optional[str] = None) -> PostgresConnectionConfig: scenario: Scenario name in YAML (e.g., "postgres_16"). If None, auto-select by priority. Returns: - PostgreSQLConnectionConfig instance. + PostgresConnectionConfig instance. """ yaml_path = _find_scenarios_yaml() if yaml_path: diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py index a89b3e52..693e5ba9 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py @@ -38,10 +38,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import AsyncComment, AsyncPost, AsyncUser, Comment, Post, User # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py index c5beca4b..4d14e29a 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py @@ -39,10 +39,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import AsyncPost, Post # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py index bcdb6660..341f0ad9 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py @@ -36,7 +36,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import Post # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py index 872e4afc..eb162419 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py @@ -38,10 +38,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import AsyncComment, AsyncPost, AsyncUser, Comment, Post, User # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py index 25f82b2a..d30ac302 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py @@ -41,7 +41,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import Post # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py index 3b19ffb7..f6bf471f 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py @@ -32,7 +32,8 @@ from rhosocial.activerecord.connection import BackendGroup from rhosocial.activerecord.connection.pool import PoolConfig, BackendPool -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig # noqa: F401 +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from config_loader import load_config from models import Comment, Post, User diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py b/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py index 52b02048..4948d838 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py @@ -24,10 +24,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config, show_active_config # noqa: E402 from models import ( # noqa: E402 diff --git a/docs/examples/chapter_10_fastapi/config_loader.py b/docs/examples/chapter_10_fastapi/config_loader.py index 8b2b3ada..4a8b2f66 100644 --- a/docs/examples/chapter_10_fastapi/config_loader.py +++ b/docs/examples/chapter_10_fastapi/config_loader.py @@ -10,7 +10,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig def load_config() -> PostgresConnectionConfig: diff --git a/docs/examples/chapter_10_fastapi/database.py b/docs/examples/chapter_10_fastapi/database.py index bec2a2a4..c713ad74 100644 --- a/docs/examples/chapter_10_fastapi/database.py +++ b/docs/examples/chapter_10_fastapi/database.py @@ -28,7 +28,7 @@ from fastapi import Request # noqa: F401 from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from config_loader import load_config from models import AsyncUser, AsyncPost, AsyncComment diff --git a/docs/zh_CN/ddl/materialized_view.md b/docs/zh_CN/ddl/materialized_view.md index a8ef20ce..100fd1c1 100644 --- a/docs/zh_CN/ddl/materialized_view.md +++ b/docs/zh_CN/ddl/materialized_view.md @@ -24,7 +24,7 @@ ```python from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression, TableExpression -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateMaterializedViewExpression, ) @@ -83,7 +83,7 @@ create = PostgresCreateMaterializedViewExpression( 关系选项,已由 [`PostgresStorageParameter`](../../api/storage_parameters.md) 枚举: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter PostgresStorageParameter.FILLFACTOR.value # 'fillfactor' PostgresStorageParameter.FILLFACTOR.value_type # PostgresStorageParameterValueType.INT diff --git a/docs/zh_CN/installation_and_configuration/README.md b/docs/zh_CN/installation_and_configuration/README.md index 26d57ec6..10089e56 100644 --- a/docs/zh_CN/installation_and_configuration/README.md +++ b/docs/zh_CN/installation_and_configuration/README.md @@ -17,10 +17,8 @@ pip install rhosocial-activerecord-postgres ``` ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", diff --git a/docs/zh_CN/installation_and_configuration/configuration.md b/docs/zh_CN/installation_and_configuration/configuration.md index 1fa02337..ad411bee 100644 --- a/docs/zh_CN/installation_and_configuration/configuration.md +++ b/docs/zh_CN/installation_and_configuration/configuration.md @@ -3,7 +3,7 @@ ## 基础配置 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", @@ -49,7 +49,7 @@ config = PostgresConnectionConfig( ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host=os.getenv("PG_HOST", "localhost"), diff --git a/docs/zh_CN/installation_and_configuration/installation.md b/docs/zh_CN/installation_and_configuration/installation.md index ee416540..30937f08 100644 --- a/docs/zh_CN/installation_and_configuration/installation.md +++ b/docs/zh_CN/installation_and_configuration/installation.md @@ -27,7 +27,7 @@ pip install rhosocial-activerecord-postgres[test,dev,docs] ## 验证安装 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend print("PostgreSQL 后端安装成功!") ``` diff --git a/docs/zh_CN/installation_and_configuration/pool.md b/docs/zh_CN/installation_and_configuration/pool.md index 09987579..1915033a 100644 --- a/docs/zh_CN/installation_and_configuration/pool.md +++ b/docs/zh_CN/installation_and_configuration/pool.md @@ -5,7 +5,7 @@ PostgreSQL 后端默认使用"随用随连"模式: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) # 此时连接尚未建立 @@ -55,7 +55,7 @@ pip install rhosocial-activerecord-postgres[pooling] ``` ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = PostgresConnectionConfig( # ... 基础配置 ... diff --git a/docs/zh_CN/introduction/relationship.md b/docs/zh_CN/introduction/relationship.md index 84c1d667..6fdcee91 100644 --- a/docs/zh_CN/introduction/relationship.md +++ b/docs/zh_CN/introduction/relationship.md @@ -93,13 +93,10 @@ class User(UUIDMixin, TimestampMixin, ActiveRecord): ### 3. 配置后端 ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgreSQLConnectionConfig, -) - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # 配置 PostgreSQL 连接 -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', port=5432, database='myapp', diff --git a/docs/zh_CN/plugins/README.md b/docs/zh_CN/plugins/README.md index bd1b9c3d..902e2b71 100644 --- a/docs/zh_CN/plugins/README.md +++ b/docs/zh_CN/plugins/README.md @@ -17,7 +17,7 @@ PostgreSQL 扩展系统允许安装额外的功能模块,如: ### 基本用法 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend( host='localhost', diff --git a/docs/zh_CN/postgres_specific_features/dialect.md b/docs/zh_CN/postgres_specific_features/dialect.md index cadc1d1b..297e41e0 100644 --- a/docs/zh_CN/postgres_specific_features/dialect.md +++ b/docs/zh_CN/postgres_specific_features/dialect.md @@ -8,8 +8,8 @@ PostgreSQL 使用 `::` 操作符进行类型转换,这是 PostgreSQL 特有的 ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import INTEGER, NUMERIC, MONEY +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import INTEGER, NUMERIC, MONEY dialect = PostgresDialect() @@ -32,8 +32,8 @@ PostgreSQL 支持链式类型转换: ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import MONEY, NUMERIC, FLOAT8 +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC, FLOAT8 dialect = PostgresDialect() col = Column(dialect, "amount") @@ -63,7 +63,7 @@ col.cast("money").cast("numeric").cast("float8") 为方便使用,提供了 PostgreSQL 类型常量: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # 数值类型 SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/zh_CN/postgres_specific_features/field_types.md b/docs/zh_CN/postgres_specific_features/field_types.md index 4f8dc73d..80b97d52 100644 --- a/docs/zh_CN/postgres_specific_features/field_types.md +++ b/docs/zh_CN/postgres_specific_features/field_types.md @@ -5,7 +5,7 @@ 为方便使用,提供了 PostgreSQL 类型常量。这些常量是字符串值,可在类型转换中使用: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # 数值类型 SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/zh_CN/postgres_specific_features/introspection.md b/docs/zh_CN/postgres_specific_features/introspection.md index 8b630fef..53960aa8 100644 --- a/docs/zh_CN/postgres_specific_features/introspection.md +++ b/docs/zh_CN/postgres_specific_features/introspection.md @@ -19,9 +19,8 @@ PostgreSQL 内省系统位于 `backend.introspector` 属性中,提供: ### 访问内省器 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend - -backend = PostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +backend = PostgresBackend( host="localhost", port=5432, database="mydb", @@ -254,9 +253,8 @@ for idx in indexes: 异步后端提供相同的内省方法,方法名与同步版本相同: ```python -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend - -backend = AsyncPostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +backend = AsyncPostgresBackend( host="localhost", port=5432, database="mydb", diff --git a/docs/zh_CN/postgres_specific_features/property_graph_query.md b/docs/zh_CN/postgres_specific_features/property_graph_query.md index d88c5e8a..8165079c 100644 --- a/docs/zh_CN/postgres_specific_features/property_graph_query.md +++ b/docs/zh_CN/postgres_specific_features/property_graph_query.md @@ -30,7 +30,7 @@ PostgreSQL 19 Beta 4 在 19.0 正式发布前撤回了 SQL/PGQ,更早的版本 ## 能力检测 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect dialect = PostgresDialect(version=(19, 0, 4)) assert dialect.supports_graph_match() is False diff --git a/docs/zh_CN/postgres_specific_features/protocol_support.md b/docs/zh_CN/postgres_specific_features/protocol_support.md index 2aa95804..8d24edc5 100644 --- a/docs/zh_CN/postgres_specific_features/protocol_support.md +++ b/docs/zh_CN/postgres_specific_features/protocol_support.md @@ -377,7 +377,7 @@ dialect.supports_postgis_spatial_functions() # 使用缓存的扩展状态 框架提供了便捷的方法进行版本感知的特性检测: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # 连接并内省(一次性) backend = PostgresBackend(...) @@ -517,7 +517,7 @@ backend.introspect_and_adapt() ### 检查协议支持 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.dialect.protocols import ( SetOperationSupport, TruncateSupport, CTESupport ) @@ -539,7 +539,7 @@ assert dialect.supports_merge_statement() # False(需要 ≥ 15) ### 版本特定特性检测 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect # 旧版本 PostgreSQL old_dialect = PostgresDialect(version=(8, 3, 0)) diff --git a/docs/zh_CN/scenarios/parallel_workers.md b/docs/zh_CN/scenarios/parallel_workers.md index 2301406f..8862b3cf 100644 --- a/docs/zh_CN/scenarios/parallel_workers.md +++ b/docs/zh_CN/scenarios/parallel_workers.md @@ -22,8 +22,8 @@ `rhosocial-activerecord` 同样遵循**一个 ActiveRecord 类,绑定一条连接**的核心设计原则: -- **同步**:`Post.configure(config, PostgreSQLBackend)` → 写入 `Post.__backend__` -- **异步**:`await Post.configure(config, AsyncPostgreSQLBackend)` → 写入 `Post.__backend__` +- **同步**:`Post.configure(config, PostgresBackend)` → 写入 `Post.__backend__` +- **异步**:`await Post.configure(config, AsyncPostgresBackend)` → 写入 `Post.__backend__` 配置写入的是**类级别属性**,多线程间仍然不安全。PostgreSQL 在以下方面与其他数据库有差异: @@ -59,19 +59,20 @@ ```python import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import Comment, Post, User def worker(post_ids: list[int]): # 1. 进程启动后,在进程内配置连接——每个进程建立独立 TCP 连接 - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - User.configure(config, PostgreSQLBackend) + User.configure(config, PostgresBackend) Post.__backend__ = User.backend() Comment.__backend__ = User.backend() @@ -103,16 +104,17 @@ if __name__ == "__main__": ```python import asyncio import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import AsyncComment, AsyncPost, AsyncUser async def async_worker_main(post_ids: list[int]): # 1. 在进程内配置异步连接(psycopg 原生 async,TCP 网络 I/O) - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - await AsyncUser.configure(config, AsyncPostgreSQLBackend) + await AsyncUser.configure(config, AsyncPostgresBackend) AsyncPost.__backend__ = AsyncUser.backend() AsyncComment.__backend__ = AsyncUser.backend() @@ -161,7 +163,7 @@ if __name__ == "__main__": ### 3.1 单连接模型的约束 -`rhosocial-activerecord` 的异步 PostgreSQL 后端(`AsyncPostgreSQLBackend`)基于 `psycopg` 版本 3 的异步接口,**每个 ActiveRecord 类绑定一条连接**。这与连接池方案不同: +`rhosocial-activerecord` 的异步 PostgreSQL 后端(`AsyncPostgresBackend`)基于 `psycopg` 版本 3 的异步接口,**每个 ActiveRecord 类绑定一条连接**。这与连接池方案不同: | 特性 | 单连接 ORM(本项目) | 连接池方案 | | --- | --- | --- | @@ -414,7 +416,7 @@ def claim_posts_with_retry(batch_size: int = 5, max_retry: int = 3) -> list: | 工作负载类型 | 特征 | 合适部署 | | --- | --- | --- | | Web API 服务 | 短请求、高并发、响应时间敏感 | FastAPI / Django + asyncio + 连接池 | -| 数据分析批处理 | 长时间运行、大数据量、CPU 密集 | 独立脚本 + multiprocessing + PostgreSQLBackend | +| 数据分析批处理 | 长时间运行、大数据量、CPU 密集 | 独立脚本 + multiprocessing + PostgresBackend | | 任务队列消费 | 定期轮询、任务间独立、易于水平扩展 | 独立 Worker 进程池 | ### 5.2 推荐架构 @@ -425,7 +427,7 @@ def claim_posts_with_retry(batch_size: int = 5, max_retry: int = 3) -> list: └──→ 任务队列(PostgreSQL 数据库表 / Redis) │ └──→ 后台 Worker 进程池 - (每进程独立 PostgreSQLBackend 同步连接) + (每进程独立 PostgresBackend 同步连接) ``` Web 应用负责接收请求、写入任务队列;Worker 进程池负责执行耗时任务。 @@ -536,7 +538,7 @@ async def async_worker_task(user_id, conn_params): async def async_worker_task(user_id, conn_params): # 只传递连接参数(可序列化),在子进程内创建新实例 config = conn_params['config_kwargs'] - await Model.configure(config, AsyncPostgreSQLBackend) + await Model.configure(config, AsyncPostgresBackend) user = await Model.find_one(user_id) await Model.backend().disconnect() ``` @@ -568,8 +570,8 @@ Worker 测试对 PostgreSQL 后端的覆盖率贡献: ### 8.3 生产环境建议 -1. **同步 Worker 是首选**:在多进程 Worker 场景下,同步后端(`PostgreSQLBackend`)是更稳定的选择 -2. **异步适用于单进程**:异步后端(`AsyncPostgreSQLBackend`)在单进程内的顺序执行场景下表现良好 +1. **同步 Worker 是首选**:在多进程 Worker 场景下,同步后端(`PostgresBackend`)是更稳定的选择 +2. **异步适用于单进程**:异步后端(`AsyncPostgresBackend`)在单进程内的顺序执行场景下表现良好 3. **避免跨进程传递异步实例**:只传递可序列化的连接参数,在子进程内创建新的异步后端实例 ### 8.4 FOR UPDATE 能力检测经验 @@ -845,7 +847,7 @@ def thread_worker(group, thread_id): ```python # database.py - 连接池管理器 from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend # 全局连接池(应用启动时创建) _pool: AsyncBackendPool = None diff --git a/docs/zh_CN/testing/configuration.md b/docs/zh_CN/testing/configuration.md index e8b6d825..d3cdb972 100644 --- a/docs/zh_CN/testing/configuration.md +++ b/docs/zh_CN/testing/configuration.md @@ -10,9 +10,8 @@ ```python from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.dummy import DummyBackend, DummyConnectionConfig - - +from rhosocial.activerecord.backend.impl.dummy.backend import DummyBackend +rhosocial.activerecord.backend.config import ConnectionConfig class User(ActiveRecord): name: str email: str @@ -25,7 +24,7 @@ class User(ActiveRecord): # 配置 Dummy 后端 -config = DummyConnectionConfig() +config = ConnectionConfig() User.configure(config, DummyBackend) ``` @@ -34,7 +33,8 @@ User.configure(config, DummyBackend) 对于需要真实数据库行为的测试,使用 SQLite 后端: ```python -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend, SQLiteConnectionConfig +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.config import SQLiteConnectionConfig class User(ActiveRecord): @@ -59,9 +59,8 @@ User.configure(config, SQLiteBackend) ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig class User(ActiveRecord): name: str email: str @@ -74,26 +73,25 @@ class User(ActiveRecord): # 从环境变量读取配置 -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host=os.environ.get('PG_HOST', 'localhost'), port=int(os.environ.get('PG_PORT', 5432)), database=os.environ.get('PG_DATABASE', 'test'), username=os.environ.get('PG_USER', 'postgres'), password=os.environ.get('PG_PASSWORD', ''), ) -User.configure(config, PostgreSQLBackend) +User.configure(config, PostgresBackend) ``` ## 测试 Fixtures ```python import pytest -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig @pytest.fixture def postgres_config(): - return PostgreSQLConnectionConfig( + return PostgresConnectionConfig( host='localhost', port=5432, database='test', @@ -104,7 +102,7 @@ def postgres_config(): @pytest.fixture def postgres_backend(postgres_config): - backend = PostgreSQLBackend(connection_config=postgres_config) + backend = PostgresBackend(connection_config=postgres_config) backend.connect() yield backend backend.disconnect() diff --git a/docs/zh_CN/transaction_support/README.md b/docs/zh_CN/transaction_support/README.md index a95bfd8e..a3b95d2b 100644 --- a/docs/zh_CN/transaction_support/README.md +++ b/docs/zh_CN/transaction_support/README.md @@ -12,7 +12,7 @@ PostgreSQL 提供强大的事务支持,包括保存点、隔离级别和可延 ## 快速开始 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) tm = backend.transaction_manager diff --git a/docs/zh_CN/transaction_support/deferrable.md b/docs/zh_CN/transaction_support/deferrable.md index e30af2bb..bb5f7c5b 100644 --- a/docs/zh_CN/transaction_support/deferrable.md +++ b/docs/zh_CN/transaction_support/deferrable.md @@ -9,7 +9,7 @@ PostgreSQL 支持 SERIALIZABLE 事务的可延迟约束。 ## 使用方法 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/zh_CN/transaction_support/isolation_level.md b/docs/zh_CN/transaction_support/isolation_level.md index 4770c619..d384150a 100644 --- a/docs/zh_CN/transaction_support/isolation_level.md +++ b/docs/zh_CN/transaction_support/isolation_level.md @@ -11,7 +11,7 @@ ## 设置隔离级别 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/zh_CN/troubleshooting/connection.md b/docs/zh_CN/troubleshooting/connection.md index 64dc25c3..722d35cc 100644 --- a/docs/zh_CN/troubleshooting/connection.md +++ b/docs/zh_CN/troubleshooting/connection.md @@ -62,7 +62,7 @@ psycopg.OperationalError: connection timeout expired ### 解决方案 ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', connect_timeout=30, # 增加超时时间 ) @@ -82,7 +82,7 @@ psycopg.OperationalError: SSL connection failed ### 解决方案 ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', sslmode='require', # 选项:disable, prefer, require, verify-ca, verify-full sslrootcert='/path/to/ca.crt', # 用于 verify-ca 或 verify-full @@ -256,7 +256,7 @@ SET GLOBAL idle_in_transaction_session_timeout = '10min'; 对于高并发场景,配置连接池: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='mydb', # 连接池设置 diff --git a/docs/zh_CN/troubleshooting/performance.md b/docs/zh_CN/troubleshooting/performance.md index 2c98997b..11f46121 100644 --- a/docs/zh_CN/troubleshooting/performance.md +++ b/docs/zh_CN/troubleshooting/performance.md @@ -20,10 +20,10 @@ SELECT pg_reload_conf(); ### 使用 EXPLAIN ANALYZE ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - -backend = PostgreSQLBackend( - connection_config=PostgreSQLConnectionConfig( +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +backend = PostgresBackend( + connection_config=PostgresConnectionConfig( host='localhost', database='myapp', username='user', @@ -87,7 +87,7 @@ VACUUM ANALYZE users; 对于高并发应用,使用连接池: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='myapp', username='user', diff --git a/docs/zh_CN/type_adapters/README.md b/docs/zh_CN/type_adapters/README.md index 00dd59ae..bc1ee57f 100644 --- a/docs/zh_CN/type_adapters/README.md +++ b/docs/zh_CN/type_adapters/README.md @@ -107,8 +107,10 @@ class User(ActiveRecord): 对于已注册适配器的 PostgreSQL 特有类型,使用对应的包装类: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresMoney, Point, PostgresTsVector +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresMoney, + Point, + PostgresTsVector, ) class Order(ActiveRecord): @@ -136,9 +138,8 @@ backend.execute( **方式二:使用 PostgresRange 包装器(推荐 ActiveRecord 模型)** ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresRange, PostgresRangeAdapter -) +from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresRangeAdapter # 手动注册适配器 backend.adapter_registry.register(PostgresRangeAdapter(), PostgresRange, str) @@ -152,10 +153,15 @@ class Order(ActiveRecord): 对于存在 str→str 映射冲突的类型,必须显式注册适配器: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter, - PostgresJsonPath, PostgresJsonPathAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, + PostgresJsonPath, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, + PostgresJsonPathAdapter, ) # 方式一:在 backend 上全局注册 @@ -175,9 +181,13 @@ result = backend.execute( ```python from rhosocial.activerecord import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, ) class Document(ActiveRecord): diff --git a/pyproject.toml b/pyproject.toml index c96b3de0..82e58ffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,9 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "psycopg[binary]>=3.2.13", + # psycopg 3.3.6 requires Python 3.10+; 3.8 and 3.9 stay on the 3.2 line. + "psycopg[binary]>=3.3.6; python_version >= '3.10'", + "psycopg[binary]>=3.2.13; python_version < '3.10'", "rhosocial-activerecord>=1.0.0.dev0,<2.0.0" ] diff --git a/requirements.txt b/requirements.txt index 6f427b7e..e47bf885 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ # Core dependency (rhosocial-activerecord) is installed via CI workflow from git # PostgreSQL driver -psycopg[binary]>=3.2.13 \ No newline at end of file +# psycopg 3.3.6 requires Python 3.10+; 3.8 and 3.9 stay on the 3.2 line. +psycopg[binary]>=3.3.6; python_version >= '3.10' +psycopg[binary]>=3.2.13; python_version < '3.10' \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py deleted file mode 100644 index 1bd9a409..00000000 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ /dev/null @@ -1,682 +0,0 @@ -# src/rhosocial/activerecord/backend/impl/postgres/__init__.py -""" -PostgreSQL backend implementation for the Python ORM. - -This module provides: -- PostgreSQL synchronous backend with connection management and query execution -- PostgreSQL asynchronous backend with async/await support -- PostgreSQL-specific connection configuration -- Type mapping and value conversion -- Transaction management with savepoint support (sync and async) -- PostgreSQL dialect and expression handling -- PostgreSQL-specific type definitions and mappings - -Architecture: -- PostgreSQLBackend: Synchronous implementation using psycopg -- AsyncPostgreSQLBackend: Asynchronous implementation using psycopg -- Independent from ORM frameworks - uses only native drivers -""" - -from .backend import PostgresBackend, AsyncPostgresBackend -from .collation import PostgresCollation # noqa: F401 -from .config import PostgresConnectionConfig -from .dialect import PostgresDialect -from .storage_parameters import ( # noqa: F401 - PostgresStorageParameter, - PostgresStorageParameterValueType, -) -from .transaction import PostgresTransactionManager, AsyncPostgresTransactionManager -from .types import PostgresEnumType -from .expression.types import ( - PostgresBigSerialType, - PostgresBitType, - PostgresBoxType, - PostgresByteaType, - PostgresCIDType, - PostgresCircleType, - PostgresDateMultirangeType, - PostgresDateRangeType, - PostgresGeographyType, - PostgresGeometryType, - PostgresHstoreType, - PostgresInetType, - PostgresInt4MultirangeType, - PostgresInt4RangeType, - PostgresInt8MultirangeType, - PostgresInt8RangeType, - PostgresJsonPathType, - PostgresLineSegmentType, - PostgresLineType, - PostgresMacAddr8Type, - PostgresMacAddrType, - PostgresMoneyType, - PostgresNumMultirangeType, - PostgresNumRangeType, - PostgresOIDType, - PostgresPathType, - PostgresPgLSNType, - PostgresPointType, - PostgresPolygonType, - PostgresRegClassType, - PostgresRegTypeType, - PostgresSerialType, - PostgresSmallSerialType, - PostgresTIDType, - PostgresTSQueryType, - PostgresTSVectorType, - PostgresTsMultirangeType, - PostgresTsRangeType, - PostgresTsTzMultirangeType, - PostgresTsTzRangeType, - PostgresUUIDType, - PostgresVarBitType, - PostgresVectorType, - PostgresHalfvecType, - PostgresSparsevecType, - PostgresXID8Type, - PostgresXIDType, - PostgresXMLType, - PostgresCidrType, -) -from .explain import PostgresExplainResult, PostgresExplainPlanLine -from .expression import ( - PostgresCopyFormat, - PostgresCopyFromExpression, - PostgresCopyLogVerbosity, - PostgresCopyOnError, - PostgresCopyToExpression, - PostgresRepackExpression, -) -from .expression.ddl import ( - PostgresAddDomainCheckAction, - PostgresAddEnumValueAction, - PostgresAddTypeAttributeAction, - PostgresAlterDomainExpression, - PostgresAlterEnumTypeAddValueExpression, - PostgresAlterEnumTypeRenameValueExpression, - PostgresAlterTypeAttributeAction, - PostgresBaseTypeDefinition, - PostgresChangeDomainOwnerAction, - PostgresChangeTypeOwnerAction, - PostgresCompositeTypeAttribute, - PostgresCompositeTypeDefinition, - PostgresCreateDomainExpression, - PostgresCreateEnumTypeExpression, - PostgresCreateRangeTypeExpression, - PostgresDropDomainCheckAction, - PostgresDropDomainExpression, - PostgresDropEnumTypeExpression, - PostgresDropTypeAttributeAction, - PostgresDropTypeExpression, - PostgresEnumTypeDefinition, - PostgresRangeTypeDefinition, - PostgresRenameDomainConstraintAction, - PostgresRenameEnumValueAction, - PostgresRenameTypeAction, - PostgresRenameTypeAttributeAction, - PostgresSetDomainSchemaAction, - PostgresSetTypePropertiesAction, - PostgresSetTypeSchemaAction, - PostgresShellTypeDefinition, - PostgresValidateDomainConstraintAction, -) -from .adapters import ( - PostgresEnumAdapter, - PostgresRangeAdapter, - PostgresMultirangeAdapter, -) -from .types.range import ( - PostgresRange, - PostgresMultirange, -) -from .types.geometric import ( - Point, - Line, - LineSegment, - Box, - Path, - Polygon, - Circle, -) -from .adapters.geometric import PostgresGeometryAdapter -from .types.bit_string import PostgresBitString -from .adapters.bit_string import PostgresBitStringAdapter -from .types.monetary import PostgresMoney -from .adapters.monetary import PostgresMoneyAdapter -from .types.xml import PostgresXML -from .adapters.xml import PostgresXMLAdapter -from .types.network_address import PostgresMacaddr, PostgresMacaddr8 -from .adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter -from .adapters.uuid import PostgresUUIDAdapter -from .types.pgvector import PostgresVector -from .adapters.pgvector import PostgresVectorAdapter -from .types.postgis import PostgresGeometry -from .adapters.postgis import PostgresPostGISAdapter -from .types.hstore import PostgresHstore -from .adapters.hstore import PostgresHstoreAdapter -from .types.text_search import ( - PostgresTsVector, - PostgresTsQuery, -) -from .functions.text_search import ( - to_tsvector, - to_tsquery, - plainto_tsquery, - phraseto_tsquery, - websearch_to_tsquery, - ts_matches, - ts_matches_expr, - ts_rank, - ts_rank_cd, - ts_headline, - tsvector_concat, - tsvector_strip, - tsvector_setweight, - tsvector_length, -) -from .adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter -from .types.pg_lsn import PostgresLsn -from .adapters.pg_lsn import PostgresLsnAdapter -from .types.object_identifier import ( - OID, - RegClass, - RegType, - RegProc, - RegProcedure, - RegOper, - RegOperator, - RegConfig, - RegDictionary, - RegNamespace, - RegRole, - RegCollation, - XID, - XID8, - CID, - TID, -) -from .adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter -from .types.json import PostgresJsonPath -from .adapters.json import PostgresJsonPathAdapter -from .types.enum import EnumTypeManager - -# Import functions from functions module -from .functions import ( - # Range functions - range_contains, - range_contained_by, - range_contains_range, - range_overlaps, - range_adjacent, - range_strictly_left_of, - range_strictly_right_of, - range_not_extend_right, - range_not_extend_left, - range_union, - range_intersection, - range_difference, - range_lower, - range_upper, - range_is_empty, - range_lower_inc, - range_upper_inc, - range_lower_inf, - range_upper_inf, - # Geometry functions - geometry_distance, - geometry_contains, - geometry_contained_by, - geometry_overlaps, - geometry_strictly_left, - geometry_strictly_right, - geometry_not_extend_right, - geometry_not_extend_left, - geometry_area, - geometry_center, - geometry_length, - geometry_width, - geometry_height, - geometry_npoints, - # Enum functions - enum_range, - enum_first, - enum_last, - enum_lt, - enum_le, - enum_gt, - enum_ge, - # Bit string functions - bit_concat, - bit_and, - bit_or, - bit_xor, - bit_not, - bit_shift_left, - bit_shift_right, - bit_length, - bit_length_func, - bit_octet_length, - bit_get_bit, - bit_set_bit, - bit_count, - # JSON functions - json_path_root, - json_path_key, - json_path_index, - json_path_wildcard, - json_path_filter, - jsonb_path_query, - jsonb_path_query_first, - jsonb_path_exists, - jsonb_path_match, - # pgvector functions - vector_l2_distance, - vector_cosine_distance, - vector_inner_product, - vector_cosine_similarity, - vector_literal, - # PostGIS functions - Construction - st_make_point, - st_geom_from_text, - st_geog_from_text, - st_set_srid, - st_transform, - # PostGIS functions - Predicates - st_contains, - st_intersects, - st_within, - st_dwithin, - st_crosses, - st_touches, - st_overlaps, - # PostGIS functions - Measurements - st_distance, - st_area, - st_length, - # PostGIS functions - Output - st_as_geojson, - st_as_text, - # PostGIS functions - Operations - st_buffer, - st_envelope, - st_centroid, - # hstore functions - Constructors - hstore_from_record, - hstore_from_key_value, - # hstore functions - Key/Value Extraction - hstore_akeys, - hstore_skeys, - hstore_avals, - hstore_svals, - hstore_each, - # hstore functions - Conversion - hstore_to_array, - hstore_to_matrix, - hstore_to_json, - hstore_to_jsonb, - hstore_to_json_loose, - hstore_to_jsonb_loose, - # hstore functions - Subset - hstore_slice, - # hstore functions - Existence - hstore_exist, - hstore_defined, - # hstore functions - Delete - hstore_delete, - hstore_delete_keys, - hstore_delete_pairs, - # hstore functions - Record - hstore_populate_record, - # hstore functions - Operators - hstore_get_value, - hstore_get_value_as_text, - hstore_get_values, - hstore_concat, - hstore_key_exists, - hstore_all_keys_exist, - hstore_any_key_exists, - hstore_contains, - hstore_contained_by, - hstore_subtract_key, - hstore_subtract_keys, - hstore_subtract_pairs, - hstore_to_array_operator, - hstore_to_matrix_operator, - hstore_record_update, - # hstore functions - Subscript - hstore_subscript_get, - hstore_subscript_set, -) - - -__all__ = [ - # Synchronous Backend - "PostgresBackend", - # Asynchronous Backend - "AsyncPostgresBackend", - # Configuration - "PostgresConnectionConfig", - # Dialect related - "PostgresDialect", - # PostgreSQL Relation Storage Parameters - "PostgresStorageParameter", - "PostgresStorageParameterValueType", - # Transaction - Sync and Async - "PostgresTransactionManager", - "AsyncPostgresTransactionManager", - # PostgreSQL-specific Type Helpers - "PostgresEnumType", - "PostgresEnumAdapter", - # PostgreSQL EXPLAIN Result Types - "PostgresExplainResult", - "PostgresExplainPlanLine", - "PostgresCopyFormat", - "PostgresCopyFromExpression", - "PostgresCopyLogVerbosity", - "PostgresCopyOnError", - "PostgresCopyToExpression", - "PostgresRepackExpression", - # PostgreSQL DDL Statements - "PostgresCompositeTypeAttribute", - "PostgresCompositeTypeDefinition", - "PostgresEnumTypeDefinition", - "PostgresRangeTypeDefinition", - "PostgresBaseTypeDefinition", - "PostgresShellTypeDefinition", - "PostgresRenameTypeAction", - "PostgresSetTypeSchemaAction", - "PostgresChangeTypeOwnerAction", - "PostgresRenameTypeAttributeAction", - "PostgresAddTypeAttributeAction", - "PostgresDropTypeAttributeAction", - "PostgresAlterTypeAttributeAction", - "PostgresAddEnumValueAction", - "PostgresRenameEnumValueAction", - "PostgresSetTypePropertiesAction", - "PostgresDropTypeExpression", - "PostgresCreateEnumTypeExpression", - "PostgresDropEnumTypeExpression", - "PostgresAlterEnumTypeAddValueExpression", - "PostgresAlterEnumTypeRenameValueExpression", - "PostgresCreateRangeTypeExpression", - "PostgresAddDomainCheckAction", - "PostgresDropDomainCheckAction", - "PostgresRenameDomainConstraintAction", - "PostgresValidateDomainConstraintAction", - "PostgresChangeDomainOwnerAction", - "PostgresSetDomainSchemaAction", - "PostgresCreateDomainExpression", - "PostgresAlterDomainExpression", - "PostgresDropDomainExpression", - # Range Types - "PostgresRange", - "PostgresRangeAdapter", - "PostgresMultirange", - "PostgresMultirangeAdapter", - "range_contains", - "range_contained_by", - "range_contains_range", - "range_overlaps", - "range_adjacent", - "range_strictly_left_of", - "range_strictly_right_of", - "range_not_extend_right", - "range_not_extend_left", - "range_union", - "range_intersection", - "range_difference", - "range_lower", - "range_upper", - "range_is_empty", - "range_lower_inc", - "range_upper_inc", - "range_lower_inf", - "range_upper_inf", - # Geometry Types - "Point", - "Line", - "LineSegment", - "Box", - "Path", - "Polygon", - "Circle", - "PostgresGeometryAdapter", - "geometry_distance", - "geometry_contains", - "geometry_contained_by", - "geometry_overlaps", - "geometry_strictly_left", - "geometry_strictly_right", - "geometry_not_extend_right", - "geometry_not_extend_left", - "geometry_area", - "geometry_center", - "geometry_length", - "geometry_width", - "geometry_height", - "geometry_npoints", - # Bit String Types - "PostgresBitString", - "PostgresBitStringAdapter", - "bit_concat", - "bit_and", - "bit_or", - "bit_xor", - "bit_not", - "bit_shift_left", - "bit_shift_right", - "bit_length", - "bit_length_func", - "bit_octet_length", - "bit_get_bit", - "bit_set_bit", - "bit_count", - # Enum Types - "EnumTypeManager", - "enum_range", - "enum_first", - "enum_last", - "enum_lt", - "enum_le", - "enum_gt", - "enum_ge", - # Money Type - "PostgresMoney", - "PostgresMoneyAdapter", - # XML Type - "PostgresXML", - "PostgresXMLAdapter", - # MACADDR Types - "PostgresMacaddr", - "PostgresMacaddrAdapter", - "PostgresMacaddr8", - "PostgresMacaddr8Adapter", - # Text Search Types - "PostgresTsVector", - "PostgresTsVectorAdapter", - "PostgresTsQuery", - "PostgresTsQueryAdapter", - "to_tsvector", - "to_tsquery", - "plainto_tsquery", - "phraseto_tsquery", - "websearch_to_tsquery", - "ts_matches", - "ts_matches_expr", - "ts_rank", - "ts_rank_cd", - "ts_headline", - "tsvector_concat", - "tsvector_strip", - "tsvector_setweight", - "tsvector_length", - # LSN Type - "PostgresLsn", - "PostgresLsnAdapter", - # OID Types - "OID", - "RegClass", - "RegType", - "RegProc", - "RegProcedure", - "RegOper", - "RegOperator", - "RegConfig", - "RegDictionary", - "RegNamespace", - "RegRole", - "RegCollation", - "XID", - "XID8", - "CID", - "TID", - "PostgresOidAdapter", - "PostgresXidAdapter", - "PostgresTidAdapter", - # JSON Path Type - "PostgresJsonPath", - "PostgresJsonPathAdapter", - "json_path_root", - "json_path_key", - "json_path_index", - "json_path_wildcard", - "json_path_filter", - "jsonb_path_query", - "jsonb_path_query_first", - "jsonb_path_exists", - "jsonb_path_match", - # UUID Type - "PostgresUUIDAdapter", - "gen_random_uuid", - "uuid_default_generator", - # pgvector Types - "PostgresVector", - "PostgresVectorAdapter", - "vector_l2_distance", - "vector_cosine_distance", - "vector_inner_product", - "vector_cosine_similarity", - "vector_literal", - # PostGIS Types - "PostgresGeometry", - "PostgresPostGISAdapter", - # hstore Types - "PostgresHstore", - "PostgresHstoreAdapter", - "st_make_point", - "st_geom_from_text", - "st_geog_from_text", - "st_set_srid", - "st_transform", - "st_contains", - "st_intersects", - "st_within", - "st_dwithin", - "st_crosses", - "st_touches", - "st_overlaps", - "st_distance", - "st_area", - "st_length", - "st_as_geojson", - "st_as_text", - "st_buffer", - "st_envelope", - "st_centroid", - # hstore functions - Constructors - "hstore_from_record", - "hstore_from_key_value", - # hstore functions - Key/Value Extraction - "hstore_akeys", - "hstore_skeys", - "hstore_avals", - "hstore_svals", - "hstore_each", - # hstore functions - Conversion - "hstore_to_array", - "hstore_to_matrix", - "hstore_to_json", - "hstore_to_jsonb", - "hstore_to_json_loose", - "hstore_to_jsonb_loose", - # hstore functions - Subset - "hstore_slice", - # hstore functions - Existence - "hstore_exist", - "hstore_defined", - # hstore functions - Delete - "hstore_delete", - "hstore_delete_keys", - "hstore_delete_pairs", - # hstore functions - Record - "hstore_populate_record", - # hstore functions - Operators - "hstore_get_value", - "hstore_get_value_as_text", - "hstore_get_values", - "hstore_concat", - "hstore_key_exists", - "hstore_all_keys_exist", - "hstore_any_key_exists", - "hstore_contains", - "hstore_contained_by", - "hstore_subtract_key", - "hstore_subtract_keys", - "hstore_subtract_pairs", - "hstore_to_array_operator", - "hstore_to_matrix_operator", - "hstore_record_update", - # hstore functions - Subscript - "hstore_subscript_get", - "hstore_subscript_set", - # 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", - "PostgresHalfvecType", - "PostgresSparsevecType", - "PostgresXID8Type", - "PostgresXIDType", - "PostgresXMLType", - "PostgresCidrType", -] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py index 6d5c7ab3..893567ef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py @@ -280,7 +280,7 @@ def _do_to_database(self, value: Any, target_type: Type, options: Optional[Dict[ # Validate if enum_type provided if options and "enum_type" in options: - from ..types import PostgresEnumType + from ..expression.enum_ import PostgresEnumType enum_type = options["enum_type"] if isinstance(enum_type, PostgresEnumType): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py index 89e2037b..49a38bc6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py @@ -15,7 +15,7 @@ from typing import Any, Dict, Optional, Set, Type -from ..types.bit_string import PostgresBitString +from ..type_values.bit_string import PostgresBitString class PostgresBitStringAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py index 498ab095..8c7f4abf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py @@ -18,7 +18,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.geometric import ( +from ..type_values.geometric import ( Point, Line, LineSegment, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py index 670b8d8f..a2048f30 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Optional, Type, Union, List from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore class PostgresHstoreAdapter(BaseSQLTypeAdapter): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py index b249c173..16301cbf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py @@ -22,7 +22,7 @@ from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter -from ..types.json import PostgresJsonPath +from ..type_values.json import PostgresJsonPath class PostgresJSONBAdapter(BaseSQLTypeAdapter): @@ -81,7 +81,7 @@ class PostgresJsonPathAdapter: 1. Register it explicitly on your backend instance: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresJsonPathAdapter + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJsonPathAdapter adapter = PostgresJsonPathAdapter() backend.adapter_registry.register(adapter, PostgresJsonPath, str) ``` @@ -89,7 +89,8 @@ class PostgresJsonPathAdapter: 2. Specify it directly when executing queries: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresJsonPath, PostgresJsonPathAdapter + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresJsonPath + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJsonPathAdapter path_adapter = PostgresJsonPathAdapter() result = backend.execute( "SELECT * FROM data WHERE content @? %s", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py index 5faec0da..3044b67f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Optional, Set, Type -from ..types.monetary import PostgresMoney +from ..type_values.monetary import PostgresMoney class PostgresMoneyAdapter: @@ -35,7 +35,7 @@ class PostgresMoneyAdapter: To use with Decimal values: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresMoney + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresMoney # Wrap Decimal in PostgresMoney money = PostgresMoney(Decimal('100.50')) ``` diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py index b8506e1c..ef8db097 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py @@ -15,7 +15,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.network_address import PostgresMacaddr, PostgresMacaddr8 +from ..type_values.network_address import PostgresMacaddr, PostgresMacaddr8 class PostgresNetworkAddressAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py index 891c71e0..37954ac1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py @@ -29,7 +29,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union -from ..types.object_identifier import ( +from ..type_values.object_identifier import ( OID, RegClass, RegType, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py index 3e2ea464..a317eb3c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.pg_lsn import PostgresLsn +from ..type_values.pg_lsn import PostgresLsn class PostgresLsnAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py index 8f06890b..2887793a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py @@ -14,7 +14,7 @@ from typing import Any, Dict, Optional, Set, Type -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector class PostgresVectorAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py index 9c888109..59aeed28 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py @@ -14,7 +14,7 @@ from typing import Any, Dict, Optional, Set, Type -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry class PostgresPostGISAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py index 6e6894ca..8a8c583c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union -from ..types.range import PostgresRange, PostgresMultirange +from ..type_values.range import PostgresRange, PostgresMultirange class PostgresRangeAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py index 3a6b36f0..776b2126 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union -from ..types.text_search import PostgresTsVector, PostgresTsQuery +from ..type_values.text_search import PostgresTsVector, PostgresTsQuery class PostgresTsVectorAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py index 86d15b04..8b05fee6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.xml import PostgresXML +from ..type_values.xml import PostgresXML class PostgresXMLAdapter: @@ -35,14 +35,15 @@ class PostgresXMLAdapter: 1. Register it explicitly on your backend instance: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresXMLAdapter + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresXMLAdapter adapter = PostgresXMLAdapter() backend.adapter_registry.register(adapter, PostgresXML, str) ``` 2. Specify it directly when executing queries: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresXML, PostgresXMLAdapter + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresXML + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresXMLAdapter xml_adapter = PostgresXMLAdapter() result = backend.execute( "INSERT INTO docs (content) VALUES (%s)", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py index 723e1782..09f72d7d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py @@ -1,22 +1,16 @@ # src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py """PostgreSQL backend implementations. -This module provides PostgreSQL backend implementations for both -synchronous and asynchronous database operations. - -Exports: - - PostgresBackendMixin: Shared functionality mixin for sync/async backends - - PostgresBackend: Synchronous PostgreSQL backend - - AsyncPostgresBackend: Asynchronous PostgreSQL backend +Every backend keeps both classes in this package: the sync class in +``backend.py`` and the async class in ``async_backend.py``. So the sync class +is at ``impl.postgres.backend.backend`` and the async class at +``impl.postgres.backend.async_backend``, and both are re-exported here. """ -from .base import PostgresBackendMixin -from .sync import PostgresBackend +from .backend import PostgresBackend from .async_backend import AsyncPostgresBackend - __all__ = [ - "PostgresBackendMixin", "PostgresBackend", "AsyncPostgresBackend", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py index 1541c237..1244990f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -109,11 +109,12 @@ def __init__(self, **kwargs: Any) -> None: "pool_name", "pool_reset_session", "pool_pre_ping", - "ssl_ca", - "ssl_cert", - "ssl_key", - "ssl_verify_cert", - "ssl_verify_identity", + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", "log_queries", "log_level", "application_name", @@ -175,7 +176,7 @@ def __init__(self, **kwargs: Any) -> None: # Initialize transaction manager (will use backend.execute()) self._transaction_manager = AsyncPostgresTransactionManager(self, self.logger) - self.log(logging.INFO, "AsyncPostgreSQLBackend initialized") + self.log(logging.INFO, "AsyncPostgresBackend initialized") def _create_introspector(self) -> Any: """Create and return an AsyncPostgreSQLIntrospector with an async executor.""" @@ -253,19 +254,10 @@ async def connect(self) -> None: else: conn_params["options"] = options_value - # Add SSL parameters if provided - ssl_params = {} - if hasattr(self.config, "ssl_ca"): - ssl_params["sslcert"] = self.config.ssl_ca - if hasattr(self.config, "ssl_cert"): - ssl_params["sslcert"] = self.config.ssl_cert - if hasattr(self.config, "ssl_key"): - ssl_params["sslkey"] = self.config.ssl_key - if hasattr(self.config, "ssl_mode"): - ssl_params["sslmode"] = self.config.ssl_mode - - if ssl_params: - conn_params.update(ssl_params) + # Add SSL/TLS parameters. The config exposes libpq keyword names + # (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) so they + # can be forwarded to psycopg verbatim. + conn_params.update(self.config.get_ssl_connection_params()) self._connection = await AsyncConnection.connect(**conn_params) await self._connection.set_autocommit(True) # Disable psycopg auto-transaction management diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py new file mode 100644 index 00000000..b9929d77 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py @@ -0,0 +1,892 @@ +# src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py +"""PostgreSQL synchronous backend implementation. + +This module provides a synchronous PostgreSQL implementation: +- PostgreSQL synchronous backend with connection management and query execution +- PostgreSQL-specific connection configuration +- Type mapping and value conversion +- Transaction management with savepoint support +- PostgreSQL dialect and expression handling +- PostgreSQL-specific type definitions and mappings + +Architecture: +- PostgresBackend: Synchronous implementation using psycopg +- Independent from ORM frameworks - uses only native drivers +""" + +import datetime +import logging +from typing import Any, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING, Union + +import psycopg +from psycopg.errors import Error as PsycopgError +from psycopg.errors import ( + IntegrityError as PsycopgIntegrityError, + OperationalError as PsycopgOperationalError, + ProgrammingError as PsycopgProgrammingError, + DeadlockDetected as PsycopgDeadlockError, +) + +from rhosocial.activerecord.backend.base import StorageBackend +from rhosocial.activerecord.backend.introspection.backend_mixin import IntrospectorBackendMixin +from rhosocial.activerecord.backend.explain import SyncExplainBackendMixin +from rhosocial.activerecord.backend.errors import ( + ConnectionError, + DatabaseError, + DeadlockError, + IntegrityError, + OperationalError, + QueryError, +) +from rhosocial.activerecord.backend.options import ExecutionOptions +from rhosocial.activerecord.backend.result import QueryResult +from ..config import PostgresConnectionConfig +from ..dialect import PostgresDialect +from .base import PostgresBackendMixin, PostgresConcurrencyMixin +from ..protocols import PostgresExtensionInfo +from ..transaction import PostgresTransactionManager +from ..introspection import SyncPostgreSQLIntrospector + +if TYPE_CHECKING: + from ..expression import PostgresCopyFromExpression, PostgresCopyToExpression + + +CopyChunk = Union[str, bytes, bytearray, memoryview] +CopyInput = Union[CopyChunk, Iterable[CopyChunk]] + + +class PostgresBackend( + IntrospectorBackendMixin, + PostgresBackendMixin, + SyncExplainBackendMixin, + PostgresConcurrencyMixin, + StorageBackend, +): + """PostgreSQL-specific backend implementation.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize PostgreSQL backend with connection configuration. + + Note: + The dialect is initialized with a default version (9.6.0) and no plugins + are enabled. Call introspect_and_adapt() after connecting to detect + the actual server version and installed extensions. + """ + graph_feature_overrides = kwargs.pop("graph_feature_overrides", None) + + # Ensure we have proper PostgreSQL configuration + connection_config = kwargs.get("connection_config") + + if connection_config is None: + # Extract PostgreSQL-specific parameters from kwargs + config_params = {} + pg_specific_params = [ + "host", + "port", + "database", + "username", + "password", + "charset", + "collation", + "timezone", + "version", + "pool_size", + "pool_timeout", + "pool_name", + "pool_reset_session", + "pool_pre_ping", + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", + "log_queries", + "log_level", + "application_name", + "fallback_application_name", + "connect_timeout", + "options", + "service", + "target_session_attrs", + "gssencmode", + "channel_binding", + "replication", + "assume_role", + "role", + "search_path", + "row_security", + "datestyle", + "intervalstyle", + "timezone_setting", + "extra_float_digits", + "client_encoding", + "tcp_user_timeout", + "tcp_keepalives_idle", + "tcp_keepalives_interval", + "tcp_keepalives_count", + "load_balance_hosts", + "replication", + "keepalives", + "keepalives_idle", + "keepalives_interval", + "keepalives_count", + ] + + for param in pg_specific_params: + if param in kwargs: + config_params[param] = kwargs[param] + + # Set defaults if not provided + if "host" not in config_params: + config_params["host"] = "localhost" + if "port" not in config_params: + config_params["port"] = 5432 + + kwargs["connection_config"] = PostgresConnectionConfig(**config_params) + + # Initialize the base class first to ensure all base properties are set + super().__init__(**kwargs) + + # Initialize PostgreSQL-specific components + # Version is None until introspect_and_adapt() is called + self._dialect = PostgresDialect( + graph_feature_overrides=graph_feature_overrides, + ) + + # Register PostgreSQL-specific type adapters (same as async backend) + # Note: XML adapter is NOT registered by default due to potential conflicts. + # See PostgresXMLAdapter documentation for details. + self._register_postgres_adapters() + + # Initialize transaction manager (will use backend.execute()) + self._transaction_manager = PostgresTransactionManager(self, self.logger) + + self.log(logging.INFO, "PostgresBackend initialized") + + def _create_introspector(self) -> Any: + """Create and return a SyncPostgreSQLIntrospector with a sync executor.""" + from rhosocial.activerecord.backend.introspection.executor import SyncIntrospectorExecutor + + return SyncPostgreSQLIntrospector(self, SyncIntrospectorExecutor(self)) + + @property + def dialect(self) -> PostgresDialect: + """Get the PostgreSQL dialect instance.""" + return self._dialect + + @property + def transaction_manager(self) -> PostgresTransactionManager: + """Get the PostgreSQL transaction manager.""" + return self._transaction_manager + + + def connect(self) -> None: + """Establish connection to PostgreSQL database.""" + try: + # Prepare connection parameters from config + conn_params = { + "host": self.config.host, + "port": self.config.port, + "dbname": self.config.database, + "user": self.config.username, + "password": self.config.password, + } + + # Add additional parameters if they exist in config + additional_params = [ + "application_name", + "fallback_application_name", + "connect_timeout", + "options", + "service", + "target_session_attrs", + "gssencmode", + "channel_binding", + "replication", + "assume_role", + "role", + "search_path", + "row_security", + "datestyle", + "intervalstyle", + "timezone", + "extra_float_digits", + "client_encoding", + "tcp_user_timeout", + "tcp_keepalives_idle", + "tcp_keepalives_interval", + "tcp_keepalives_count", + "load_balance_hosts", + "keepalives", + "keepalives_idle", + "keepalives_interval", + "keepalives_count", + ] + + for param in additional_params: + if hasattr(self.config, param): + value = getattr(self.config, param) + if value is not None: # Only add the parameter if it's not None + conn_params[param] = value + + # Handle 'options' parameter specially as it should be a string, not a dict + if hasattr(self.config, "options") and self.config.options is not None: + # Convert options dict to string format if needed, or use as-is if it's already a string + options_value = self.config.options + if isinstance(options_value, dict): + # Convert dict to connection string format + options_str = " ".join([f"-c {k}={v}" for k, v in options_value.items()]) + conn_params["options"] = options_str + else: + conn_params["options"] = options_value + + # Add SSL/TLS parameters. The config exposes libpq keyword names + # (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) so they + # can be forwarded to psycopg verbatim. + conn_params.update(self.config.get_ssl_connection_params()) + + self._connection = psycopg.connect(**conn_params) + self._connection.autocommit = True # Disable psycopg auto-transaction management + + self.log( + logging.INFO, + f"Connected to PostgreSQL database: {self.config.host}:{self.config.port}/{self.config.database}", + ) + except PsycopgError as e: + self.log(logging.ERROR, f"Failed to connect to PostgreSQL database: {str(e)}") + raise ConnectionError(f"Failed to connect to PostgreSQL: {str(e)}") from e + + def disconnect(self) -> None: + """Close connection to PostgreSQL database.""" + if self._connection: + try: + # Rollback any active transaction + if self.in_transaction: + self.transaction_manager.rollback() + + self._connection.close() + self._connection = None + self.log(logging.INFO, "Disconnected from PostgreSQL database") + except PsycopgError as e: + self.log(logging.ERROR, f"Error during disconnection: {str(e)}") + raise OperationalError(f"Error during PostgreSQL disconnection: {str(e)}") from e + + def _get_cursor(self) -> Any: + """Get a database cursor, ensuring connection is active. + + This method implements automatic connection health checking (Plan A): + - Checks if connection object exists + - Checks if connection is still valid using closed/broken attributes + - Automatically reconnects if connection was lost + """ + if not self._connection: + self.log(logging.DEBUG, "No connection, connecting...") + self.connect() + elif self._connection.closed or self._connection.broken: + self.log(logging.DEBUG, "Connection lost, reconnecting...") + self._connection = None # Clear the closed/broken connection + self.connect() + + return self._connection.cursor() + + @staticmethod + def _copy_output_chunk(chunk: Any) -> bytes: + if isinstance(chunk, bytes): + return chunk + if isinstance(chunk, str): + return chunk.encode("utf-8") + if isinstance(chunk, (bytearray, memoryview)): + return bytes(chunk) + raise TypeError("PostgreSQL COPY stream yielded a non-buffer chunk") + + @staticmethod + def _validate_copy_input_chunk( + chunk: Any, + binary: bool, + encoding: Optional[str] = None, + ) -> CopyChunk: + if not isinstance(chunk, (str, bytes, bytearray, memoryview)): + raise TypeError("COPY input chunks must be str or bytes-like objects") + if binary and isinstance(chunk, str): + raise TypeError("COPY FROM BINARY requires bytes-like input chunks") + if encoding is not None and isinstance(chunk, str): + raise TypeError("COPY FROM with ENCODING requires bytes-like input chunks") + return chunk + + def copy_to(self, expression: "PostgresCopyToExpression") -> bytes: + """Execute COPY TO STDOUT and return the complete byte stream.""" + from ..expression import PostgresCopyToExpression + + if not isinstance(expression, PostgresCopyToExpression): + raise TypeError("expression must be a PostgresCopyToExpression") + sql, params = expression.to_sql() + chunks = [] + try: + with self._get_cursor() as cursor: + with cursor.copy(sql, params) as copy: + for chunk in copy: + chunks.append(self._copy_output_chunk(chunk)) + except BaseException as error: + if isinstance(error, PsycopgError): + self._handle_error(error) + else: + self._try_rollback_transaction() + raise + return b"".join(chunks) + + def copy_from( + self, + expression: "PostgresCopyFromExpression", + data: CopyInput, + ) -> int: + """Stream COPY FROM STDIN data and return the server-reported row count.""" + from ..expression import PostgresCopyFromExpression + + if not isinstance(expression, PostgresCopyFromExpression): + raise TypeError("expression must be a PostgresCopyFromExpression") + sql, params = expression.to_sql() + binary = str(expression.format).lower() == "binary" + chunks: Iterable[CopyChunk] + if isinstance(data, (str, bytes, bytearray, memoryview)): + chunks = (data,) + else: + try: + chunks = iter(data) + except TypeError as error: + raise TypeError("COPY input must be bytes, str, or an iterable of chunks") from error + try: + with self._get_cursor() as cursor: + with cursor.copy(sql, params) as copy: + for chunk in chunks: + copy.write( + self._validate_copy_input_chunk(chunk, binary, expression.encoding) + ) + rowcount = int(cursor.rowcount) + if getattr(self, "_transaction_manager", None) is not None: + self._handle_auto_commit_if_needed() + return rowcount + except BaseException as error: + if isinstance(error, PsycopgError): + self._handle_error(error) + else: + self._try_rollback_transaction() + raise + + def execute_many(self, sql: str, params_list: List[Tuple]) -> QueryResult: + """Execute the same SQL statement multiple times with different parameters.""" + if not self._connection: + self.connect() + + cursor = None + start_time = datetime.datetime.now() + + try: + cursor = self._get_cursor() + + # Log the batch operation if logging is enabled + if getattr(self.config, "log_queries", False): + self.log(logging.DEBUG, f"Executing batch operation: {sql}") + self.log(logging.DEBUG, f"With {len(params_list)} parameter sets") + + # Execute multiple statements + affected_rows = 0 + for params in params_list: + cursor.execute(sql, params) + affected_rows += cursor.rowcount + + duration = (datetime.datetime.now() - start_time).total_seconds() + + result = QueryResult(affected_rows=affected_rows, data=None, duration=duration) + + self.log( + logging.INFO, + f"Batch operation completed, affected {affected_rows} rows, duration={duration:.3f}s", + ) + return result + + except PsycopgIntegrityError as e: + self.log(logging.ERROR, f"Integrity error in batch: {str(e)}") + raise IntegrityError(str(e)) from e + except PsycopgError as e: + self.log(logging.ERROR, f"PostgreSQL error in batch: {str(e)}") + raise DatabaseError(str(e)) from e + except Exception as e: + self.log(logging.ERROR, f"Unexpected error during batch execution: {str(e)}") + raise QueryError(str(e)) from e + finally: + if cursor: + cursor.close() + + def get_server_version(self) -> Optional[Tuple[int, int, int]]: + """Get PostgreSQL server version. + + Returns: + Tuple of (major, minor, patch) version numbers, or None if version + cannot be determined. + """ + if not self._connection: + self.connect() + + cursor = None + try: + cursor = self._get_cursor() + cursor.execute("SELECT version()") + result = cursor.fetchone() + if not result or not result[0]: + self.log(logging.WARNING, "PostgreSQL version query returned no result") + return None + + version_str = result[0] + + # Extract version from string like "PostgreSQL 13.2..." + import re + + match = re.search(r"PostgreSQL (\d+)(?:\.(\d+))?(?:[^\d]*(\d+))?", version_str) + if match: + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + patch = int(match.group(3)) if match.group(3) else 0 + return (major, minor, patch) + else: + self.log(logging.WARNING, f"Could not parse PostgreSQL version: {version_str}") + return None + except Exception as e: + self.log(logging.WARNING, f"Could not determine PostgreSQL version: {str(e)}") + return None + finally: + if cursor: + cursor.close() + + def introspect_and_adapt(self) -> None: + """Introspect backend and adapt backend instance to actual server capabilities. + + This method ensures a connection exists, queries the actual PostgreSQL server version + and installed extensions, and updates the backend's internal state. + + Introspection includes: + 1. Server version: affects version-dependent feature support checks + 2. Installed extensions: queries pg_extension system table to determine plugin availability + + After detection, the dialect instance caches extension information, + which can be queried via is_extension_installed(). + + Note: If version cannot be determined, the default version (9.6.0) is retained. + """ + # Ensure connection exists + if not self._connection: + self.connect() + + # Get server version + actual_version = self.get_server_version() + + # If version cannot be determined, keep the default version + if actual_version is None: + self.log(logging.WARNING, "Could not determine server version, retaining default (9.6.0)") + actual_version = (9, 6, 0) + + cached_version = getattr(self, "_server_version_cache", None) + version_changed = cached_version != actual_version + + if version_changed: + self._server_version_cache = actual_version + self._dialect = PostgresDialect( + actual_version, + graph_feature_overrides=getattr( + self._dialect, "_graph_feature_overrides", None + ), + ) + + # Detect installed extensions + extensions = self._detect_extensions() + self._dialect._extensions = extensions + + # Log detected extensions + installed_exts = [f"{k}={v.version}" for k, v in extensions.items() if v.installed] + ext_info = ", ".join(installed_exts) if installed_exts else "none" + + if version_changed: + self.log(logging.INFO, f"Adapted to PostgreSQL {actual_version}, extensions: {ext_info}") + else: + self.log(logging.DEBUG, f"Extensions detected: {ext_info}") + + def _detect_extensions(self) -> Dict[str, PostgresExtensionInfo]: + """Detect installed and available extensions.""" + with self._connection.cursor() as cursor: + cursor.execute(PostgresDialect.SQL_INSTALLED_EXTENSIONS) + installed_rows = cursor.fetchall() + + cursor.execute(PostgresDialect.SQL_AVAILABLE_EXTENSIONS) + available_rows = cursor.fetchall() + + return PostgresDialect.build_extension_map(installed_rows, available_rows) + + def ping(self, reconnect: bool = True) -> bool: + """ + Ping the PostgreSQL server to check if the connection is alive. + + Args: + reconnect: If True, attempt to reconnect if the connection is dead. + If False, just return the current connection status. + + Returns: + True if the connection is alive (or was successfully reconnected), + False if the connection is dead and reconnect is False or reconnection failed. + """ + try: + if not self._connection: + if reconnect: + self.connect() + return True + else: + return False + + # Check connection status without triggering auto-reconnect + if self._connection.closed or self._connection.broken: + if reconnect: + self._connection = None + self.connect() + return True + else: + return False + + # Execute a simple query to verify connection is actually working + cursor = self._get_cursor() + cursor.execute("SELECT 1") + cursor.fetchone() + cursor.close() + + return True + except psycopg.Error as e: + self.log(logging.WARNING, f"PostgreSQL connection ping failed: {str(e)}") + if reconnect: + try: + self._connection = None + self.connect() + return True + except Exception as connect_error: + self.log(logging.ERROR, f"Failed to reconnect after ping failure: {str(connect_error)}") + return False + return False + + def _reconnect(self) -> bool: + """ + Attempt to reconnect to the PostgreSQL server. + + Returns: + True if reconnection was successful, False otherwise + """ + try: + self._connection = None + self.connect() + return True + except Exception as e: + self.log(logging.ERROR, f"Failed to reconnect: {str(e)}") + return False + + def execute( + self, + sql: str, + params: Optional[Tuple] = None, + *, + options: Optional[ExecutionOptions] = None, + max_retries: int = 2, + **kwargs: Any, + ) -> QueryResult: + """ + Execute a SQL statement with optional parameters. + + This method wraps the base execute method with automatic reconnection + logic for connection errors (Plan B: Error Retry Mechanism). + + Args: + sql: SQL statement to execute + params: Optional parameters for the SQL statement + options: Execution options + max_retries: Maximum number of retry attempts for connection errors + **kwargs: Additional keyword arguments + + Returns: + QueryResult containing the query results + """ + from rhosocial.activerecord.backend.options import StatementType + + # If no options provided, create default options from kwargs + if options is None: + # Determine statement type based on SQL + sql_upper = sql.strip().upper() + if sql_upper.startswith(("SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN")): + stmt_type = StatementType.DQL + elif sql_upper.startswith(("INSERT", "UPDATE", "DELETE", "UPSERT")): + stmt_type = StatementType.DML + else: + stmt_type = StatementType.DDL + + # Extract column_mapping and column_adapters from kwargs if present + column_mapping = kwargs.get("column_mapping") + column_adapters = kwargs.get("column_adapters") + + options = ExecutionOptions( + stmt_type=stmt_type, + process_result_set=None, + column_adapters=column_adapters, + column_mapping=column_mapping, + ) + else: + # If options is provided but column_mapping or column_adapters are explicitly passed in kwargs, + # update the options with these values + if "column_mapping" in kwargs: + options.column_mapping = kwargs["column_mapping"] + if "column_adapters" in kwargs: + options.column_adapters = kwargs["column_adapters"] + + # Retry loop for connection errors (Plan B) + # We catch the framework's OperationalError because the original PsycopgOperationalError + # has already been converted by _handle_error() in the base class. + for attempt in range(max_retries + 1): + try: + return super().execute(sql, params, options=options) + except OperationalError as e: + error_msg = str(e) + if self._is_connection_error_message(error_msg) and attempt < max_retries: + self.log(logging.WARNING, + f"Connection error on attempt {attempt + 1}, retrying... Error: {error_msg}") + if self._reconnect(): + continue + # If reconnection fails, break and raise the error + self.log(logging.ERROR, "Failed to reconnect after connection error") + break + raise + + def _handle_error(self, error: Exception) -> None: + """Handle PostgreSQL-specific errors.""" + error_msg = str(error) + + # Try to rollback transaction to recover from aborted state + # PostgreSQL requires explicit ROLLBACK after errors in a transaction + self._try_rollback_transaction() + + if isinstance(error, PsycopgIntegrityError): + if "duplicate key value violates unique constraint" in error_msg.lower(): + self.log(logging.ERROR, f"Unique constraint violation: {error_msg}") + raise IntegrityError(f"Unique constraint violation: {error_msg}") from error + elif "violates foreign key constraint" in error_msg.lower(): + self.log(logging.ERROR, f"Foreign key constraint violation: {error_msg}") + raise IntegrityError(f"Foreign key constraint violation: {error_msg}") from error + self.log(logging.ERROR, f"Integrity error: {error_msg}") + raise IntegrityError(error_msg) from error + elif isinstance(error, PsycopgOperationalError): + if "deadlock detected" in error_msg.lower(): + self.log(logging.ERROR, f"Deadlock error: {error_msg}") + raise DeadlockError(error_msg) from error + self.log(logging.ERROR, f"Operational error: {error_msg}") + raise OperationalError(error_msg) from error + elif isinstance(error, PsycopgProgrammingError): + self.log(logging.ERROR, f"Programming error: {error_msg}") + raise QueryError(error_msg) from error + elif isinstance(error, PsycopgDeadlockError): + self.log(logging.ERROR, f"Deadlock error: {error_msg}") + raise DeadlockError(error_msg) from error + elif isinstance(error, PsycopgError): + self.log(logging.ERROR, f"PostgreSQL error: {error_msg}") + raise DatabaseError(error_msg) from error + else: + self.log(logging.ERROR, f"Unexpected error: {error_msg}") + raise error + + def _try_rollback_transaction(self) -> None: + """Attempt to rollback transaction to recover from aborted state. + + PostgreSQL requires explicit ROLLBACK after query errors in a transaction. + This method attempts to recover by rolling back the transaction, regardless + of the transaction manager's state. + """ + try: + # First, try the transaction manager if active + if self.in_transaction: + self.log(logging.INFO, "Attempting to rollback transaction after error") + self._transaction_manager.rollback() + + # Also try direct connection rollback to handle edge cases + # This ensures we recover from aborted transaction state + if self._connection and not self._connection.closed: + try: + self._connection.rollback() + self.log(logging.INFO, "Direct connection rollback completed") + except Exception as direct_error: + self.log(logging.DEBUG, f"Direct rollback not needed or failed: {direct_error}") + except Exception as rollback_error: + self.log(logging.WARNING, f"Failed to rollback transaction: {rollback_error}") + + def _handle_auto_commit(self) -> None: + """Handle auto commit based on PostgreSQL connection and transaction state. + + This method will commit the current connection if: + 1. The connection exists and is open + 2. There is no active transaction managed by transaction_manager + + It's used by insert/update/delete operations to ensure changes are + persisted immediately when auto_commit=True is specified. + """ + try: + # Check if connection exists + if not self._connection: + return + + # Check if we're not in an active transaction + if not self.in_transaction: + # For PostgreSQL, if autocommit is disabled, we need to commit explicitly + if not self.config.autocommit: + self._connection.commit() + self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") + except Exception as e: + # Just log the error but don't raise - this is a convenience feature + self.log(logging.WARNING, f"Failed to auto-commit: {str(e)}") + + def _handle_auto_commit_if_needed(self) -> None: + """ + Handle auto-commit for PostgreSQL. + + PostgreSQL respects the autocommit setting, but we also need to handle explicit commits. + """ + if not self.in_transaction and self._connection: + if not self.config.autocommit: + self._connection.commit() + self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") + + # region Advisory Lock Methods + + def execute_advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ) -> None: + """ + Acquire an advisory lock. + + This method blocks until the lock is acquired. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Raises: + DatabaseError: If the lock operation fails + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryLockExpression + + expr = PostgresAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) + sql, params = expr.to_sql() + self.execute(sql, params) + + def execute_advisory_unlock(self, key, shared: bool = False) -> bool: + """ + Release an advisory lock. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, release shared lock; otherwise exclusive lock + + Returns: + True if the lock was released, False if it was not held + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockExpression + + expr = PostgresAdvisoryUnlockExpression(self.dialect, key=key, shared=shared) + sql, params = expr.to_sql() + result = self.execute(sql, params) + # pg_advisory_unlock returns boolean in first column + if result.data and len(result.data) > 0: + row = result.data[0] + # Result is a dict with column name as key + return bool(list(row.values())[0]) + return False + + def execute_advisory_unlock_all(self) -> None: + """ + Release all advisory locks held by the current session. + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockAllExpression + + expr = PostgresAdvisoryUnlockAllExpression(self.dialect) + sql, params = expr.to_sql() + self.execute(sql, params) + + def try_advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ) -> bool: + """ + Non-blocking advisory lock acquisition. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Returns: + True if the lock was acquired, False if it was not available + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresTryAdvisoryLockExpression + + expr = PostgresTryAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) + sql, params = expr.to_sql() + result = self.execute(sql, params) + # pg_try_advisory_lock returns boolean in first column + if result.data and len(result.data) > 0: + row = result.data[0] + # Result is a dict with column name as key + return bool(list(row.values())[0]) + return False + + def advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ): + """ + Context manager for advisory locks. + + Automatically acquires the lock on entry and releases it on exit. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Returns: + Context manager that holds the lock + + Example: + with backend.advisory_lock(key=12345): + # Critical section - lock is held + process_data() + # Lock automatically released + + # Shared lock for read-only access + with backend.advisory_lock(key=12345, shared=True): + read_data() + """ + from contextlib import contextmanager + + @contextmanager + def _advisory_lock_context(): + self.execute_advisory_lock(key=key, shared=shared, session=session) + try: + yield + finally: + # Only release session-level locks; transaction-level are auto-released + if session: + self.execute_advisory_unlock(key=key) + + return _advisory_lock_context() + + # endregion + + +__all__ = ["PostgresBackend"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py index d14bbe9d..7abb647e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py @@ -285,14 +285,14 @@ def get_default_adapter_suggestions(self) -> Dict[Type, Tuple["SQLTypeAdapter", from enum import Enum # Import PostgreSQL-specific types - from ..types.range import PostgresRange, PostgresMultirange - from ..types.geometric import Point, Line, LineSegment, Box, Path, Polygon, Circle - from ..types.text_search import PostgresTsVector, PostgresTsQuery - from ..types.monetary import PostgresMoney - from ..types.network_address import PostgresMacaddr, PostgresMacaddr8 - from ..types.pg_lsn import PostgresLsn - from ..types.json import PostgresJsonPath - from ..types.object_identifier import ( + from ..type_values.range import PostgresRange, PostgresMultirange + from ..type_values.geometric import Point, Line, LineSegment, Box, Path, Polygon, Circle + from ..type_values.text_search import PostgresTsVector, PostgresTsQuery + from ..type_values.monetary import PostgresMoney + from ..type_values.network_address import PostgresMacaddr, PostgresMacaddr8 + from ..type_values.pg_lsn import PostgresLsn + from ..type_values.json import PostgresJsonPath + from ..type_values.object_identifier import ( OID, RegClass, RegType, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index bcba3f5f..09ce8a3a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -95,11 +95,12 @@ def __init__(self, **kwargs: Any) -> None: "pool_name", "pool_reset_session", "pool_pre_ping", - "ssl_ca", - "ssl_cert", - "ssl_key", - "ssl_verify_cert", - "ssl_verify_identity", + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", "log_queries", "log_level", "application_name", @@ -161,7 +162,7 @@ def __init__(self, **kwargs: Any) -> None: # Initialize transaction manager (will use backend.execute()) self._transaction_manager = PostgresTransactionManager(self, self.logger) - self.log(logging.INFO, "PostgreSQLBackend initialized") + self.log(logging.INFO, "PostgresBackend initialized") def _create_introspector(self) -> Any: """Create and return a SyncPostgreSQLIntrospector with a sync executor.""" @@ -240,19 +241,10 @@ def connect(self) -> None: else: conn_params["options"] = options_value - # Add SSL parameters if provided - ssl_params = {} - if hasattr(self.config, "ssl_ca"): - ssl_params["sslcert"] = self.config.ssl_ca - if hasattr(self.config, "ssl_cert"): - ssl_params["sslcert"] = self.config.ssl_cert - if hasattr(self.config, "ssl_key"): - ssl_params["sslkey"] = self.config.ssl_key - if hasattr(self.config, "ssl_mode"): - ssl_params["sslmode"] = self.config.ssl_mode - - if ssl_params: - conn_params.update(ssl_params) + # Add SSL/TLS parameters. The config exposes libpq keyword names + # (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) so they + # can be forwarded to psycopg verbatim. + conn_params.update(self.config.get_ssl_connection_params()) self._connection = psycopg.connect(**conn_params) self._connection.autocommit = True # Disable psycopg auto-transaction management diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py index e3d3518d..c30a1e69 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py @@ -148,7 +148,7 @@ def resolve_connection_config_from_args(args): def create_backend(args): """Create, connect, and introspect a PostgreSQL backend from parsed args.""" - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = resolve_connection_config_from_args(args) backend = PostgresBackend(connection_config=config) backend.connect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py index b1a2122d..7a8e2c81 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py @@ -206,7 +206,7 @@ def handle(args): named_conn = getattr(args, "named_connection", None) if named_conn or args.database: try: - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = resolve_connection_config_from_args(args) backend = PostgresBackend(connection_config=config) backend.connect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py index d5d42a91..a20b5632 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py @@ -12,7 +12,8 @@ from enum import Enum from typing import Any -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py index 93f3d166..40828cc4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py @@ -4,7 +4,8 @@ named-expression requires connection arguments and output arguments. """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from .connection import create_connection_parent_parser, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py index 3d89eb89..ae1d1e54 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py @@ -3,7 +3,8 @@ from __future__ import annotations -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py index 2df72470..e8802c79 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py @@ -4,7 +4,8 @@ named-procedure requires connection arguments, output arguments, and --rich-ascii. """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py index c3686f7e..b069c271 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py @@ -15,7 +15,7 @@ myapp.npg.monthly_report """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider @@ -64,7 +64,9 @@ def disconnect(): def backend_async_factory(): nonlocal async_backend - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) config = resolve_connection_config_from_args(args) async_backend = AsyncPostgresBackend(connection_config=config) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py index 42e4aab3..a6efb32a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py @@ -9,7 +9,8 @@ import logging import sys -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py index 40bf8de0..a7a938a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py @@ -13,7 +13,8 @@ from enum import Enum from typing import Any -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/config.py b/src/rhosocial/activerecord/backend/impl/postgres/config.py index 8d7340b7..b1b18c7d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/config.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/config.py @@ -24,9 +24,26 @@ class RangeAdapterMode(Enum): BOTH = "both" +SSL_CONNECTION_PARAMS = ( + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", +) + + @dataclass class PostgresSSLMixin: - """Mixin implementing postgres-specific SSL/TLS options.""" + """Mixin implementing postgres-specific SSL/TLS options. + + The field names mirror libpq connection keywords so they can be handed to + ``psycopg.connect()`` unchanged. They are deliberately distinct from the + generic :class:`~rhosocial.activerecord.backend.config.SSLMixin` names + (``ssl_ca``/``ssl_cert``/``ssl_key``/``ssl_mode``), which libpq does not + understand. + """ sslmode: Optional[str] = None # 'disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full' sslcert: Optional[str] = None @@ -96,6 +113,22 @@ class PostgresConnectionConfig( enable_type_compatibility_warnings: Enable type conversion warnings (default: True) """ + def get_ssl_connection_params(self) -> Dict[str, Any]: + """Get the libpq SSL/TLS keywords that should be forwarded to the driver. + + Both the sync and async backends call this when building their + ``psycopg.connect()`` keyword arguments, so the two paths cannot drift + apart. ``None`` values are dropped so that psycopg (which also drops + ``None``) falls back to libpq/PGSERVICE defaults for anything the user + did not set. + """ + params: Dict[str, Any] = {} + for name in SSL_CONNECTION_PARAMS: + value = getattr(self, name, None) + if value is not None: + params[name] = value + return params + def to_dict(self) -> Dict[str, Any]: """Convert config to dictionary, including postgres-specific parameters.""" # Get base config diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py b/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py similarity index 56% rename from src/rhosocial/activerecord/backend/impl/postgres/types/enum.py rename to src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py index e27cc15f..3efb5e3a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py @@ -1,10 +1,8 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/enum.py -"""PostgreSQL ENUM type support. +# src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py +"""PostgreSQL ENUM type lifecycle management. This module provides: -- PostgresEnumType: Type reference expression for ENUM types -- EnumTypeManager for lifecycle management -- Enum utility functions (enum_range, enum_first, enum_last, etc.) +- EnumTypeManager for lifecycle management of PostgreSQL ENUM types PostgreSQL Documentation: https://www.postgresql.org/docs/current/datatype-enum.html @@ -16,166 +14,20 @@ - Adding values: PostgreSQL 9.1+ """ -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from rhosocial.activerecord.backend.expression.bases import BaseExpression - -if TYPE_CHECKING: - from rhosocial.activerecord.backend.dialect import SQLDialectBase +from typing import Any, Dict, List, Optional from rhosocial.activerecord.backend.expression.statements.ddl_type import ( AlterTypeExpression, CreateTypeExpression, ) -from ..expression.ddl import ( +from .expression.ddl import ( PostgresAddEnumValueAction, PostgresDropTypeExpression, PostgresEnumTypeDefinition, PostgresRenameEnumValueAction, ) - - -class PostgresEnumType(BaseExpression): - """PostgreSQL ENUM type reference expression. - - This class represents a PostgreSQL ENUM type reference as a SQL expression. - It is used in column definitions and type casts, NOT for DDL operations. - - For DDL operations (CREATE TYPE, DROP TYPE, etc.), use: - - PostgresCreateEnumTypeExpression - - PostgresDropEnumTypeExpression - - PostgresAlterEnumTypeAddValueExpression - - PostgresAlterEnumTypeRenameValueExpression - - PostgreSQL ENUM types are custom types created with CREATE TYPE. - They are reusable across tables, unlike MySQL's inline ENUM. - - Attributes: - name: Enum type name - values: List of allowed values - schema: Optional schema name - - Examples: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect - >>> dialect = PostgresDialect() - >>> status_enum = PostgresEnumType( - ... dialect=dialect, - ... name='video_status', - ... values=['pending', 'processing', 'ready', 'failed'] - ... ) - >>> status_enum.to_sql() - ('video_status', ()) - - >>> # With schema - >>> status_enum = PostgresEnumType( - ... dialect=dialect, - ... name='video_status', - ... values=['draft', 'published'], - ... schema='app' - ... ) - >>> status_enum.to_sql() - ('app.video_status', ()) - """ - - def __init__(self, dialect: "SQLDialectBase", name: str, values: List[str], schema: Optional[str] = None): - """Initialize PostgreSQL ENUM type reference expression. - - Args: - dialect: SQL dialect instance - name: Enum type name - values: List of allowed values - schema: Optional schema name - - Raises: - ValueError: If name is empty or values is empty - """ - super().__init__(dialect) - - if not name: - raise ValueError("Enum type name cannot be empty") - if not values: - raise ValueError("ENUM must have at least one value") - if len(values) != len(set(values)): - raise ValueError("ENUM values must be unique") - - self._name = name - self._values = list(values) - self._schema = schema - - @property - def name(self) -> str: - """Enum type name.""" - return self._name - - @property - def values(self) -> List[str]: - """List of allowed values.""" - return list(self._values) - - @property - def schema(self) -> Optional[str]: - """Schema name, if any.""" - return self._schema - - @property - def format_method(self) -> str: - """The dialect formatting method that renders this expression.""" - return "format_enum_type_expression" - - def validate_value(self, value: str) -> bool: - """Check if a value is valid for this enum. - - Args: - value: Value to validate - - Returns: - True if value is valid - """ - return value in self._values - - @classmethod - def from_python_enum( - cls, dialect: "SQLDialectBase", enum_class: type, name: Optional[str] = None, schema: Optional[str] = None - ) -> "PostgresEnumType": - """Create PostgresEnumType from Python Enum class. - - Args: - dialect: SQL dialect instance - enum_class: Python Enum class - name: Optional type name (defaults to enum class name in lowercase) - schema: Optional schema name - - Returns: - PostgresEnumType instance - """ - from enum import Enum - - if not issubclass(enum_class, Enum): - raise TypeError("enum_class must be a Python Enum class") - - type_name = name or enum_class.__name__.lower() - values = [e.name for e in enum_class] - return cls(dialect=dialect, name=type_name, values=values, schema=schema) - - def __str__(self) -> str: - if self._schema: - return f"{self._schema}.{self._name}" - return self._name - - def __repr__(self) -> str: - return ( - f"PostgresEnumType(dialect={self._dialect!r}, name={self._name!r}, " - f"values={self._values!r}, schema={self._schema!r})" - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, PostgresEnumType): - return NotImplemented - return self._name == other._name and self._values == other._values and self._schema == other._schema - - def __hash__(self) -> int: - return hash((self._name, tuple(self._values), self._schema)) +from .expression.enum_ import PostgresEnumType class EnumTypeManager: @@ -364,4 +216,5 @@ def get_type_values(self, name: str, schema: Optional[str] = None) -> Optional[L return [r["enumlabel"] for r in results] if results else None -__all__ = ["PostgresEnumType", "EnumTypeManager"] + +__all__ = ["EnumTypeManager"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py index ea715be8..fb4b1dcd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py @@ -29,7 +29,7 @@ ColumnConstraintType, ColumnDefinition, ) -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py index ca5cb766..2cf85a23 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py @@ -8,7 +8,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py index 5e0c1ea3..ac10af4b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py index 5fd1e5a0..2256fe52 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py index 759c709c..bc611ee0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py @@ -10,7 +10,7 @@ # SECTION: Setup # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py index 6057a364..cbd91864 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py @@ -22,7 +22,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, @@ -45,7 +45,7 @@ ColumnConstraint, ColumnConstraintType, ) -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresAlterMaterializedViewExpression, PostgresChangeMaterializedViewOwnerAction, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py index 3a8fc5f4..2944e802 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( DropTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py index cd371781..e342af32 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py index 29ec005e..fc75c2d4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py index 896b65aa..8d5b9ccd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py index dbcc571b..e1c1a8aa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py index f8a8efdb..6b980e2d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py index 2e1333fe..3d7a38d4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py index b2201ee0..bec5324b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py index 4d819c0d..93c9fcaa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py index 1a474e88..8ee6f04f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py index 67b91a56..db9d472e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py index 6adf5266..19de3671 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py @@ -17,7 +17,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py index 0b1a6d77..8115c7a7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py index 5c233e3a..829f6301 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py index c3fb44f6..63dadc7c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py index 4cdd848d..d3bace83 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py index 823fc37c..1b823337 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py index 5c8d9b7d..848ad0f1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py index 96f65570..65ab4f7e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py @@ -19,7 +19,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py index 68b6e1da..b3c81ebd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py @@ -20,7 +20,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py index ce19f1c4..f7817f42 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py @@ -16,7 +16,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py index 801456fe..5c107760 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py @@ -18,7 +18,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py index 612284f6..06c9eb2a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py index ab748414..91f6cc9a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py index 0452d6d7..920a72d9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py index 19ab5de4..7e55f039 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py index 7840a516..1668005f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py index 10e92200..af6f2d19 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py index e20bffb5..fc0f88c1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py index 8b5543cc..ece4456b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py index 9ce28c39..3026e3e0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py index 375c2029..c02d94e3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py index 7b27544d..3ed5e65e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py index 9ccacaa9..42eaac9e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py index fd3bfeb2..09c25c26 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py @@ -10,7 +10,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py index 9d1c98f2..9e3378e8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py index 89c056ac..92240083 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py index f597f210..ea29824e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py @@ -162,7 +162,8 @@ def prepare_orders_demo(backend) -> None: if __name__ == "__main__": import os - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host=os.getenv("POSTGRES_HOST", "127.0.0.1"), diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py index 8effaa83..c2322992 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py @@ -19,7 +19,7 @@ import tempfile import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.migration import ( MigrationRunner, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py index f2c4f07e..b09f2852 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py @@ -17,7 +17,7 @@ import tempfile import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.migration import ( MigrationRunner, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py index 20db15c2..c2e6e9a4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py @@ -135,7 +135,8 @@ def run(self, ctx: ProcedureContext) -> None: if __name__ == "__main__": import os - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions import ( prepare_orders_demo, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py index dccd0fb3..236dc523 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py @@ -19,7 +19,7 @@ # SECTION: Setup (reference only — creates tables and data) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py index e62a3234..abcd4eac 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py index 06431c89..30f7c1a2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py index d68b866e..de626b8b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py index 691982b1..13812756 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py index 0790ff44..31505509 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py index 3b666510..7565f68c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py @@ -10,7 +10,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py index e733712d..a945175a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py index a8d68e30..947fb974 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py index 6f087cbf..c6159fab 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py index cc2f6f33..f9cc7b2a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py index 56231777..262a0a35 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py index a9b76948..8e59959a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py index d89b00cf..24a234f7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py @@ -10,7 +10,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py index 5df3faf4..1f97712c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py index 7a1ab16b..9b1731cb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py index 3ee9ea7d..b2ba8fb5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py index 86b9f629..48e98a9b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py index 1c8598eb..55b64319 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py index 5e0e63de..25654c6a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py @@ -17,7 +17,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression.statements import IdentityClause from rhosocial.activerecord.backend.expression import ( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py index c138e480..86413613 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py @@ -12,7 +12,7 @@ # ============================================================ import json import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py index e6cc2703..55f3dcb3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py index dba2aac5..157695db 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py @@ -41,11 +41,9 @@ xmlserialize, xmltable, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig, - PostgresDialect, -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect SQLParam = Optional[Union[str, int, float, bool]] SQLParams = Tuple[SQLParam, ...] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py index 6eb5c9e3..7dd699ee 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py index f5bda6ef..1e720f7a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py index fc8cc9fa..33b46164 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py index 36ef2ff2..7aefe90a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py index da478356..9479fad4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py index 21c54946..7ce054a5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py index c2b32db9..73f56aef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py index 6c374b85..add86c57 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/expression/advisory/__init__.py +# src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py """ Advisory lock expression classes. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py index e5de03c5..3b822a3d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py @@ -42,7 +42,7 @@ Example:: - from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py index 3b0999e3..0e9c250a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py @@ -36,7 +36,7 @@ class PostgresClusterExpression(BaseExpression): verbose: When True, report the table being clustered (``VERBOSE``). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresClusterExpression( ... dialect, table_name="orders", using_index="orders_pkey" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py index 59ba7d03..b8a029bf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py @@ -35,7 +35,7 @@ class PostgresCommentExpression(BaseExpression): schema: Schema name for the object. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Comment on a table >>> comment = PostgresCommentExpression( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py index 2a1b99b1..05c53bd5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py @@ -38,7 +38,7 @@ class PostgresCreateExtensionExpression(BaseExpression): cascade: Cascade to dependent extensions. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> create_ext = PostgresCreateExtensionExpression( ... dialect=dialect, @@ -94,7 +94,7 @@ class PostgresDropExtensionExpression(BaseExpression): restrict: Drop only if no dependent objects (RESTRICT). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> drop_ext = PostgresDropExtensionExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py index 5d5eb035..287c6265 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py @@ -36,7 +36,7 @@ Example:: - from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) @@ -89,7 +89,7 @@ class PostgresAlterIndexExpression(BaseExpression): - Bulk tablespace move with ALL IN TABLESPACE Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Rename index >>> expr = PostgresAlterIndexExpression( @@ -153,7 +153,7 @@ class PostgresReindexExpression(BaseExpression): verbose: Print progress messages. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Reindex a specific index >>> reindex = PostgresReindexExpression( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py index 7145123d..5aae391d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py @@ -169,7 +169,7 @@ class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpressio concurrently: Alias for concurrent (backward compatibility). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Regular refresh >>> refresh = PostgresRefreshMaterializedViewExpression( @@ -277,7 +277,7 @@ class PostgresSetMaterializedViewPropertiesAction(MaterializedViewAlterAction): :class:`~....storage_parameters.PostgresStorageParameter`. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter + >>> from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter >>> action = PostgresSetMaterializedViewPropertiesAction( ... dialect, ... {PostgresStorageParameter.FILLFACTOR: 90, "autovacuum_enabled": "true"}, @@ -346,7 +346,7 @@ class PostgresAlterMaterializedViewExpression(BaseExpression): """Expression for ``ALTER MATERIALIZED VIEW``. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( ... PostgresAlterMaterializedViewExpression, ... PostgresRenameMaterializedViewAction, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py index e087a908..6dd27f34 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py @@ -74,7 +74,7 @@ class PartitionValue(BaseExpression): partition_type: Optional RANGE, LIST, or HASH context. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> PartitionValue(dialect=dialect, value=None).to_sql() ('NULL', ()) @@ -169,7 +169,7 @@ class PostgresCreatePartitionExpression(BaseExpression): if_not_exists: Add IF NOT EXISTS clause. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # RANGE partition >>> partition = PostgresCreatePartitionExpression( @@ -243,7 +243,7 @@ class PostgresDetachPartitionExpression(BaseExpression): finalize: Finalize concurrent detach (PG 14+). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> detach = PostgresDetachPartitionExpression( ... dialect=dialect, @@ -299,7 +299,7 @@ class PostgresAttachPartitionExpression(BaseExpression): UnsupportedFeatureError: If ``concurrently=True``. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> attach = PostgresAttachPartitionExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py index 2feabfbd..3a4590ad 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py @@ -91,7 +91,7 @@ class PostgresCreatePolicyExpression(BaseExpression): when ``command`` is SELECT or DELETE. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresCreatePolicyExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py index 133fd743..03c75524 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py @@ -57,7 +57,7 @@ class PostgresAlterTableRlsExpression(BaseExpression): ``ALWAYS`` has no meaning with ``DISABLE`` and raises an error. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresAlterTableRlsExpression( ... dialect, table_name="orders", mode=RlsConfigurationMode.ENABLE diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py index 37f597df..a1ab1327 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py @@ -39,7 +39,7 @@ class PostgresCreateStatisticsExpression(BaseExpression): if_not_exists: Add IF NOT EXISTS clause. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Create ndistinct statistics >>> stats = PostgresCreateStatisticsExpression( @@ -99,7 +99,7 @@ class PostgresDropStatisticsExpression(BaseExpression): if_exists: Add IF EXISTS clause (prevent error if not exists). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> drop = PostgresDropStatisticsExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py index e5aaeb10..e15391a3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py @@ -50,7 +50,7 @@ class PostgresAlterTableSettingsExpression(BaseExpression): ``logging_mode``. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresAlterTableSettingsExpression( ... dialect, table_name="orders", mode=LoggingMode.UNLOGGED diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py index 95c3dc7c..5651d7e3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py @@ -46,7 +46,7 @@ class PostgresVacuumExpression(BaseExpression): columns: Specific columns to analyze (PG 16+). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> vacuum = PostgresVacuumExpression( ... dialect=dialect, @@ -118,7 +118,7 @@ class PostgresAnalyzeExpression(BaseExpression): columns: Specific columns to analyze (PG 16+). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> analyze = PostgresAnalyzeExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py new file mode 100644 index 00000000..4b0c24b4 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py @@ -0,0 +1,168 @@ +# src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py +"""PostgreSQL ENUM type reference expression. + +This module provides: +- PostgresEnumType: Type reference expression for ENUM types +- Enum utility functions (enum_range, enum_first, enum_last, etc.) + +PostgreSQL Documentation: https://www.postgresql.org/docs/current/datatype-enum.html + +PostgreSQL ENUM types are custom types created with CREATE TYPE. +They are reusable across tables. + +Version requirements: +- Basic ENUM: PostgreSQL 8.3+ +- Adding values: PostgreSQL 9.1+ +""" + +from typing import TYPE_CHECKING, List, Optional + +from rhosocial.activerecord.backend.expression.bases import BaseExpression + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.dialect import SQLDialectBase + + +class PostgresEnumType(BaseExpression): + """PostgreSQL ENUM type reference expression. + + This class represents a PostgreSQL ENUM type reference as a SQL expression. + It is used in column definitions and type casts, NOT for DDL operations. + + For DDL operations (CREATE TYPE, DROP TYPE, etc.), use: + - PostgresCreateEnumTypeExpression + - PostgresDropEnumTypeExpression + - PostgresAlterEnumTypeAddValueExpression + - PostgresAlterEnumTypeRenameValueExpression + + PostgreSQL ENUM types are custom types created with CREATE TYPE. + They are reusable across tables, unlike MySQL's inline ENUM. + + Attributes: + name: Enum type name + values: List of allowed values + schema: Optional schema name + + Examples: + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + >>> dialect = PostgresDialect() + >>> status_enum = PostgresEnumType( + ... dialect=dialect, + ... name='video_status', + ... values=['pending', 'processing', 'ready', 'failed'] + ... ) + >>> status_enum.to_sql() + ('video_status', ()) + + >>> # With schema + >>> status_enum = PostgresEnumType( + ... dialect=dialect, + ... name='video_status', + ... values=['draft', 'published'], + ... schema='app' + ... ) + >>> status_enum.to_sql() + ('app.video_status', ()) + """ + + def __init__(self, dialect: "SQLDialectBase", name: str, values: List[str], schema: Optional[str] = None): + """Initialize PostgreSQL ENUM type reference expression. + + Args: + dialect: SQL dialect instance + name: Enum type name + values: List of allowed values + schema: Optional schema name + + Raises: + ValueError: If name is empty or values is empty + """ + super().__init__(dialect) + + if not name: + raise ValueError("Enum type name cannot be empty") + if not values: + raise ValueError("ENUM must have at least one value") + if len(values) != len(set(values)): + raise ValueError("ENUM values must be unique") + + self._name = name + self._values = list(values) + self._schema = schema + + @property + def name(self) -> str: + """Enum type name.""" + return self._name + + @property + def values(self) -> List[str]: + """List of allowed values.""" + return list(self._values) + + @property + def schema(self) -> Optional[str]: + """Schema name, if any.""" + return self._schema + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_enum_type_expression" + + def validate_value(self, value: str) -> bool: + """Check if a value is valid for this enum. + + Args: + value: Value to validate + + Returns: + True if value is valid + """ + return value in self._values + + @classmethod + def from_python_enum( + cls, dialect: "SQLDialectBase", enum_class: type, name: Optional[str] = None, schema: Optional[str] = None + ) -> "PostgresEnumType": + """Create PostgresEnumType from Python Enum class. + + Args: + dialect: SQL dialect instance + enum_class: Python Enum class + name: Optional type name (defaults to enum class name in lowercase) + schema: Optional schema name + + Returns: + PostgresEnumType instance + """ + from enum import Enum + + if not issubclass(enum_class, Enum): + raise TypeError("enum_class must be a Python Enum class") + + type_name = name or enum_class.__name__.lower() + values = [e.name for e in enum_class] + return cls(dialect=dialect, name=type_name, values=values, schema=schema) + + def __str__(self) -> str: + if self._schema: + return f"{self._schema}.{self._name}" + return self._name + + def __repr__(self) -> str: + return ( + f"PostgresEnumType(dialect={self._dialect!r}, name={self._name!r}, " + f"values={self._values!r}, schema={self._schema!r})" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PostgresEnumType): + return NotImplemented + return self._name == other._name and self._values == other._values and self._schema == other._schema + + def __hash__(self) -> int: + return hash((self._name, tuple(self._values), self._schema)) + + +__all__ = ["PostgresEnumType"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py index b8402df4..7e611f88 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py @@ -79,7 +79,7 @@ def citext_literal( BaseExpression with ::citext type cast applied Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> citext_literal(dialect, 'Hello World') # Produces: 'Hello World'::citext diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py index ab1f5033..6d440e47 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py @@ -92,7 +92,7 @@ def cube_literal( FunctionCall for cube(ARRAY[coordinates]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> cube_literal(dialect, [1.0, 2.0, 3.0]) # Generates SQL: cube(ARRAY[1.0, 2.0, 3.0]) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py index edef188e..cbef72fc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py @@ -36,7 +36,7 @@ def xid8_literal( BaseExpression with ::xid8 type cast applied Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> xid8_literal(dialect, 123456789) # Produces: 123456789::xid8 @@ -67,7 +67,7 @@ def array_literal( BaseExpression representing the array literal Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> array_literal(dialect, [1, 2, 3], 'int') # Produces parameterized: '{1,2,3}'::int[] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py index 4ab6af4e..354c1cef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py @@ -81,7 +81,7 @@ def ll_to_earth( FunctionCall for ll_to_earth(latitude, longitude) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> ll_to_earth(dialect, 40.7128, -74.0060) # Generates: ll_to_earth(40.7128, -74.006) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py index 4dab00e9..0ffdd6a5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py @@ -77,7 +77,7 @@ def levenshtein( FunctionCall for levenshtein(source, target) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> levenshtein(dialect, 'name_col', 'search_term') """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py index d834337c..5a953f47 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py @@ -38,7 +38,7 @@ from rhosocial.activerecord.backend.expression import bases, core from rhosocial.activerecord.backend.expression.operators import BinaryExpression -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py index c64ba3c5..adc54173 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py @@ -89,7 +89,7 @@ def intarray_contains( BinaryExpression for column @> values Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> intarray_contains(dialect, 'tags', '{1,2}') # Generates SQL: tags @> '{1,2}' diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py index 18636ec4..377b961b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath def _convert_to_expression( @@ -64,7 +64,7 @@ def _convert_jsonpath( Returns: BaseExpression representing the jsonpath literal """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return core.Literal(dialect, str(path)) @@ -87,7 +87,7 @@ def json_path_root() -> "PostgresJsonPath": >>> json_path_root() PostgresJsonPath('$') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath return PostgresJsonPath("$") @@ -109,7 +109,7 @@ def json_path_key(path: Union["PostgresJsonPath", str], key: str) -> "PostgresJs >>> json_path_key('$.store', 'book') PostgresJsonPath('$.store.book') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.key(key) @@ -133,7 +133,7 @@ def json_path_index(path: Union["PostgresJsonPath", str], index: Union[int, str] >>> json_path_index('$.items', 'last') PostgresJsonPath('$.items[last]') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.index(index) @@ -160,7 +160,7 @@ def json_path_wildcard(path: Union["PostgresJsonPath", str], array: bool = True) >>> json_path_wildcard('$.items') PostgresJsonPath('$.items[*]') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.wildcard_array() if array else path.wildcard_object() @@ -185,7 +185,7 @@ def json_path_filter(path: Union["PostgresJsonPath", str], condition: str) -> "P >>> json_path_filter('$.books[*]', '@.author == "King"') PostgresJsonPath('$.books[*]?(@.author == "King")') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.filter(condition) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py index b72279c3..3406cdb8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py @@ -93,7 +93,7 @@ def ltree_literal( Literal expression for the ltree path Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> ltree_literal(dialect, 'Top.Science.Astronomy') """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py index cab407bf..aaede880 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py @@ -90,7 +90,7 @@ def cron_schedule( FunctionCall for cron_schedule(schedule, command[, comment]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> cron_schedule(dialect, '0 * * * *', 'DELETE FROM logs WHERE created < now() - interval ''7 days''') >>> cron_schedule(dialect, '30 3 * * *', 'VACUUM ANALYZE', 'Nightly vacuum') diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py index f5773b90..2ec5e5fd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py @@ -63,7 +63,7 @@ def pg_stat_statements_reset( FunctionCall for pg_stat_statements_reset() Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pg_stat_statements_reset(dialect) # Generates: pg_stat_statements_reset() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py index 6df5882e..7138f136 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py @@ -84,7 +84,7 @@ def similarity( FunctionCall for similarity(text1, text2) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> similarity(dialect, 'name_col', 'search_term') >>> # Generates: similarity(name_col, 'search_term') @@ -119,7 +119,7 @@ def word_similarity( FunctionCall for word_similarity(column, query) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> word_similarity(dialect, 'title_col', 'search') >>> # Generates: word_similarity(title_col, 'search') @@ -151,7 +151,7 @@ def show_trgm( FunctionCall for show_trgm(text) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> show_trgm(dialect, 'hello') >>> # Generates: show_trgm('hello') @@ -189,7 +189,7 @@ def similarity_operator( BinaryExpression for column % text (or column !% text if negate) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> similarity_operator(dialect, 'name_col', 'search') >>> # Generates: name_col % 'search' diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py index 2d677e74..a21ad855 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py @@ -88,7 +88,7 @@ def pgaudit_set_role( FunctionCall for set_config('pgaudit.role', role, false) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pgaudit_set_role(dialect, 'audit_role') # Generates: set_config('pgaudit.role', 'audit_role', false) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py index 0e4a27b3..3462cf4f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py @@ -82,7 +82,7 @@ def gen_salt( FunctionCall for gen_salt(algorithm) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> gen_salt(dialect, 'bf') >>> gen_salt(dialect) # uses md5 diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py index e0a2123e..2c918f19 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py @@ -82,7 +82,7 @@ def pglogical_create_node( FunctionCall for pglogical.create_node(node_name, dsn) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pglogical_create_node(dialect, 'provider_node', 'host=127.0.0.1 dbname=mydb') """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py index a32d08fc..079e9288 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py @@ -89,7 +89,7 @@ def pgr_dijkstra( FunctionCall for pgr_dijkstra(edges_sql, start_vid, end_vid[, directed]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pgr_dijkstra( ... dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py index 00e88d31..30eb0cf8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py @@ -25,7 +25,7 @@ from rhosocial.activerecord.backend.expression import bases, core from rhosocial.activerecord.backend.expression.operators import BinaryArithmeticExpression -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase @@ -90,7 +90,7 @@ def vector_l2_distance( BinaryArithmeticExpression for the distance calculation Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> d = PostgresDialect() >>> expr = vector_l2_distance(d, "embedding", [1.0, 2.0, 3.0]) """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py index a3b62882..510861ba 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py @@ -15,7 +15,7 @@ from typing import Optional, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression import bases, core -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py index a4b9f9f7..f3587d69 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py @@ -92,7 +92,7 @@ def st_rast_from_hexwkb( FunctionCall for ST_SetSRID(ST_RastFromHexWKB(raster_data), srid) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> st_rast_from_hexwkb(dialect, '0100000...', srid=4326) # Generates: ST_SetSRID(ST_RastFromHexWKB('0100000...'), 4326) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py index fd5abdb0..e45ce036 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py @@ -16,7 +16,7 @@ from rhosocial.activerecord.backend.expression import bases, core from rhosocial.activerecord.backend.expression.operators import BinaryExpression -from rhosocial.activerecord.backend.impl.postgres.types.range import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.type_values.range import PostgresRange if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase @@ -69,7 +69,7 @@ def range_contains( BinaryExpression for range @> element Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> range_contains(dialect, 'int4range_col', 5) """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py index 31452d75..d701431c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py @@ -94,7 +94,7 @@ def crosstab( FunctionCall for crosstab(source_sql[, categories_sql]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> crosstab(dialect, 'SELECT row_name, category, value FROM data') >>> crosstab( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py index 4810f5a6..d129ed1b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase -from ..types.xml import PostgresXML +from ..type_values.xml import PostgresXML def _convert_to_expression( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py b/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py index 5964c3f7..454e69a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py @@ -245,7 +245,7 @@ class SyncPostgreSQLStatusIntrospector( Usage:: - backend = PostgreSQLBackend(connection_config=config) + backend = PostgresBackend(connection_config=config) backend.connect() status = backend.introspector.status.get_overview() print(status.server_version) @@ -839,7 +839,7 @@ class AsyncPostgreSQLStatusIntrospector( Usage:: - backend = AsyncPostgreSQLBackend(connection_config=config) + backend = AsyncPostgresBackend(connection_config=config) await backend.connect() status = await backend.introspector.status.get_overview() print(status.server_version) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py index 2950ba46..9d4bf39c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +# src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py import re from typing import Any, List, Tuple diff --git a/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py index 2da88248..9cc51fa3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py @@ -55,7 +55,7 @@ def create_next_monthly_partition( ValueError: If reference_date is not a valid date string. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = create_next_monthly_partition( ... dialect, "orders", reference_date="2026-06-01") @@ -128,7 +128,7 @@ def create_range_partition_for_month( ValueError: If month is not in 1..12. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = create_range_partition_for_month(dialect, "orders", 2026, 6) >>> sql, _ = expr.to_sql() @@ -186,7 +186,7 @@ def create_range_partition_for_quarter( ValueError: If quarter is not in 1..4. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = create_range_partition_for_quarter(dialect, "orders", 2026, 1) >>> sql, _ = expr.to_sql() @@ -253,7 +253,7 @@ def create_range_partitions_for_interval( if date range is invalid. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> exprs = create_range_partitions_for_interval( ... dialect, "orders", "2026-01-01", "2026-03-01") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py b/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py index 35686517..dee395f0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgresql/reserved_words.py +# src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py """ PostgreSQL reserved words list. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py similarity index 97% rename from src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py index d67bbdf2..423d066a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py """ PostgreSQL type definitions. @@ -21,12 +21,6 @@ - xml: XML type - https://www.postgresql.org/docs/current/datatype-xml.html """ -# Enum types -from .enum import ( - PostgresEnumType, - EnumTypeManager, -) - # Bit string types from .bit_string import ( PostgresBitString, @@ -226,8 +220,6 @@ __all__ = [ # Enum types - "PostgresEnumType", - "EnumTypeManager", # Bit string types "PostgresBitString", # Geometric types diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py similarity index 97% rename from src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py index 7346f57a..fd492ef8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py """ PostgreSQL bit string type representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/constants.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/constants.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py index 447331a6..89b15f44 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/constants.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/constants.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py """PostgreSQL data type constants. These constants provide convenient type name references for use with @@ -7,7 +7,7 @@ PostgreSQL Documentation: https://www.postgresql.org/docs/current/datatype.html Example: - >>> from rhosocial.activerecord.backend.impl.postgres.types.constants import MONEY, NUMERIC + >>> from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC >>> col.cast(MONEY) # Equivalent to col.cast("money") >>> col.cast("money") # Also valid diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py index 1d90e96e..03b50d4c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py """ PostgreSQL geometric types representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py index 10ac04fe..fc626551 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py """ PostgreSQL hstore type definition. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/json.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/json.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py index e66c317c..c7ce70c8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/json.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py """PostgreSQL jsonpath type representation. This module provides PostgresJsonPath for representing PostgreSQL diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py index db73ddd5..5a8c4d17 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py """ PostgreSQL MONEY type representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py index 14cdd8e5..ead3492c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py """PostgreSQL Network Address Types. This module provides type representations for PostgreSQL network address types: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py index 562e5831..e4150edd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py """ PostgreSQL Object Identifier (OID) types representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py index fc6a37f5..c1d6bffe 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py """PostgreSQL pg_lsn type representation. This module provides PostgresLsn for representing PostgreSQL diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py index 7f2f174d..660b8265 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py """ PostgreSQL pgvector vector type. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py index 0baebe53..9771a78d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py """ PostgreSQL PostGIS geometry/geography type. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/range.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/range.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py index accf2f4b..a73a3e64 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/range.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py """ PostgreSQL range types representation. @@ -45,7 +45,7 @@ 1. **Recommended**: Use PostgresRange for Python code: ```python - from rhosocial.activerecord.backend.impl.postgres.types import PostgresRange + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresRange range_obj = PostgresRange(1, 10) ``` diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py index da10b5e1..f099efc1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py """ PostgreSQL text search types representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py similarity index 96% rename from src/rhosocial/activerecord/backend/impl/postgres/types/xml.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py index 6e377196..c8b4b0a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/xml.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py """ PostgreSQL XML type representation. diff --git a/tests/benchmark/backend/conftest.py b/tests/benchmark/backend/conftest.py index 58825d1c..a8371131 100644 --- a/tests/benchmark/backend/conftest.py +++ b/tests/benchmark/backend/conftest.py @@ -70,7 +70,7 @@ def pytest_configure(config): @pytest.fixture(scope="function", params=SCENARIO_PARAMS) def postgres_backend_sync_context(request, benchmark_size): - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend scenario = request.param _, config = get_scenario(scenario) @@ -96,7 +96,9 @@ def postgres_backend_sync_context(request, benchmark_size): @pytest.fixture(scope="function", params=SCENARIO_PARAMS) def postgres_backend_async_context(request, benchmark_size): - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) scenario = request.param _, config = get_scenario(scenario) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index bcd7be81..7547b1f4 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -356,7 +356,7 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = N if pooled_db: config_dict = {**config_dict, "database": pooled_db} return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres.backend', 'backend_class_name': backend_class_name, 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', 'config_class_name': 'PostgresConnectionConfig', @@ -395,7 +395,9 @@ async def get_dialect(self, scenario_name: str = "default"): return super().get_dialect(scenario_name) async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/basic_connection.py b/tests/providers/basic_connection.py index 36aefaf8..9e4ab4b4 100644 --- a/tests/providers/basic_connection.py +++ b/tests/providers/basic_connection.py @@ -8,7 +8,7 @@ from typing import Type, Tuple, Optional, List from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # noqa: F401 from rhosocial.activerecord.connection.pool import BackendPool, AsyncBackendPool, PoolConfig from rhosocial.activerecord.backend.options import ExecutionOptions @@ -103,7 +103,9 @@ def setup_sync_pool_and_model(self, scenario_name: str) -> Tuple[BackendPool, Ty async def setup_async_pool_and_model(self, scenario_name: str) -> Tuple[AsyncBackendPool, Type[AsyncActiveRecord]]: """Setup async connection pool and model for context tests.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) diff --git a/tests/providers/crud_benchmark.py b/tests/providers/crud_benchmark.py index 675a8d14..bfc77fdd 100644 --- a/tests/providers/crud_benchmark.py +++ b/tests/providers/crud_benchmark.py @@ -128,7 +128,9 @@ def _setup_model(self, model_class: Type[ActiveRecord], scenario: str) -> Type[A async def _setup_async_model( self, model_class: Type[ActiveRecord], scenario: str ) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario) await model_class.configure(config, AsyncPostgresBackend) diff --git a/tests/providers/events.py b/tests/providers/events.py index b53f7499..f20be52f 100644 --- a/tests/providers/events.py +++ b/tests/providers/events.py @@ -159,7 +159,9 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/fastapi_benchmark.py b/tests/providers/fastapi_benchmark.py index db0ae072..3ebdcba6 100644 --- a/tests/providers/fastapi_benchmark.py +++ b/tests/providers/fastapi_benchmark.py @@ -143,7 +143,9 @@ async def teardown_benchmark_async( async def _setup_async_model( self, model_class: Type[ActiveRecord], scenario: str ) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario) await model_class.configure(config, AsyncPostgresBackend) @@ -158,7 +160,9 @@ async def _initialize_schema_async(self, backend): await backend.execute(self._schema_sql(), options=options) def _create_async_context_factory(self, config): - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) @asynccontextmanager async def context_factory(): @@ -177,7 +181,9 @@ def _fastapi_runtime_config(self, connection_strategy): ) from exc async def _create_async_pool(self, config, runtime_config): - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) from rhosocial.activerecord.connection.pool import AsyncBackendPool, PoolConfig pool_config = PoolConfig( diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py index fec5d58f..1443a91f 100644 --- a/tests/providers/fixtures/basic.py +++ b/tests/providers/fixtures/basic.py @@ -38,7 +38,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresSerialType, PostgresUUIDType, PostgresByteaType, diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py index 3e942487..a31e189c 100644 --- a/tests/providers/fixtures/events.py +++ b/tests/providers/fixtures/events.py @@ -20,9 +20,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresSerialType, -) +from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType # --------------------------------------------------------------------------- diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py index 9b706da8..a70e53a7 100644 --- a/tests/providers/fixtures/mixins.py +++ b/tests/providers/fixtures/mixins.py @@ -22,9 +22,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresSerialType, -) +from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType # --------------------------------------------------------------------------- diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py index 2f0fbdec..61c423ab 100644 --- a/tests/providers/fixtures/query.py +++ b/tests/providers/fixtures/query.py @@ -25,9 +25,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresSerialType, -) +from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType _CASCADE = ReferentialAction.CASCADE diff --git a/tests/providers/mixins.py b/tests/providers/mixins.py index 72afb884..42f844c1 100644 --- a/tests/providers/mixins.py +++ b/tests/providers/mixins.py @@ -183,7 +183,9 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/partition.py b/tests/providers/partition.py index c6d082f6..571711e9 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -16,7 +16,7 @@ PartitionStrategy, TruncateExpression, ) -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresAttachPartitionExpression, PostgresColumnDefinition, diff --git a/tests/providers/query.py b/tests/providers/query.py index 1e18c2a2..142790bc 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -417,7 +417,7 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = ' if pooled_db: config_dict = {**config_dict, "database": pooled_db} return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres.backend', 'backend_class_name': backend_class_name, 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', 'config_class_name': 'PostgresConnectionConfig', @@ -468,7 +468,9 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) if shared_backend is None: await model_class.configure(config, AsyncPostgresBackend) @@ -580,7 +582,9 @@ async def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveR async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: """Set up the composite-PK AsyncOrderItem model for the query feature tests.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType from providers.fixtures._common import drop_table from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_EXPRS @@ -609,10 +613,10 @@ async def setup_schema_fixtures(self, scenario_name: str): AsyncSchemaOrder, ) - from rhosocial.activerecord.backend.impl.postgres import ( + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( AsyncPostgresBackend, - PostgresBackend, ) + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend _, config = get_scenario(scenario_name) @@ -665,7 +669,9 @@ async def cleanup_after_test(self, scenario_name: str): async def setup_mixed_schema_fixtures(self, scenario_name: str): """(AsyncUser, AsyncOrder, AsyncMixedSchemaOrder) with orders also in SCHEMA_A.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) from rhosocial.activerecord.testsuite.feature.query.cross_schema.mixed_schema_models import ( AsyncMixedSchemaOrder, ) diff --git a/tests/providers/query_connection.py b/tests/providers/query_connection.py index 7fab8e23..f26e8a00 100644 --- a/tests/providers/query_connection.py +++ b/tests/providers/query_connection.py @@ -8,7 +8,7 @@ from typing import Type, Tuple, Optional, List from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # noqa: F401 from rhosocial.activerecord.connection.pool import BackendPool, AsyncBackendPool, PoolConfig from rhosocial.activerecord.backend.options import ExecutionOptions @@ -103,7 +103,9 @@ def setup_sync_pool_and_model(self, scenario_name: str) -> Tuple[BackendPool, Ty async def setup_async_pool_and_model(self, scenario_name: str) -> Tuple[AsyncBackendPool, Type[AsyncActiveRecord]]: """Setup async connection pool and model for query context tests.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) diff --git a/tests/providers/relation.py b/tests/providers/relation.py index f58fb93d..2ec56c93 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -2,7 +2,7 @@ from typing import Dict, List, Tuple, Type, Set from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJSONBAdapter from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationSyncProvider, IRelationAsyncProvider from rhosocial.activerecord.testsuite.feature.relation.fixtures.models import ( diff --git a/tests/providers/scenarios.py b/tests/providers/scenarios.py index 2d1a04c4..621b6677 100644 --- a/tests/providers/scenarios.py +++ b/tests/providers/scenarios.py @@ -4,7 +4,7 @@ import os from dataclasses import replace from typing import Dict, Any, Tuple, Type -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.testsuite.core.pool import pooled_database_name diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py index 4d13f625..3fdec40c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py @@ -7,7 +7,8 @@ import os from typing import Dict, Any, Tuple, Type # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py index 957e192f..bfb5145c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py @@ -690,11 +690,11 @@ def test_min_version_metadata(self): assert PostgresStorageParameter.AUTOVACUUM_PARALLEL_WORKERS.min_version == (16, 0, 0) assert PostgresStorageParameter.FILLFACTOR.min_version is None - def test_enum_exported_from_package_root(self): - from rhosocial.activerecord.backend.impl import postgres as pg + def test_enum_exported_from_owning_package(self): + from rhosocial.activerecord.backend.impl.postgres import storage_parameters as pkg - assert pg.PostgresStorageParameter is PostgresStorageParameter - assert "PostgresStorageParameter" in pg.__all__ + assert pkg.PostgresStorageParameter is PostgresStorageParameter + assert "PostgresStorageParameter" in pkg.__all__ def test_expressions_render_enum_members(self): """Enum members and their string values render identically.""" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py index 54e20116..ef0c49cb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py @@ -10,7 +10,7 @@ Priority: config.default_schema > first entry of config.search_path > 'public'. """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.introspection import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py index d724e920..6403ad0c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py @@ -17,7 +17,7 @@ async_ensure_extension_installed, ) from rhosocial.activerecord.backend.impl.postgres.adapters.pgvector import PostgresVectorAdapter -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector from rhosocial.activerecord.backend.errors import DatabaseError from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py index be457157..49929f50 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py @@ -13,7 +13,7 @@ from rhosocial.activerecord.backend.impl.postgres.adapters.bit_string import ( PostgresBitStringAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.bit_string import PostgresBitString +from rhosocial.activerecord.backend.impl.postgres.type_values.bit_string import PostgresBitString BITSTRING_TABLE = "test_bitstring_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py index 704272f6..138c63cb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py @@ -8,7 +8,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.bit_string import PostgresBitString +from rhosocial.activerecord.backend.impl.postgres.type_values.bit_string import PostgresBitString from rhosocial.activerecord.backend.impl.postgres.adapters.bit_string import PostgresBitStringAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py index 335b57e9..e1082eaa 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py @@ -14,11 +14,9 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres import ( - AsyncPostgresBackend, - PostgresBackend, - PostgresConnectionConfig, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig class FakeConnection: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py index 9b285096..28b909bb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py @@ -22,7 +22,7 @@ PartitionClause, PartitionStrategy, ) -from rhosocial.activerecord.backend.impl.dummy import DummyDialect +from rhosocial.activerecord.backend.impl.dummy.backend import DummyDialect from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PartitionValue, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py index 52842575..08f33de7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py @@ -12,10 +12,10 @@ import pytest_asyncio from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresEnumAdapter -from rhosocial.activerecord.backend.impl.postgres.types.enum import ( +from rhosocial.activerecord.backend.impl.postgres.enum_type_manager import ( EnumTypeManager, - PostgresEnumType, ) +from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType ENUM_TYPE = "test_status_enum" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py index 34acd9a0..cdf0c31d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py @@ -11,8 +11,8 @@ from typing import Tuple # noqa: F401 from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types import PostgresEnumType +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresEnumAdapter from rhosocial.activerecord.backend.impl.postgres.protocols import PostgresEnumTypeSupport # noqa: F401 from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py index 012da4ce..44a7a457 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py @@ -8,7 +8,7 @@ """ import pytest # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.types.geometric import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.geometric import ( Point, Line, LineSegment, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py index 0683dc26..fbe3c3f5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore from rhosocial.activerecord.backend.impl.postgres.adapters.hstore import PostgresHstoreAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py index 785db19b..94551e3a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py @@ -10,7 +10,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.json import PostgresJsonPath +from rhosocial.activerecord.backend.impl.postgres.type_values.json import PostgresJsonPath from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJsonPathAdapter from rhosocial.activerecord.backend.impl.postgres.functions.json import ( json_path_root, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py index f01a46d8..7311d209 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py @@ -12,7 +12,7 @@ import pytest_asyncio from rhosocial.activerecord.backend.impl.postgres.adapters.pg_lsn import PostgresLsnAdapter -from rhosocial.activerecord.backend.impl.postgres.types.pg_lsn import PostgresLsn +from rhosocial.activerecord.backend.impl.postgres.type_values.pg_lsn import PostgresLsn LSN_TABLE = "test_lsn_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py index ab404113..9865183d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.pg_lsn import PostgresLsn +from rhosocial.activerecord.backend.impl.postgres.type_values.pg_lsn import PostgresLsn from rhosocial.activerecord.backend.impl.postgres.adapters.pg_lsn import PostgresLsnAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py index 222539c7..f32ab682 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.network_address import PostgresMacaddr8 +from rhosocial.activerecord.backend.impl.postgres.type_values.network_address import PostgresMacaddr8 from rhosocial.activerecord.backend.impl.postgres.adapters.network_address import PostgresMacaddr8Adapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py index c4ae17bb..f41e6286 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py @@ -10,7 +10,7 @@ import pytest from rhosocial.activerecord.backend.errors import DatabaseError -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresAlterMaterializedViewExpression, PostgresChangeMaterializedViewOwnerAction, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py index 392e4a2c..0092b8b5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py @@ -15,7 +15,7 @@ from rhosocial.activerecord.backend.impl.postgres.adapters.monetary import ( PostgresMoneyAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.monetary import PostgresMoney +from rhosocial.activerecord.backend.impl.postgres.type_values.monetary import PostgresMoney MONEY_TABLE = "test_money_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py index 9cdea68a..be49b29a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py @@ -8,7 +8,7 @@ import pytest from decimal import Decimal -from rhosocial.activerecord.backend.impl.postgres.types.monetary import PostgresMoney +from rhosocial.activerecord.backend.impl.postgres.type_values.monetary import PostgresMoney from rhosocial.activerecord.backend.impl.postgres.adapters.monetary import PostgresMoneyAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py index e76a9555..553888f9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py @@ -18,7 +18,7 @@ PostgresMacaddrAdapter, PostgresNetworkAddressAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.network_address import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.network_address import ( PostgresMacaddr, PostgresMacaddr8, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py index 6bc58336..02d5ef2b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py @@ -16,7 +16,7 @@ PostgresOidAdapter, PostgresTidAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.object_identifier import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.object_identifier import ( OID, RegClass, RegType, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py index 47dc6396..f1bc7d10 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py @@ -9,7 +9,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.object_identifier import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.object_identifier import ( OID, RegClass, RegType, RegProc, RegProcedure, RegOper, RegOperator, RegConfig, RegDictionary, RegNamespace, RegRole, RegCollation, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py index 5b781c50..3e9cb636 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py @@ -16,16 +16,16 @@ TableExpression, ) from rhosocial.activerecord.backend.expression.serialization import deserialize, serialize -from rhosocial.activerecord.backend.impl.postgres import ( - AsyncPostgresBackend, - PostgresBackend, +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCopyFromExpression, PostgresCopyLogVerbosity, PostgresCopyOnError, PostgresCopyToExpression, - PostgresDialect, PostgresRepackExpression, ) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.mixins import ( PostgresCopyMixin, PostgresRepackMixin, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py index ab599fb5..80ff0cc9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py @@ -4,7 +4,7 @@ import pytest from rhosocial.activerecord.backend.expression import Column, Literal, QueryExpression, TableExpression -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCopyFromExpression, PostgresCopyToExpression, PostgresRepackExpression, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py index 65b970e8..4af6d2e0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector from rhosocial.activerecord.backend.impl.postgres.adapters.pgvector import PostgresVectorAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py index d464e4dd..00439023 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py @@ -18,7 +18,7 @@ from rhosocial.activerecord.backend.expression import Column, Literal from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl.policy import ( AlterPolicyMode, PostgresAlterPolicyExpression, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py index a40e59bf..eb14ce0e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry from rhosocial.activerecord.backend.impl.postgres.adapters.postgis import PostgresPostGISAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py index 8cd07bf0..b0bff327 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py @@ -5,7 +5,7 @@ from rhosocial.activerecord.backend.expression import ColumnCommentClause, ColumnDefinition from rhosocial.activerecord.backend.expression.types import TextType -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresColumnDefinition, PostgresColumnOptions, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py index 8d9f9364..ff4be722 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py @@ -60,7 +60,7 @@ hstore_subscript_get, hstore_subscript_set, ) -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore from rhosocial.activerecord.backend.expression import bases from rhosocial.activerecord.backend.expression.core import FunctionCall from rhosocial.activerecord.backend.expression.operators import BinaryExpression diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py index c406924d..d45cc312 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py @@ -16,7 +16,7 @@ vector_cosine_similarity, vector_literal, ) -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector from rhosocial.activerecord.backend.expression.operators import BinaryArithmeticExpression from rhosocial.activerecord.backend.expression import bases diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py index a890c3e9..6d42347d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py @@ -34,7 +34,7 @@ st_envelope, st_centroid, ) -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry from rhosocial.activerecord.backend.expression import bases from rhosocial.activerecord.backend.expression.core import FunctionCall diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py index e2632a58..cc999f14 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py @@ -25,10 +25,8 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig, -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType from rhosocial.activerecord.base.field_proxy import FieldProxy diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py index af3dc886..7915ca51 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py @@ -31,7 +31,7 @@ range_lower_inf, range_upper_inf, ) -from rhosocial.activerecord.backend.impl.postgres.types.range import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.type_values.range import PostgresRange class TestRangeContains: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py index b013d905..79a4464d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py @@ -12,7 +12,7 @@ from datetime import date, datetime # noqa: F401 from decimal import Decimal # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.types.range import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.range import ( PostgresRange, PostgresMultirange, _find_bound_separator, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py index 7182072e..e28bc9e2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py @@ -12,7 +12,7 @@ from datetime import date, datetime # noqa: F401 from decimal import Decimal # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.types.range import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.range import ( PostgresRange, # noqa: F401 PostgresMultirange, # noqa: F401 ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py index 36bbd0c5..9bd78fef 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py @@ -10,7 +10,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.text_search import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.text_search import ( TsVectorLexeme, PostgresTsVector, TsQueryLexeme, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py index 84bef0e1..f232a2c7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py @@ -11,8 +11,8 @@ import warnings from rhosocial.activerecord.backend.expression.core import Column, Literal, CastExpression -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( MONEY, NUMERIC, FLOAT8, INTEGER, VARCHAR, TEXT, JSONB, TIMESTAMP, TIMESTAMPTZ, BOOLEAN, BIGINT, ) @@ -239,7 +239,7 @@ def test_boolean_constants(self): """Test boolean type constants.""" assert BOOLEAN == "boolean" # BOOL is an alias - from rhosocial.activerecord.backend.impl.postgres.types.constants import BOOL + from rhosocial.activerecord.backend.impl.postgres.type_values.constants import BOOL assert BOOL == "bool" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py index da3160ac..37d81770 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py @@ -19,7 +19,7 @@ xpath_exists, xpath_query, ) -from rhosocial.activerecord.backend.impl.postgres.types.xml import PostgresXML +from rhosocial.activerecord.backend.impl.postgres.type_values.xml import PostgresXML XML_TABLE = "test_xml_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py index 776e0b33..66511538 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py @@ -11,7 +11,7 @@ from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.expression import XMLParseExpression, core from rhosocial.activerecord.backend.expression.functions.xml import xmlparse -from rhosocial.activerecord.backend.impl.postgres.types.xml import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.xml import ( PostgresXML, ) from rhosocial.activerecord.backend.impl.postgres.functions.xml import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py index 639e509e..92105126 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py @@ -19,7 +19,7 @@ from rhosocial.activerecord.backend.impl.postgres.adapters.range import PostgresRangeAdapter, PostgresMultirangeAdapter from rhosocial.activerecord.backend.impl.postgres.adapters.base import PostgresListAdapter, PostgresEnumAdapter from rhosocial.activerecord.backend.impl.postgres.adapters.pgvector import PostgresVectorAdapter -from rhosocial.activerecord.backend.impl.postgres.types.range import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.range import ( PostgresRange as PG_RANGE, PostgresMultirange as PG_MULTIRANGE, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py index d35c6621..c494d6dd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py @@ -14,7 +14,7 @@ ) from rhosocial.activerecord.backend.expression import RawSQLExpression from rhosocial.activerecord.backend.expression.statements import ExplainOptions -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.explain import ( PostgresExplainResult, PostgresExplainPlanLine, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py index a9e8b113..8e1db137 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py @@ -1,6 +1,7 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend, PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.transaction import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py index f4110306..4c685b42 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py @@ -428,7 +428,8 @@ def fake_handle(args, provider, **kwargs): 'resolve_connection_config_from_args', return_value='config', ), patch( - 'rhosocial.activerecord.backend.impl.postgres.AsyncPostgresBackend', + 'rhosocial.activerecord.backend.impl.postgres.backend.async_backend.' + 'AsyncPostgresBackend', return_value=async_backend, ) as backend_cls, patch( 'rhosocial.activerecord.backend.named_expression.cli_procedure_graph.' diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py index 6f757709..33580f3b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py @@ -23,7 +23,8 @@ import time import logging -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend logger = logging.getLogger(__name__) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py new file mode 100644 index 00000000..1670cb17 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py @@ -0,0 +1,134 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py +"""Guardrail: every expression must live under the backend's expression namespace. + +``BaseExpression`` subclasses render SQL, so they belong in +``rhosocial.activerecord.backend.impl.postgres.expression``. Data types are no +exception: a value-level ``DataType`` belongs in ``expression/types.py`` and a +DDL-level type in ``expression/ddl/type.py``. + +This test failed repeatedly in the past (SHOW expressions sat in ``show/``, and +``PostgresEnumType`` sat in ``types/``), so the invariant is asserted +mechanically rather than left to review. +""" + +import importlib +import inspect +import pkgutil + +import pytest + +from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.impl import postgres + +EXPRESSION_NS = postgres.__name__ + ".expression" +BACKEND_NS = postgres.__name__ + "." + + +def _iter_subclasses(cls): + """Yield every transitive subclass, breadth-first, cycle-safe.""" + seen = {cls} + queue = [cls] + while queue: + current = queue.pop(0) + for sub in current.__subclasses__(): + if sub not in seen: + seen.add(sub) + queue.append(sub) + yield sub + + +@pytest.fixture(scope="module") +def all_expression_classes(): + """Import every module in the postgres backend, then collect expressions. + + Imports are required so that ``__subclasses__()`` is populated; classes + that are never imported cannot be discovered by introspection. + """ + failures = [] + for mod in pkgutil.walk_packages(postgres.__path__, prefix=postgres.__name__ + "."): + name = mod.name + if ".examples" in name or ".cli" in name: + continue # example/CLI modules have import side effects + try: + importlib.import_module(name) + except Exception as exc: # pragma: no cover - diagnostic path + failures.append(f"{name}: {type(exc).__name__}: {exc}") + assert not failures, "could not import backend modules:\n" + "\n".join(failures) + return sorted(_iter_subclasses(BaseExpression), key=lambda c: (c.__module__, c.__qualname__)) + + +def test_backend_modules_import_cleanly(): + """Sanity check: every non-example backend module is importable.""" + failures = [] + for mod in pkgutil.walk_packages(postgres.__path__, prefix=postgres.__name__ + "."): + if ".examples" in mod.name or ".cli" in mod.name: + continue + try: + importlib.import_module(mod.name) + except Exception as exc: # pragma: no cover - diagnostic path + failures.append(f"{mod.name}: {type(exc).__name__}: {exc}") + assert not failures, "unimportable modules:\n" + "\n".join(failures) + + +def test_no_expression_defined_outside_expression_namespace(all_expression_classes): + """The core invariant: backend expressions are only defined under ``expression``. + + Only classes defined *inside this backend's* package are constrained. Core + expressions live in ``rhosocial.activerecord.backend.expression`` and + expressions belonging to other backends are irrelevant here, so both are + excluded by requiring the module to start with the backend namespace. + """ + offenders = [ + f"{cls.__module__}.{cls.__qualname__}" + for cls in all_expression_classes + if cls.__module__.startswith(BACKEND_NS) + and not cls.__module__.startswith(EXPRESSION_NS) + ] + assert not offenders, ( + "BaseExpression subclasses defined in this backend but outside " + f"{EXPRESSION_NS}:\n " + "\n ".join(sorted(offenders)) + ) + + +def test_type_values_package_holds_no_expressions(all_expression_classes): + """``type_values`` is the domain value-type layer, so it holds no expressions.""" + type_values_ns = postgres.__name__ + ".type_values" + offenders = [ + f"{cls.__module__}.{cls.__qualname__}" + for cls in all_expression_classes + if cls.__module__.startswith(type_values_ns) + ] + assert not offenders, ( + f"expressions leaked into {type_values_ns}:\n " + "\n ".join(sorted(offenders)) + ) + + +def test_every_expression_class_is_inspectable(all_expression_classes): + """Sanity check: the collected set is non-trivial, so the test cannot pass vacuously.""" + assert len(all_expression_classes) > 50, ( + f"only found {len(all_expression_classes)} expression classes; " + "module discovery is probably broken" + ) + + +def test_postgres_enum_type_lives_in_expression_namespace(): + """Regression test for the specific violation this branch fixes.""" + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType + + assert issubclass(PostgresEnumType, BaseExpression) + assert PostgresEnumType.__module__ == f"{EXPRESSION_NS}.enum_" + assert inspect.getmodule(PostgresEnumType) is not None + + +def test_old_types_package_is_gone(): + """The renamed package must not linger under its old name. + + ``types`` is a heavily overloaded name in this series, so a leftover + ``postgres/types`` package would silently re-open the ambiguity that the + ``type_values`` rename was meant to remove. + """ + import importlib.util + + assert importlib.util.find_spec(f"{postgres.__name__}.types") is None, ( + f"{postgres.__name__}.types still exists; it should have been renamed to type_values" + ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py index cc4480b7..7e34d2ec 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py @@ -33,7 +33,7 @@ TableConstraintType, ) from rhosocial.activerecord.backend.expression.types import BigIntType, TextType, TimestampType -from rhosocial.activerecord.backend.impl.postgres import PostgresExplainResult +from rhosocial.activerecord.backend.impl.postgres.explain import PostgresExplainResult from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCreatePartitionExpression, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py index 9f16564c..eb689c87 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py @@ -23,7 +23,7 @@ extract, ) from rhosocial.activerecord.backend.expression.query_parts import OrderByClause -from rhosocial.activerecord.backend.impl.postgres import PostgresExplainResult +from rhosocial.activerecord.backend.impl.postgres.explain import PostgresExplainResult from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py new file mode 100644 index 00000000..106db47e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py @@ -0,0 +1,133 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py +"""Regression tests: SSL/TLS config must actually reach ``psycopg.connect()``. + +The config exposes libpq keyword names (``sslmode``/``sslcert``/``sslkey``/ +``sslrootcert``/``sslcrl``/``sslcompression``). These tests pin that the driver +call receives exactly those names, so the generic ``ssl_ca``/``ssl_cert``/ +``ssl_key``/``ssl_mode`` naming can never creep back in and silently drop the +settings. +""" + +from unittest.mock import patch + +import pytest + +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig + + +@pytest.fixture +def ssl_config(): + return PostgresConnectionConfig( + host="db.example.com", + port=5432, + database="test_db", + username="tester", + password="secret", + sslmode="verify-full", + sslcert="/certs/client.crt", + sslkey="/certs/client.key", + sslrootcert="/certs/ca.crt", + sslcrl="/certs/ca.crl", + sslcompression=True, + ) + + +def test_config_exposes_libpq_ssl_field_names(ssl_config): + """The generic SSL mixin names must not be present on the postgres config. + + ``ConnectionConfig`` does not inherit ``SSLMixin``, so a config carrying + ``ssl_ca``/``ssl_mode`` would make the driver look for attributes that do + not exist and quietly forward nothing. + """ + for missing in ("ssl_ca", "ssl_cert", "ssl_key", "ssl_mode"): + assert not hasattr(ssl_config, missing), ( + f"PostgresConnectionConfig must not expose generic SSL field {missing!r}" + ) + + +def test_get_ssl_connection_params_returns_all_set_values(ssl_config): + assert ssl_config.get_ssl_connection_params() == { + "sslmode": "verify-full", + "sslcert": "/certs/client.crt", + "sslkey": "/certs/client.key", + "sslrootcert": "/certs/ca.crt", + "sslcrl": "/certs/ca.crl", + "sslcompression": True, + } + + +def test_get_ssl_connection_params_omits_unset_values(): + config = PostgresConnectionConfig(host="localhost", database="test_db", sslmode="require") + assert config.get_ssl_connection_params() == {"sslmode": "require"} + + +def test_get_ssl_connection_params_empty_by_default(): + config = PostgresConnectionConfig(host="localhost", database="test_db") + assert config.get_ssl_connection_params() == {} + + +def test_sync_connect_forwards_ssl_params(ssl_config): + """Sync connect() must hand the libpq SSL keywords to psycopg verbatim.""" + backend = PostgresBackend(connection_config=ssl_config) + with patch("rhosocial.activerecord.backend.impl.postgres.backend.backend.psycopg.connect") as connect: + backend.connect() + + params = connect.call_args.kwargs + for key, value in ssl_config.get_ssl_connection_params().items(): + assert params[key] == value + assert params["host"] == "db.example.com" + assert params["dbname"] == "test_db" + + +@pytest.mark.asyncio +async def test_async_connect_forwards_ssl_params(ssl_config): + """Async connect() must hand the same libpq SSL keywords to psycopg.""" + backend = AsyncPostgresBackend(connection_config=ssl_config) + connect = patch( + "rhosocial.activerecord.backend.impl.postgres.backend.async_backend.AsyncConnection.connect" + ) + with connect as mock_connect: + mock_connect.return_value.set_autocommit = _noop_coroutine + await backend.connect() + + params = mock_connect.call_args.kwargs + for key, value in ssl_config.get_ssl_connection_params().items(): + assert params[key] == value + assert params["host"] == "db.example.com" + assert params["dbname"] == "test_db" + + +async def _noop_coroutine(*args, **kwargs): + return None + + +def test_sync_connect_sends_no_ssl_keys_when_unset(): + """No stray ``ssl*`` key may be emitted when the user configured nothing. + + psycopg drops ``None`` kwargs, but an explicit ``sslmode=None`` would hide + the difference between "unset" and "dropped by accident"; keeping the key + out entirely is what lets libpq/PGSERVICE defaults apply. + """ + config = PostgresConnectionConfig(host="localhost", port=5432, database="test_db") + backend = PostgresBackend(connection_config=config) + with patch("rhosocial.activerecord.backend.impl.postgres.backend.backend.psycopg.connect") as connect: + backend.connect() + + assert not [key for key in connect.call_args.kwargs if key.startswith("ssl")] + + +def test_sslmode_disable_is_distinguishable_from_require(): + """A stored sslmode must not be normalised away. + + ``disable`` and ``require`` are the two values that visibly change the + resulting session, so a config that collapses them is a broken config. + """ + def build(mode): + return PostgresConnectionConfig( + host="localhost", database="test_db", sslmode=mode + ).get_ssl_connection_params() + + assert build("disable") == {"sslmode": "disable"} + assert build("require") == {"sslmode": "require"} diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py index 87b9d09a..51adb382 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py @@ -2,7 +2,8 @@ import pytest import pytest_asyncio # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend class TestPostgresVersion: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py index d3b9c99b..3de4de2d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py @@ -67,7 +67,7 @@ def test_supports_data_type_postgres_enum_exists(dialect): def test_postgres_enum_expression_renders_via_format_enum_type_expression(dialect): """PostgresEnumType (BaseExpression) renders via format_enum_type_expression.""" - from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType enum_ref = PostgresEnumType( dialect=dialect, @@ -80,7 +80,7 @@ def test_postgres_enum_expression_renders_via_format_enum_type_expression(dialec def test_postgres_enum_expression_with_schema_renders_via_dialect(dialect): - from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType enum_ref = PostgresEnumType( dialect=dialect, @@ -353,7 +353,7 @@ def test_time_precision_validation(dialect): def test_postgres_enum_type_is_not_data_type(): """PostgresEnumType extends BaseExpression, not DataType.""" from rhosocial.activerecord.backend.expression.bases import BaseExpression - from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType assert not issubclass(PostgresEnumType, DataType) assert issubclass(PostgresEnumType, BaseExpression) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py index 7935cb2a..91197815 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py @@ -14,7 +14,8 @@ import pytest_asyncio import yaml -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.connection.pool import ( PoolConfig, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py b/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py index 93b67fbd..20d03525 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py @@ -6,7 +6,8 @@ import pytest from rhosocial.activerecord.backend.expression import Column, Literal -from rhosocial.activerecord.backend.impl.postgres import PostgresCollation, PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.collation import PostgresCollation @pytest.fixture