From beb040adf35533208026085a64d1fbdb8e4d6163 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 16 Aug 2026 20:50:28 +0800 Subject: [PATCH 01/22] chore: bump version to 1.0.0.dev17 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 248ee365..e7d07199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rhosocial-activerecord-postgres" -version = "1.0.0.dev16" +version = "1.0.0.dev17" description = "postgres backend implementation for rhosocial-activerecord, providing a robust and optimized postgres database support." readme = "README.md" license = { file = "LICENSE" } From 6779f08038dedf9799aa0255195b92608dcc15b1 Mon Sep 17 00:00:00 2001 From: zhao vistart <864657+vistart@users.noreply.github.com> Date: Sat, 29 Aug 2026 01:30:27 +0800 Subject: [PATCH 02/22] feat: parallel testing infrastructure and PostgreSQL backend improvements (#55) * feat: parallel scenario tests with per-worker database pooling Wire the testsuite database pool for postgres: register the pool reset handler, make scenario resolution pool-aware, install core and testsuite from the feature/parallel-testing branches, and run tests with -n 8. * fix(test): load testsuite conftest plugin and guard --scenarios registration Without -p rhosocial.activerecord.testsuite.conftest the pooling hooks never run, so configure_pool_size stays 0 and every xdist worker falls back to the shared scenario database, racing on schema DDL. Load the plugin and guard the backend's own --scenarios option so both conftests coexist. * fix(test): apply pooled database names in backend and connection feature conftests * fix(test): apply pooled database names in worker connection params * fix(test): check for simultaneous double-hold instead of id uniqueness * fix(test): use a non-empty LSN range for pg_get_wal_records_info * feat: CLI async unification, --ssl param; postgres backend fixes * feat(postgres): add named expression/procedure example modules - Add named_expressions order_expressions (import-safe, prepare_orders_demo helper) - Add named_procedures order_workflow (OrderProcessingProcedure, max_concurrency=1 for psycopg single-connection concurrency limit) - Update bash scripts to reference real module paths instead of placeholders and full module paths for --list (package path returns no entries) * test(expression): add serialization round-trip coverage for PostgreSQL expressions * fix(dialect): translate is_auto_increment to GENERATED BY DEFAULT AS IDENTITY ColumnConstraint(is_auto_increment=True) on PRIMARY_KEY was silently ignored, producing INTEGER PRIMARY KEY without auto-increment. PostgreSQL requires explicit GENERATED BY DEFAULT AS IDENTITY for auto-increment columns. The existing dialect_options['identity'] path is unaffected. * test: add is_auto_increment/DDL regression tests Dialect-level tests (no DB needed) verifying: 1. is_auto_increment=True produces correct auto-increment DDL keyword 2. All standard types (Integer/Text/Boolean/VarChar/Timestamp) compile 3. BooleanType + DEFAULT True is accepted 4. TimestampType maps to correct backend-specific type Prevents recurrence of bugs found in rbac cross-backend testing. * fix(config): declare missing autocommit field on PostgresConnectionConfig The backend read config.autocommit (requires_manual_commit and the auto-commit handlers in sync/async backends) to decide whether an explicit COMMIT is needed outside managed transactions, but the field did not exist on the dataclass, so passing autocommit=True raised a TypeError and the option was effectively dead. All other backends (mysql, sqlserver, clickhouse, snowflake, firebird) already declare it. Replace the defensive getattr() fallbacks with direct attribute access and add unit tests covering the default value, explicit assignment, conninfo exclusion, and requires_manual_commit() for both backends. * test: cover column qualifier resolution under JOINs Regression tests for qualifier generation vs FROM context: plain field accessors, mixed/partial aliases, prebuilt-predicate reuse across contexts, explicit-alias joins, and an xfail sentinel documenting the accepted dynamic __table_name__ drift limitation. Companion to the python-activerecord fix that stops forcing table-name aliases on JOIN ranges. * test: cover dynamic table/schema names as supported usage Replace the R4 xfail sentinel with positive tests for the documented contract: after changing __table_name__ / __schema_name__, expressions built through field proxies pick up the new identity, and full DQL stays consistent across FROM / WHERE / SELECT. Seed an archive table and a second tenant-style schema so both switch directions assert real data. * feat(test): provide cross-schema fixtures for testsuite Implements the optional setup_schema_fixtures capability: provisions ar_crm / ar_shop schemas with customers and orders tables (sync + async variants; DDL runs through a short-lived sync connection for async), tracks created schemas, and drops them in cleanup_after_test. Declares supports_schema() == True. * feat(dialect): declare supports_schema capability and schema DDL tests * fix(protocols): declare supports_schema and supports_schema_authorization in PostgresSchemaSupport * test(provider): provision mixed default/non-default schema fixtures * test(adapters): offline adapter round-trip coverage for all types * docs: record stateflow cross-backend test issues (JSON/bool/RETURNING) * fix(hstore): pass raw % operator to format_binary_operator to avoid double escaping hstore_to_array_operator passed the already-escaped '%%' operator to format_binary_operator, which escapes '%' again to '%%%%' (psycopg folds to '%%' instead of the expected '%'). Pass the raw '%' so format_binary_operator performs the single %->%% escape as the sole collection point. * fix(adapters): parse array literals on read for list fields in TEXT columns PostgresListAdapter now parses JSON and PostgreSQL array literals via psycopg's array loader when reading list fields stored in TEXT columns, fixing the cross-backend round-trip exposed by stateflow tests. * test: add format_binary_operator % escaping and hstore operator tests * ci: update testsuite branch to release/v1.0.0.dev17 * ci: update rhosocial-activerecord branch to release/v1.0.0.dev30 * ci: update testsuite branch to release/v1.0.0.dev17 * feat(postgres): add pgvector vector_search function and expression type support - Add vector_search.py function module with pgvector search capabilities - Extend expression types with pgvector-related serialization - Add data_type_formatting mixin improvements - Add vector search and example compile test coverage * test: add targeted coverage for low-coverage patch areas - extend PostgresListAdapter string parsing tests (JSON + PG array literal) - add HALFVEC/SPARSEVEC DDL format/parse and eq/hash coverage - cover vector_search edge cases (ip similarity, index options, invalid metric) - add _get_default_schema config-aware resolution tests for both introspection stacks - cover backend autocommit handler branches (sync + async, True/False) - add CLI --ssl mapping and --async flag tests - add CLI blackbox tests for introspect/status --async fix(cli): correct async introspect method names (remove _async suffix) The _handle_introspect_async path called nonexistent list_tables_async, list_views_async etc. Async introspector methods use the same names as sync (no _async suffix). Fixes broken async introspect branch. * fix(test): tolerate empty table list in introspect blackbox tests CI runs against a fresh test_db where 'introspect tables' returns [], so assert json.loads(out) (truthy) fails. Assert the parsed JSON is a list instead, which holds whether or not tables exist. * chore: add changelog fragment for PR #55 * docs: remove non-standard changelog fragment The file name stateflow-cross-backend-issues.fixed.md does not follow the ..md convention. Its content was already merged into the relevant fix commit. --- .github/workflows/test.yml | 24 +- changelog.d/55.added.md | 1 + pyproject.toml | 2 +- .../backend/impl/postgres/README.md | 2 +- .../backend/impl/postgres/__init__.py | 4 + .../backend/impl/postgres/__main__.py | 8 +- .../backend/impl/postgres/adapters/base.py | 44 +- .../impl/postgres/backend/async_backend.py | 4 +- .../backend/impl/postgres/backend/base.py | 2 +- .../backend/impl/postgres/backend/sync.py | 4 +- .../backend/impl/postgres/cli/connection.py | 21 +- .../backend/impl/postgres/cli/introspect.py | 18 +- .../backend/impl/postgres/cli/query.py | 2 +- .../backend/impl/postgres/cli/status.py | 2 +- .../backend/impl/postgres/config.py | 5 + .../examples/cli/bash/cli_commands.sh | 12 +- .../examples/cli/bash/named_expression.sh | 2 +- .../examples/cli/bash/named_procedure.sh | 2 +- .../postgres/examples/ddl/create_table.py | 24 +- .../postgres/examples/extensions/pgvector.py | 149 ++--- .../examples/named_expressions/__init__.py | 2 + .../named_expressions/order_expressions.py | 189 ++++++ .../examples/named_procedures/__init__.py | 2 + .../named_procedures/order_workflow.py | 174 ++++++ .../impl/postgres/expression/__init__.py | 4 + .../backend/impl/postgres/expression/types.py | 50 ++ .../impl/postgres/functions/__init__.py | 11 + .../backend/impl/postgres/functions/hstore.py | 2 +- .../impl/postgres/functions/pgvector.py | 56 +- .../impl/postgres/functions/vector_search.py | 216 +++++++ .../postgres/introspection/introspector.py | 17 +- .../postgres/mixins/extensions/pgvector.py | 13 +- .../impl/postgres/mixins/introspection.py | 19 +- .../backend/impl/postgres/mixins/schema.py | 7 + .../backend/impl/postgres/mixins/table.py | 4 + .../mixins/types/data_type_formatting.py | 31 +- .../postgres/protocols/extensions/pgvector.py | 2 + .../backend/impl/postgres/protocols/schema.py | 4 + tests/conftest.py | 18 +- tests/providers/basic.py | 5 + tests/providers/pooling.py | 132 ++++ tests/providers/query.py | 197 +++++- tests/providers/registry.py | 4 + tests/providers/scenarios.py | 28 +- .../feature/backend/conftest.py | 9 +- .../backend/dialect/test_schema_support.py | 66 ++ .../feature/backend/examples_compile_test.py | 36 ++ .../backend/expression/vector_search_test.py | 198 ++++++ .../test_introspection_default_schema.py | 84 +++ .../test_pg_walinspect_integration.py | 40 +- .../postgres/test_connection_config.py | 154 +++++ .../test_qualified_reference_context.py | 226 +++++++ .../feature/backend/test_adapters.py | 564 +++++++----------- .../backend/test_auto_increment_ddl.py | 83 +++ .../backend/test_cli_connection_args.py | 46 ++ .../backend/test_expression_roundtrip_all.py | 80 +++ .../backend/test_postgres_dialect_security.py | 62 +- .../types/test_data_type_formatting.py | 56 ++ .../feature/connection/conftest.py | 9 +- .../test_connection_pool_concurrent.py | 58 +- .../feature/backend/cli/test_cli_blackbox.py | 103 ++++ 61 files changed, 2824 insertions(+), 569 deletions(-) create mode 100644 changelog.d/55.added.md create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/__init__.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/__init__.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py create mode 100644 tests/providers/pooling.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_schema_support.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/examples_compile_test.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/expression/vector_search_test.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli_connection_args.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py create mode 100644 tests/rhosocial/activerecord_test/feature/backend/cli/test_cli_blackbox.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce44767e..b1f9792b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,8 +68,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install rhosocial-activerecord==1.0.0.dev29 - pip install rhosocial-activerecord-testsuite==1.0.0.dev16 + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" - name: Create postgres scenario config run: | @@ -279,8 +279,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install rhosocial-activerecord==1.0.0.dev29 - pip install rhosocial-activerecord-testsuite==1.0.0.dev16 + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" - name: Create postgres scenario config run: | @@ -322,7 +322,7 @@ jobs: - name: Run tests run: | export PYTHONPATH=src:tests - python -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup + python -m pytest tests/ --tb=short -n 8 --dist=loadgroup test: name: Python ${{ matrix.python-version }} / postgres ${{ matrix.postgres-version }} @@ -391,8 +391,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install rhosocial-activerecord==1.0.0.dev29 - pip install rhosocial-activerecord-testsuite==1.0.0.dev16 + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" - name: Create postgres scenario config run: | @@ -435,14 +435,14 @@ jobs: if: matrix.postgres-version != '18' run: | export PYTHONPATH=src:tests - python -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup + python -m pytest tests/ --tb=short -n 8 --dist=loadgroup - name: Run tests with coverage (only for postgres 18) if: matrix.postgres-version == '18' run: | export PYTHONPATH=src:tests pip install codecov - python -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup --cov=src --cov-report=xml + python -m pytest tests/ --tb=short -n 8 --dist=loadgroup --cov=src --cov-report=xml - name: Upload coverage to Codecov if: matrix.postgres-version == '18' @@ -490,8 +490,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install rhosocial-activerecord==1.0.0.dev29 - pip install rhosocial-activerecord-testsuite==1.0.0.dev16 + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" - name: Create postgres scenario config run: | @@ -522,4 +522,4 @@ jobs: - name: Run tests run: | export PYTHONPATH=src:tests - python -m pytest tests/ --tb=short --scenario-parallel -n 2 --dist=loadgroup + python -m pytest tests/ --tb=short -n 8 --dist=loadgroup diff --git a/changelog.d/55.added.md b/changelog.d/55.added.md new file mode 100644 index 00000000..851351bc --- /dev/null +++ b/changelog.d/55.added.md @@ -0,0 +1 @@ +Add parallel testing infrastructure and PostgreSQL backend improvements \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e7d07199..ea713912 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ rhosocial = ["**/*.pyi", "**/*.py"] [tool.pytest.ini_options] minversion = "7.0" -addopts = "-ra -q" +addopts = "-ra -q -p rhosocial.activerecord.testsuite.conftest" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" testpaths = [ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/README.md b/src/rhosocial/activerecord/backend/impl/postgres/README.md index 7342669b..356af4d1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/README.md +++ b/src/rhosocial/activerecord/backend/impl/postgres/README.md @@ -30,7 +30,7 @@ python -m rhosocial.activerecord.backend.impl.postgres [OPTIONAL_FLAGS] [YOUR_SQ | `--database` | `POSTGRES_DATABASE` env var or _None_ | Database name to connect to. | | `--user` | `POSTGRES_USER` env var or `postgres` | Database user. | | `--password` | `POSTGRES_PASSWORD` env var or _empty string_ | Database password. | -| `--use-async` | _False_ | Use the asynchronous backend. | +| `--async` | _False_ | Use the asynchronous backend. | | `--output` | `table` (if rich available) or `json` (fallback) | Output format. Choices are `table` (rich formatted), `json`, `csv`, `tsv`. | | `--log-level` | `INFO` | Set the logging level (e.g., `DEBUG`, `INFO`). | | `--rich-ascii` | _False_ | Use ASCII characters for table borders. | diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py index 9c6f720e..18b53f69 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py @@ -67,6 +67,8 @@ PostgresUUIDType, PostgresVarBitType, PostgresVectorType, + PostgresHalfvecType, + PostgresSparsevecType, PostgresXID8Type, PostgresXIDType, PostgresXMLType, @@ -598,6 +600,8 @@ "PostgresUUIDType", "PostgresVarBitType", "PostgresVectorType", + "PostgresHalfvecType", + "PostgresSparsevecType", "PostgresXID8Type", "PostgresXIDType", "PostgresXMLType", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__main__.py b/src/rhosocial/activerecord/backend/impl/postgres/__main__.py index 1c1761e1..24151fec 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__main__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__main__.py @@ -23,18 +23,18 @@ def _build_parser(): return parser -def parse_args(): +def parse_args(argv=None): """Parse command-line arguments. Provided for test compatibility. In production, main() is used instead. """ parser = _build_parser() - return parser.parse_args() + return parser.parse_args(argv) -def main(): +def main(argv=None): parser = _build_parser() - args = parser.parse_args() + args = parser.parse_args(argv) if args.command is None: cmd_list = ", ".join(f"'{c}'" for c in COMMAND_NAMES[:-1]) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py index e1c93541..6d5c7ab3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py @@ -18,8 +18,12 @@ class PostgresListAdapter(BaseSQLTypeAdapter): """ Adapts Python list to PostgreSQL array types. - This adapter does not perform any conversion - psycopg handles - Python lists natively for PostgreSQL array types. + This adapter passes Python lists through to psycopg for native array + column support. On read, if the database returns a string (e.g. when + a list was stored in a TEXT column via psycopg array serialization), + it parses the value back using psycopg's own array loader — the exact + mirror of the ``ListDumper`` used on write — and falls back to JSON + for the portable ``["a","b"]`` representation. """ def __init__(self): @@ -27,12 +31,46 @@ def __init__(self): self._register_type(list, list) def _do_to_database(self, value: list, target_type: Type, options: Optional[Dict[str, Any]] = None) -> Any: - # psycopg handles list natively for PostgreSQL arrays return value + @staticmethod + def _parse_array_literal(value: str) -> list: + """Parse a PostgreSQL array literal using psycopg's array loader. + + ``psycopg.types.array._load_text`` is the exact reverse of the + ``ListDumper`` that psycopg uses to serialize a Python list on + write, so the round-trip is symmetric and handles quoting, + backslash escaping, ``NULL`` elements and nested arrays correctly. + """ + from psycopg.types.array import _load_text + + class _TextElementLoader: + def load(self, data): + return data.decode("utf-8") + + try: + return _load_text(value.encode("utf-8"), _TextElementLoader()) + except Exception: + raise ValueError(f"Not a PostgreSQL array literal: {value!r}") from None + def _do_from_database(self, value: Any, target_type: Type, options: Optional[Dict[str, Any]] = None) -> list: if isinstance(value, list): return value + if isinstance(value, str): + stripped = value.strip() + # Try JSON first (more portable format) + if stripped.startswith("["): + try: + return json.loads(stripped) + except (json.JSONDecodeError, ValueError): + pass + # Try PostgreSQL array literal + if stripped.startswith("{"): + try: + return self._parse_array_literal(stripped) + except (ValueError, IndexError): + pass + raise TypeError(f"Cannot convert str to list: {value!r}") raise TypeError(f"Cannot convert {type(value).__name__} to list") def to_database_batch( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py index 7aba7eb1..cd99f36d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -644,7 +644,7 @@ async def _handle_auto_commit(self) -> None: # 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 getattr(self.config, "autocommit", False): + if not self.config.autocommit: await self._connection.commit() self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") except Exception as e: @@ -658,7 +658,7 @@ async def _handle_auto_commit_if_needed(self) -> None: PostgreSQL respects the autocommit setting, but we also need to handle explicit commits. """ if not self.in_transaction and self._connection: - if not getattr(self.config, "autocommit", False): + if not self.config.autocommit: await self._connection.commit() self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py index eb4366e2..d14bbe9d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py @@ -82,7 +82,7 @@ def threadsafety(self) -> int: def requires_manual_commit(self) -> bool: """Check if manual commit is required for this database.""" - return not getattr(self.config, "autocommit", False) + return not self.config.autocommit def _register_postgres_adapters(self): """Register PostgreSQL-specific type adapters. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index b23d26fa..e1ee833a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -645,7 +645,7 @@ def _handle_auto_commit(self) -> None: # 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 getattr(self.config, "autocommit", False): + if not self.config.autocommit: self._connection.commit() self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") except Exception as e: @@ -659,7 +659,7 @@ def _handle_auto_commit_if_needed(self) -> None: PostgreSQL respects the autocommit setting, but we also need to handle explicit commits. """ if not self.in_transaction and self._connection: - if not getattr(self.config, "autocommit", False): + if not self.config.autocommit: self._connection.commit() self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py index 06dada96..e3d3518d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py @@ -37,8 +37,15 @@ def add_connection_args(parser): help="Database password (env: POSTGRES_PASSWORD)", ) parser.add_argument( - "--use-async", + "--ssl", + choices=["auto", "require", "verify-ca", "verify-full", "disabled"], + default="auto", + help="SSL mode (env: POSTGRES_SSL, default: auto)", + ) + parser.add_argument( + "--async", action="store_true", + dest="is_async", help="Use asynchronous backend", ) parser.add_argument( @@ -118,12 +125,24 @@ def resolve_connection_config_from_args(args): return resolver.resolve({}) # Fallback to explicit connection parameters + # SSL parameter mapping (simplified for CLI unification) + ssl_param = getattr(args, "ssl", None) + sslmode_map = { + "auto": "prefer", + "require": "require", + "verify-ca": "verify-ca", + "verify-full": "verify-full", + "disabled": "disable", + } + sslmode = sslmode_map.get(ssl_param, "prefer") + return PostgresConnectionConfig( host=args.host or "localhost", port=args.port or 5432, database=args.database, username=args.user, password=args.password, + sslmode=sslmode, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py index 09895572..d5d42a91 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py @@ -128,7 +128,7 @@ def handle(args): config = resolve_connection_config_from_args(args) - if args.use_async: + if args.is_async: backend = AsyncPostgresBackend(connection_config=config) asyncio.run(_handle_introspect_async(args, backend, provider)) else: @@ -280,7 +280,7 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider introspector = backend.introspector if args.type == "tables": - tables = await introspector.list_tables_async( + tables = await introspector.list_tables( schema=args.schema, include_system=args.include_system ) @@ -288,7 +288,7 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider provider.display_results(data, title="Tables") elif args.type == "views": - views = await introspector.list_views_async(schema=args.schema) + views = await introspector.list_views(schema=args.schema) data = _serialize_for_output(views) provider.display_results(data, title="Views") @@ -296,7 +296,7 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider if not args.name: print("Error: Table name is required for 'table' introspection", file=sys.stderr) sys.exit(1) - info = await introspector.get_table_info_async(args.name, schema=args.schema) + info = await introspector.get_table_info(args.name, schema=args.schema) if info: provider.display_results(_serialize_for_output(info.columns), title=f"Columns of {args.name}") if info.indexes: @@ -314,7 +314,7 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider if not args.name: print("Error: Table name is required for 'columns' introspection", file=sys.stderr) sys.exit(1) - columns = await introspector.list_columns_async(args.name, schema=args.schema) + columns = await introspector.list_columns(args.name, schema=args.schema) data = _serialize_for_output(columns) provider.display_results(data, title=f"Columns of {args.name}") @@ -322,7 +322,7 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider if not args.name: print("Error: Table name is required for 'indexes' introspection", file=sys.stderr) sys.exit(1) - indexes = await introspector.list_indexes_async(args.name, schema=args.schema) + indexes = await introspector.list_indexes(args.name, schema=args.schema) data = _serialize_for_output(indexes) provider.display_results(data, title=f"Indexes of {args.name}") @@ -330,12 +330,12 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider if not args.name: print("Error: Table name is required for 'foreign-keys' introspection", file=sys.stderr) sys.exit(1) - fks = await introspector.list_foreign_keys_async(args.name, schema=args.schema) + fks = await introspector.list_foreign_keys(args.name, schema=args.schema) data = _serialize_for_output(fks) provider.display_results(data, title=f"Foreign Keys of {args.name}") elif args.type == "triggers": - triggers = await introspector.list_triggers_async( + triggers = await introspector.list_triggers( table_name=args.name, schema=args.schema ) @@ -343,7 +343,7 @@ async def _handle_introspect_async(args, backend: AsyncPostgresBackend, provider provider.display_results(data, title="Triggers") elif args.type == "database": - info = await introspector.get_database_info_async() + info = await introspector.get_database_info() data = _serialize_for_output(info) provider.display_results([data], title="Database Info") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py index 9307efaf..42e4aab3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py @@ -139,7 +139,7 @@ def handle(args): config = resolve_connection_config_from_args(args) kwargs = {"use_ascii": args.rich_ascii} - if args.use_async: + if args.is_async: backend = AsyncPostgresBackend(connection_config=config) asyncio.run(_execute_query_async(sql_source, backend, provider, **kwargs)) else: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py index 0e7573e8..40bf8de0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py @@ -110,7 +110,7 @@ def handle(args): config = resolve_connection_config_from_args(args) - if args.use_async: + if args.is_async: backend = AsyncPostgresBackend(connection_config=config) asyncio.run(_handle_status_async(args, backend, provider)) else: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/config.py b/src/rhosocial/activerecord/backend/impl/postgres/config.py index 251b3306..8d7340b7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/config.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/config.py @@ -47,6 +47,11 @@ class PostgresConnectionMixin: service: Optional[str] = None gssencmode: Optional[str] = None channel_binding: Optional[str] = None + # Transaction behavior. The psycopg connection always runs with driver-level + # autocommit enabled and transactions are managed explicitly by the framework. + # When False (default), the backend issues an explicit COMMIT after statements + # executed outside a managed transaction; when True, those commits are skipped. + autocommit: bool = False # Schema namespace support search_path: Optional[str] = None # Set connection's search_path (passed directly to psycopg) default_schema: Optional[str] = None # Default schema when Model doesn't specify __schema_name__ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/cli_commands.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/cli_commands.sh index e9f2d368..b8351c98 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/cli_commands.sh +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/cli_commands.sh @@ -173,15 +173,15 @@ run_named_expression() { echo "" echo "--- List named expressions (module may not exist) ---" - $PYTHON_CMD named-expression --list rhosocial.activerecord.backend.impl.postgres.examples.named_expressions 2>/dev/null || echo "(No named expression examples found)" + $PYTHON_CMD named-expression --list rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions 2>/dev/null || echo "(No named expression examples found)" echo "" echo "--- List with dialect version ---" - $PYTHON_CMD named-expression --list rhosocial.activerecord.backend.impl.postgres.examples.named_expressions --dialect-version 15.0.0 2>/dev/null || echo "(No named expression examples found)" + $PYTHON_CMD named-expression --list rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions --dialect-version 15.0.0 2>/dev/null || echo "(No named expression examples found)" echo "" echo "--- Describe expression ---" - $PYTHON_CMD named-expression --describe rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.some_expression 2>/dev/null || echo "(No named expression examples found)" + $PYTHON_CMD named-expression --describe rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.get_order 2>/dev/null || echo "(No named expression examples found)" } # Command: named-procedure @@ -193,15 +193,15 @@ run_named_procedure() { echo "" echo "--- List named procedures (module may not exist) ---" - $PYTHON_CMD named-procedure --list rhosocial.activerecord.backend.impl.postgres.examples.named_procedures 2>/dev/null || echo "(No named procedure examples found)" + $PYTHON_CMD named-procedure --list rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.order_workflow 2>/dev/null || echo "(No named procedure examples found)" echo "" echo "--- Describe procedure ---" - $PYTHON_CMD named-procedure --describe rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.SomeProcedure 2>/dev/null || echo "(No named procedure examples found)" + $PYTHON_CMD named-procedure --describe rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.order_workflow.OrderProcessingProcedure 2>/dev/null || echo "(No named procedure examples found)" echo "" echo "--- Dry run procedure ---" - $PYTHON_CMD named-procedure rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.SomeProcedure --dry-run 2>/dev/null || echo "(No named procedure examples found)" + $PYTHON_CMD named-procedure rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.order_workflow.OrderProcessingProcedure --dry-run 2>/dev/null || echo "(No named procedure examples found)" } # Command: named-procedure-graph diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_expression.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_expression.sh index ff246bd9..46eb1b02 100755 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_expression.sh +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_expression.sh @@ -17,5 +17,5 @@ echo "==========================================" echo "PostgreSQL CLI - named-expression command examples" echo "==========================================" -$PYTHON_CMD named-expression --list rhosocial.activerecord.backend.impl.postgres.examples.named_expressions 2>/dev/null || echo "(No named expressions found)" +$PYTHON_CMD named-expression --list rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions 2>/dev/null || echo "(No named expressions found)" $PYTHON_CMD named-connection --list rhosocial.activerecord.backend.impl.postgres.examples.named_connections \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh index fdc97289..c9abc740 100755 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/cli/bash/named_procedure.sh @@ -17,5 +17,5 @@ echo "==========================================" echo "PostgreSQL CLI - named-procedure command examples" echo "==========================================" -$PYTHON_CMD named-procedure --list rhosocial.activerecord.backend.impl.postgres.examples.named_procedures 2>/dev/null || echo "(No named procedures found)" +$PYTHON_CMD named-procedure --list rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.order_workflow 2>/dev/null || echo "(No named procedures found)" $PYTHON_CMD named-connection --list rhosocial.activerecord.backend.impl.postgres.examples.named_connections \ No newline at end of file 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 0cb8edd7..5fd1e5a0 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 @@ -41,51 +41,55 @@ dialect = backend.dialect # Drop if exists for clean setup -drop = DropTableExpression(dialect=dialect, table_name='products', if_exists=True) +drop = DropTableExpression(dialect=dialect, table='products', if_exists=True) sql, params = drop.to_sql() backend.execute(sql, params) # ============================================================ # SECTION: Business Logic (the pattern to learn) # ============================================================ +# Column data types are built with dialect.parse_type() (DataType instance). columns = [ ColumnDefinition( name='id', - data_type='SERIAL', + data_type=dialect.parse_type('SERIAL'), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( name='name', - data_type='VARCHAR(200)', + data_type=dialect.parse_type('VARCHAR(200)'), constraints=[ ColumnConstraint(ColumnConstraintType.NOT_NULL), ], ), ColumnDefinition( name='price', - data_type='NUMERIC(10,2)', + data_type=dialect.parse_type('NUMERIC(10,2)'), constraints=[ ColumnConstraint(ColumnConstraintType.NOT_NULL), ], ), ColumnDefinition( name='category', - data_type='VARCHAR(100)', + data_type=dialect.parse_type('VARCHAR(100)'), ), ColumnDefinition( name='is_active', - data_type='BOOLEAN', + data_type=dialect.parse_type('BOOLEAN'), constraints=[ ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True), ], ), ColumnDefinition( name='created_at', - data_type='TIMESTAMP', + data_type=dialect.parse_type('TIMESTAMP'), constraints=[ - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=current_timestamp(dialect)), + ColumnConstraint( + ColumnConstraintType.DEFAULT, + default_value=current_timestamp(dialect), + ), ], ), ] @@ -99,7 +103,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name='products', + table='products', columns=columns, indexes=indexes, if_not_exists=True, @@ -124,7 +128,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_table = DropTableExpression(dialect=dialect, table_name='products', if_exists=True) +drop_table = DropTableExpression(dialect=dialect, table='products', if_exists=True) sql, params = drop_table.to_sql() backend.execute(sql, params) backend.disconnect() 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 1f43e5d4..e20bffb5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py @@ -6,8 +6,9 @@ 1. Check if vector extension is available 2. CREATE EXTENSION and create table with vector column 3. Insert vector data -4. Execute cosine similarity search -5. Create HNSW index for fast vector search +4. Cosine similarity search (via the vector_search helper) +5. L2 distance search (via the vector_search helper) +6. Create HNSW index (via the create_vector_index helper) """ # ============================================================ @@ -34,7 +35,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="documents", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table="documents", if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -49,11 +50,6 @@ ColumnDefinition, ColumnConstraint, ColumnConstraintType, - CreateIndexExpression, -) -from rhosocial.activerecord.backend.expression.core import Literal, Subquery -from rhosocial.activerecord.backend.expression.operators import ( - BinaryExpression, ) from rhosocial.activerecord.backend.expression.statements.dml import ( InsertExpression, @@ -61,17 +57,13 @@ from rhosocial.activerecord.backend.expression.statements import ( ValuesSource, ) -from rhosocial.activerecord.backend.expression import ( - Column, - QueryExpression, - TableExpression, -) -from rhosocial.activerecord.backend.expression.query_parts import ( - OrderByClause, - LimitOffsetClause, -) +from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.functions import ( + vector_search, + create_vector_index, +) # Check if vector extension is available available = dialect.is_extension_available("vector") @@ -96,30 +88,31 @@ if installed: # Example 1: Create table with vector column + # Column data types are built with dialect.parse_type() (DataType instance). columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=dialect.parse_type("SERIAL"), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( name="content", - data_type="TEXT", + data_type=dialect.parse_type("TEXT"), constraints=[ ColumnConstraint(ColumnConstraintType.NOT_NULL), ], ), ColumnDefinition( name="embedding", - data_type="VECTOR(3)", + data_type=dialect.parse_type("VECTOR(3)"), ), ] create_expr = CreateTableExpression( dialect=dialect, - table_name="documents", + table="documents", columns=columns, if_not_exists=True, ) @@ -129,7 +122,7 @@ backend.execute(sql, params) # Example 2: Insert vector data - # Use Literal with .cast("vector") for type-safe vector insertion + # Use Literal with .cast("vector") for type-safe vector insertion. insert_expr = InsertExpression( dialect=dialect, into="documents", @@ -159,108 +152,66 @@ sql, params = insert_expr.to_sql() print("\n--- INSERT vector data ---") print(f"SQL: {sql}") - print(f"Params: {params}") backend.execute(sql, params) - # Example 3: Cosine similarity search - # Find documents most similar to "kitten" represented as [1.0, 0.55, 0.18] - # pgvector <=> operator is cosine distance; cosine_similarity = 1 - cosine_distance - query_vector_cosine = Literal(dialect, "[1.0, 0.55, 0.18]").cast("vector") - cosine_dist = BinaryExpression( - dialect, "<=>", - Column(dialect, "embedding"), - query_vector_cosine, - ) - - # cosine_similarity = 1 - (embedding <=> query_vector) - # Wrap cosine_dist with Subquery to ensure parentheses for operator precedence: - # without parentheses, PostgreSQL parses "1 - embedding <=> ..." as "(1 - embedding) <=> ..." - # because subtraction (-) has higher precedence than the custom <=> operator. - # Wrap the outer expression with Subquery to add alias since BinaryExpression lacks AliasableMixin. - cosine_sim = Subquery( + # Example 3: Cosine similarity search (query similar to "cat") + opts = ExecutionOptions(stmt_type=StatementType.DQL) + query = vector_search( dialect, - BinaryExpression( - dialect, "-", - Literal(dialect, 1), - Subquery(dialect, cosine_dist), - ), - ).as_("cosine_similarity") - - query = QueryExpression( - dialect=dialect, - select=[Column(dialect, "content"), cosine_sim], - from_=TableExpression(dialect, "documents"), - order_by=OrderByClause(dialect, [cosine_dist]), - limit_offset=LimitOffsetClause(dialect, limit=3), + "documents", + [1.0, 0.55, 0.18], + metric="cosine", + top_k=3, + columns=["content"], + include_similarity=True, ) sql, params = query.to_sql() print("\n--- Cosine similarity search ---") print("Query vector: [1.0, 0.55, 0.18] (similar to 'cat')") print(f"SQL: {sql}") print(f"Params: {params}") - opts = ExecutionOptions(stmt_type=StatementType.DQL) result = backend.execute(sql, params, options=opts) print(f"Results: {result.data}") - # Example 4: L2 distance search - # pgvector <-> operator is L2 (Euclidean) distance - query_vector_l2 = Literal(dialect, "[0.1, 0.2, 0.9]").cast("vector") - l2_dist = BinaryExpression( - dialect, "<->", - Column(dialect, "embedding"), - query_vector_l2, - ) - - # Wrap with Subquery to add alias since BinaryExpression lacks AliasableMixin - l2_dist_aliased = Subquery(dialect, l2_dist).as_("l2_distance") - - # ORDER BY needs the raw expression (without alias wrapper) - l2_dist_order = BinaryExpression( - dialect, "<->", - Column(dialect, "embedding"), - Literal(dialect, "[0.1, 0.2, 0.9]").cast("vector"), - ) - - query = QueryExpression( - dialect=dialect, - select=[Column(dialect, "content"), l2_dist_aliased], - from_=TableExpression(dialect, "documents"), - order_by=OrderByClause(dialect, [l2_dist_order]), - limit_offset=LimitOffsetClause(dialect, limit=3), + # Example 4: L2 distance search (query vector equal to "car" row) + query = vector_search( + dialect, + "documents", + [0.1, 0.2, 0.9], + metric="l2", + top_k=3, + columns=["content"], + include_distance=True, ) sql, params = query.to_sql() print("\n--- L2 distance search ---") print("Query vector: [0.1, 0.2, 0.9] (similar to 'car')") print(f"SQL: {sql}") - print(f"Params: {params}") result = backend.execute(sql, params, options=opts) print(f"Results: {result.data}") # Example 5: Create HNSW index for fast vector search - # HNSW index provides approximate nearest neighbor search - # Use dialect_options={"opclasses": {"embedding": "vector_cosine_ops"}} - # to specify the operator class for the vector column. - create_idx = CreateIndexExpression( - dialect=dialect, - index_name="idx_documents_embedding", - table_name="documents", - columns=["embedding"], - index_type="HNSW", - if_not_exists=True, - dialect_options={"opclasses": {"embedding": "vector_cosine_ops"}}, + # Metric 'cosine' maps to the vector_cosine_ops operator class. + index_expr = create_vector_index( + dialect, + "documents", + metric="cosine", + index_type="hnsw", + m=16, + ef_construction=64, ) - sql, params = create_idx.to_sql() + sql, params = index_expr.to_sql() print("\n--- CREATE HNSW INDEX ---") print(f"SQL: {sql}") backend.execute(sql, params) - print("HNSW index created: idx_documents_embedding") + print("HNSW index created") - # Note: HNSW index options can be set via: - # SET hnsw.ef_search = 100; -- query-time parameter - # The index creation supports operator classes: - # vector_cosine_ops (default for <=>) - # vector_l2_ops (for <->) - # vector_ip_ops (for <#> inner product) + # Note: HNSW query-time parameter can be set via: + # SET hnsw.ef_search = 100; + # Supported operator classes (metric -> opclass): + # cosine -> vector_cosine_ops (<=>) + # l2 -> vector_l2_ops (<->) + # ip -> vector_ip_ops (<#> inner product) else: print("\nSkipping execution - vector extension not available on this server") @@ -269,7 +220,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="documents", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table="documents", if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/__init__.py new file mode 100644 index 00000000..79aedea2 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/__init__.py @@ -0,0 +1,2 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/__init__.py +"""Named query examples for PostgreSQL backend.""" \ No newline at end of file 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 new file mode 100644 index 00000000..f597f210 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py @@ -0,0 +1,189 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py +""" +Order-related named query examples. + +This file demonstrates how to define named queries (Named Query) for encapsulating +reusable SQL query logic. Named queries are backend features, independent of +ActiveRecord models. + +PostgreSQL notes +---------------- +Unlike the SQLite examples, this module does NOT create any tables or data at +import time. Importing this module must stay side-effect free (the CLI imports +it for --list/--describe without a live database). Use ``prepare_orders_demo`` +to create the schema and seed data, then the CLI named-expression / +named-procedure commands to execute the queries. +""" + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.expression import ( # noqa: E402 + Column, + Literal, + QueryExpression, + TableExpression, +) + + +def get_order(dialect, order_id: int): + """Get order details by ID.""" + return QueryExpression( + dialect, + select=[Column(dialect, "id"), Column(dialect, "status"), Column(dialect, "user_id")], + from_=TableExpression(dialect, "orders"), + where=Column(dialect, "id") == Literal(dialect, order_id), + ) + + +def check_inventory(dialect, order_id: int): + """Check available inventory for an order.""" + return QueryExpression( + dialect, + select=[Column(dialect, "available")], + from_=TableExpression(dialect, "inventory"), + where=Column(dialect, "order_id") == Literal(dialect, order_id), + ) + + +def reserve_inventory(dialect, order_id: int): + """Reserve inventory for an order.""" + return QueryExpression( + dialect, + select=[Column(dialect, "id"), Column(dialect, "available")], + from_=TableExpression(dialect, "inventory"), + where=Column(dialect, "order_id") == Literal(dialect, order_id), + ) + + +def send_notification(dialect, user_id: int, type: str): + """Send notification to a user.""" + return QueryExpression( + dialect, + select=[Column(dialect, "id")], + from_=TableExpression(dialect, "notifications"), + where=Column(dialect, "user_id") == Literal(dialect, user_id), + ) + + +def process_payment(dialect, order_id: int, amount: float): + """Process payment for an order.""" + return QueryExpression( + dialect, + select=[Column(dialect, "status"), Column(dialect, "transaction_id")], + from_=TableExpression(dialect, "payments"), + where=Column(dialect, "order_id") == Literal(dialect, order_id), + ) + + +def release_inventory(dialect, order_id: int): + """Release reserved inventory.""" + return QueryExpression( + dialect, + select=[Column(dialect, "id")], + from_=TableExpression(dialect, "inventory"), + where=Column(dialect, "order_id") == Literal(dialect, order_id), + ) + + +def create_order_record(dialect, order_id: int, user_id: int, amount: float): + """Create an order record.""" + return QueryExpression( + dialect, + select=[Column(dialect, "id"), Column(dialect, "created_at")], + from_=TableExpression(dialect, "order_records"), + where=Column(dialect, "order_id") == Literal(dialect, order_id), + ) + + +def confirm_inventory(dialect, order_id: int): + """Confirm inventory (final confirmation).""" + return QueryExpression( + dialect, + select=[Column(dialect, "id")], + from_=TableExpression(dialect, "inventory"), + where=Column(dialect, "order_id") == Literal(dialect, order_id), + ) + + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +# The setup below is import-safe: it only prepares the schema when this module +# is run directly, so importing it for --list/--describe never touches a database. + +_PREPARE_STATEMENTS = [ + "DROP TABLE IF EXISTS order_records", + "DROP TABLE IF EXISTS payments", + "DROP TABLE IF EXISTS notifications", + "DROP TABLE IF EXISTS inventory", + "DROP TABLE IF EXISTS orders", + ( + "CREATE TABLE orders (" + " id SERIAL PRIMARY KEY," + " status VARCHAR(20) DEFAULT 'pending'," + " user_id INTEGER NOT NULL)" + ), + "CREATE TABLE inventory (id SERIAL PRIMARY KEY, order_id INTEGER NOT NULL, available INTEGER DEFAULT 0)", + "CREATE TABLE notifications (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, type VARCHAR(20))", + ( + "CREATE TABLE payments (" + " id SERIAL PRIMARY KEY," + " order_id INTEGER NOT NULL," + " status VARCHAR(20)," + " transaction_id VARCHAR(40))" + ), + ( + "CREATE TABLE order_records (" + " id SERIAL PRIMARY KEY," + " order_id INTEGER NOT NULL," + " created_at VARCHAR(30))" + ), + "INSERT INTO orders (id, status, user_id) VALUES (1, 'pending', 100)", + "INSERT INTO inventory (id, order_id, available) VALUES (1, 1, 10)", +] + + +def prepare_orders_demo(backend) -> None: + """Create the demo schema and seed data. + + Drops any existing demo tables first so the demo is idempotent. + + Args: + backend: Connected PostgresBackend instance. + """ + for statement in _PREPARE_STATEMENTS: + backend.execute(statement) + + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +if __name__ == "__main__": + import os + + from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig + + config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "127.0.0.1"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), + database=os.getenv("POSTGRES_DATABASE", "postgres"), + ) + backend = PostgresBackend(connection_config=config) + backend.connect() + dialect = backend.dialect + + prepare_orders_demo(backend) + + print("=== Named Query Examples ===\n") + query = get_order(dialect, order_id=1) + sql, params = query.to_sql() + print(f"get_order SQL: {sql}") + print(f"Params: {params}\n") + + result = backend.execute(sql, params) + print(f"Execution result: {result.data}\n") + + backend.disconnect() \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/__init__.py new file mode 100644 index 00000000..4bea42f2 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/__init__.py @@ -0,0 +1,2 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/__init__.py +"""Named procedure examples for PostgreSQL backend.""" \ No newline at end of file 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 new file mode 100644 index 00000000..20db15c2 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py @@ -0,0 +1,174 @@ +# src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py +""" +Order processing workflow example - demonstrates Named Procedure flowchart capabilities. + +This procedure includes: +- Conditional branching (inventory check) +- Parallel execution (inventory reservation + notification) +- Conditional rollback (payment failure) + +Named Procedure is a backend feature, independent of ActiveRecord models. + +PostgreSQL notes +---------------- +This module is import-safe: no database connection is made at import time. +Prepare the schema and seed data with +``rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.prepare_orders_demo`` +before running the procedure. +""" + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.named_expression import ( # noqa: E402 + Procedure, + ProcedureContext, + ParallelStep, +) + + +class OrderProcessingProcedure(Procedure): + """Complete order processing workflow. + + Flow: + 1. Query order details + 2. Check inventory (abort if insufficient) + 3. Parallel: reserve inventory + send notification + 4. Process payment (rollback inventory on failure) + 5. Create order record + 6. Final inventory confirmation + """ + + order_id: int + user_id: int + amount: float = 0.0 + + def run(self, ctx: ProcedureContext) -> None: + ctx.log(f"Starting order processing: {self.order_id}", "INFO") + + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.get_order", + params={"order_id": self.order_id}, + bind="order", + ) + + order = ctx.scalar("order", "status") + if order is None: + ctx.log(f"Order {self.order_id} not found", "ERROR") + ctx.abort("OrderProcessingProcedure", f"Order {self.order_id} not found") + + ctx.log(f"Order status: {order}", "DEBUG") + + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.check_inventory", + params={"order_id": self.order_id}, + bind="inventory", + ) + + available = ctx.scalar("inventory", "available") + if not available or available < 1: + ctx.log("Insufficient inventory, aborting", "WARNING") + ctx.abort("OrderProcessingProcedure", "Insufficient inventory") + + ctx.parallel( + ParallelStep( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.reserve_inventory", + params={"order_id": self.order_id}, + bind="reserved", + ), + ParallelStep( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.send_notification", + params={"user_id": self.user_id, "type": "order_started"}, + ), + max_concurrency=1, + ) + + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.process_payment", + params={"order_id": self.order_id, "amount": self.amount}, + bind="payment", + ) + + payment_status = ctx.scalar("payment", "status") + if payment_status != "success": + ctx.log(f"Payment failed: {payment_status}, rolling back inventory", "ERROR") + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.release_inventory", + params={"order_id": self.order_id}, + output=True, + ) + ctx.abort("OrderProcessingProcedure", f"Payment failed: {payment_status}") + + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.create_order_record", + params={ + "order_id": self.order_id, + "user_id": self.user_id, + "amount": self.amount, + }, + bind="order_record", + output=True, + ) + + ctx.execute( + "rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions.confirm_inventory", + params={"order_id": self.order_id}, + output=True, + ) + + ctx.log(f"Order {self.order_id} processing complete", "INFO") + + +# Demo: Generate static diagram +if __name__ == "__main__": + print("=== Order Processing Procedure ===\n") + print("Static Diagram (Flowchart):") + print(OrderProcessingProcedure.static_diagram("flowchart")) + print("\nStatic Diagram (Sequence):") + print(OrderProcessingProcedure.static_diagram("sequence")) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +from rhosocial.activerecord.backend.named_expression import ProcedureRunner, TransactionMode # noqa: E402 + +if __name__ == "__main__": + import os + + from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions import ( + prepare_orders_demo, + ) + + config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "127.0.0.1"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), + database=os.getenv("POSTGRES_DATABASE", "postgres"), + ) + backend = PostgresBackend(connection_config=config) + backend.connect() + + prepare_orders_demo(backend) + + runner = ProcedureRunner( + "rhosocial.activerecord.backend.impl.postgres.examples.named_procedures.order_workflow.OrderProcessingProcedure" + ).load() + + result = runner.run( + backend, + user_params={"order_id": 1, "user_id": 100, "amount": 99.99}, + transaction_mode=TransactionMode.AUTO, + ) + + print(f"Procedure completed. Aborted: {result.aborted}") + if result.aborted: + print(f"Abort reason: {result.abort_reason}") + for log in result.logs: + print(f"[{log.level}] {log.message}") + + # ============================================================ + # SECTION: Teardown (necessary for execution, reference only) + # ============================================================ + backend.disconnect() \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py index 14200826..7d79bfc2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py @@ -93,6 +93,8 @@ PostgresUUIDType, PostgresVarBitType, PostgresVectorType, + PostgresHalfvecType, + PostgresSparsevecType, PostgresXID8Type, PostgresXIDType, PostgresXMLType, @@ -179,6 +181,8 @@ "PostgresUUIDType", "PostgresVarBitType", "PostgresVectorType", + "PostgresHalfvecType", + "PostgresSparsevecType", "PostgresXID8Type", "PostgresXIDType", "PostgresXMLType", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index 53d43944..d4dbefa2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -368,6 +368,56 @@ def __hash__(self) -> int: return hash((type(self), self.dim)) +class PostgresHalfvecType(DataType, backend="postgres"): + """pgvector ``HALFVEC(n)`` — half-precision vector (pgvector 0.5.0+). + + Stores each component as a half-precision float, halving memory compared + to ``VECTOR``. Requires the pgvector extension. + + Args: + dim: Number of dimensions. + """ + + dim: int + + def __init__(self, dim: int): + super().__init__() + self.dim = dim + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.dim == other.dim + + def __hash__(self) -> int: + return hash((type(self), self.dim)) + + +class PostgresSparsevecType(DataType, backend="postgres"): + """pgvector ``SPARSEVEC(n)`` — sparse vector (pgvector 0.7.0+). + + Stores only non-zero components as ``{idx:value,...}/dim``, suitable for + high-dimensional sparse embeddings. Requires the pgvector extension. + + Args: + dim: Number of dimensions. + """ + + dim: int + + def __init__(self, dim: int): + super().__init__() + self.dim = dim + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.dim == other.dim + + def __hash__(self) -> int: + return hash((type(self), self.dim)) + + # --------------------------------------------------------------------------- # Array container # --------------------------------------------------------------------------- diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py index da294485..3c3dfbc8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/__init__.py @@ -287,10 +287,17 @@ vector_l2_distance, vector_cosine_distance, vector_inner_product, + vector_distance, vector_cosine_similarity, vector_literal, ) +# pgvector similarity-search helpers +from .vector_search import ( + vector_search, + create_vector_index, +) + # PostGIS functions from .postgis import ( st_make_point, @@ -714,8 +721,12 @@ "vector_l2_distance", "vector_cosine_distance", "vector_inner_product", + "vector_distance", "vector_cosine_similarity", "vector_literal", + # pgvector similarity-search helpers + "vector_search", + "create_vector_index", # PostGIS functions - Construction "st_make_point", "st_geom_from_text", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py index 8d56b2de..d834337c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py @@ -752,7 +752,7 @@ def hstore_to_array_operator( BinaryExpression for %%hstore """ return BinaryExpression( - dialect, "%%", + dialect, "%", core.Literal(dialect, ""), _convert_to_expression(dialect, hstore_expr), ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py index 4bbb2df1..00e88d31 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py @@ -13,6 +13,10 @@ - <=> : Cosine distance - <#> : Inner product (negative) +NOTE: The operator factories treat plain string arguments as literal values, +NOT column references. Pass ``Column(dialect, name)`` (or use ``vector_distance``) +to reference a vector column. + The vector type requires the pgvector extension: CREATE EXTENSION IF NOT EXISTS vector; """ @@ -27,6 +31,13 @@ from rhosocial.activerecord.backend.dialect import SQLDialectBase +_METRIC_OPERATORS = { + "cosine": "<=>", + "l2": "<->", + "ip": "<#>", +} + + def _convert_to_expression( dialect: "SQLDialectBase", expr: Union[PostgresVector, List[float], str, "bases.BaseExpression"], @@ -71,7 +82,8 @@ def vector_l2_distance( Args: dialect: The SQL dialect instance - left: Left operand (column name, vector, or expression) + left: Left operand (vector, string literal, or expression); plain + strings are literal values, use ``Column(dialect, name)`` for columns right: Right operand (vector literal, or expression) Returns: @@ -96,7 +108,8 @@ def vector_cosine_distance( Args: dialect: The SQL dialect instance - left: Left operand (column name, vector, or expression) + left: Left operand (vector, string literal, or expression); plain + strings are literal values, use ``Column(dialect, name)`` for columns right: Right operand (vector literal, or expression) Returns: @@ -119,7 +132,8 @@ def vector_inner_product( Args: dialect: The SQL dialect instance - left: Left operand (column name, vector, or expression) + left: Left operand (vector, string literal, or expression); plain + strings are literal values, use ``Column(dialect, name)`` for columns right: Right operand (vector literal, or expression) Returns: @@ -130,6 +144,41 @@ def vector_inner_product( return BinaryArithmeticExpression(dialect, "<#>", left_expr, right_expr) +def vector_distance( + dialect: "SQLDialectBase", + column: Union[str, "bases.BaseExpression"], + query_vector: Union[PostgresVector, List[float], str, "bases.BaseExpression"], + metric: str = "cosine", +) -> BinaryArithmeticExpression: + """Distance operator for a vector column, dispatching by metric. + + Unlike the raw operator factories, a plain string ``column`` is wrapped in + ``Column`` (a column reference), so passing a column name is safe. + + Args: + dialect: The SQL dialect instance + column: Vector column name (string) or column expression + query_vector: The query vector (``PostgresVector``, ``List[float]``, + str, or expression) + metric: Distance metric - 'cosine' | 'l2' | 'ip' + + Returns: + BinaryArithmeticExpression for the requested distance + """ + if metric not in _METRIC_OPERATORS: + raise ValueError( + f"Unsupported vector metric '{metric}'; expected one of " + f"{tuple(_METRIC_OPERATORS)}" + ) + column_expr = ( + core.Column(dialect, column) if isinstance(column, str) else column + ) + return BinaryArithmeticExpression( + dialect, _METRIC_OPERATORS[metric], + column_expr, _convert_to_expression(dialect, query_vector), + ) + + # === Similarity === def vector_cosine_similarity( @@ -187,6 +236,7 @@ def vector_literal( "vector_l2_distance", "vector_cosine_distance", "vector_inner_product", + "vector_distance", "vector_cosine_similarity", "vector_literal", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py new file mode 100644 index 00000000..44e856b1 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py @@ -0,0 +1,216 @@ +# src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py +""" +PostgreSQL pgvector similarity-search helpers. + +High-level helpers built on top of the pgvector operator factories in +``functions/pgvector.py``: + +- ``vector_search``: assemble a complete similarity-search + ``QueryExpression`` (SELECT distance/similarity, WHERE filters, + ORDER BY distance, LIMIT top_k). +- ``create_vector_index``: assemble a ``CreateIndexExpression`` for + HNSW/IVFFlat with metric-to-operator-class mapping. + +They target the RAG knowledge-base pattern: + index(chunk/tenant/meta/vector) + search(query_vector, top_k, filters) + +Requires the pgvector extension: CREATE EXTENSION IF NOT EXISTS vector; +""" + +from typing import List, Optional, Union, Dict, TYPE_CHECKING + +from rhosocial.activerecord.backend.expression import ( + Column, + Literal, + QueryExpression, + TableExpression, +) +from rhosocial.activerecord.backend.expression.core import ( + BaseExpression, + WildcardExpression, +) +from rhosocial.activerecord.backend.expression.operators import BinaryArithmeticExpression +from rhosocial.activerecord.backend.expression.query_parts import ( + LimitOffsetClause, + OrderByClause, +) +from rhosocial.activerecord.backend.expression.query_parts import ( + SQLPredicate, + WhereClause, +) +from rhosocial.activerecord.backend.expression.statements.ddl_index import ( + CreateIndexExpression, +) + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.dialect import SQLDialectBase + +from .pgvector import ( + vector_cosine_distance, + vector_inner_product, + vector_l2_distance, +) + +__all__ = ["vector_search", "create_vector_index"] + +VECTOR_METRICS = ("cosine", "l2", "ip") + +_METRIC_DISTANCE_FN: Dict[str, object] = { + "cosine": vector_cosine_distance, + "l2": vector_l2_distance, + "ip": vector_inner_product, +} + +_METRIC_DISTANCE_ALIAS = { + "cosine": "cosine_distance", + "l2": "l2_distance", + "ip": "inner_product", +} + +_METRIC_SIMILARITY_ALIAS = { + "cosine": "cosine_similarity", + "l2": "l2_similarity", +} + +_METRIC_OPCLASS = { + "cosine": "vector_cosine_ops", + "l2": "vector_l2_ops", + "ip": "vector_ip_ops", +} + + +def _validate_metric(metric: str) -> None: + if metric not in VECTOR_METRICS: + raise ValueError( + f"Unsupported vector metric '{metric}'; expected one of {VECTOR_METRICS}" + ) + + +def vector_search( + dialect: "SQLDialectBase", + table: str, + query_vector, + metric: str = "cosine", + top_k: int = 10, + *, + vector_column: str = "embedding", + columns: Optional[List[Union[str, BaseExpression]]] = None, + where: Optional[Union[SQLPredicate, WhereClause]] = None, + include_distance: bool = True, + include_similarity: bool = False, +) -> QueryExpression: + """Build a pgvector similarity-search query. + + Args: + dialect: The SQL dialect instance + table: Table name to search + query_vector: Query vector (``PostgresVector``, ``List[float]``, str, + or expression) + metric: Distance metric - 'cosine' | 'l2' | 'ip' + top_k: Number of nearest neighbors to return (LIMIT) + vector_column: Vector column name (default 'embedding') + columns: Select list; strings become columns, expressions pass through. + Defaults to ``SELECT *``. + where: Optional filter predicate (e.g. tenant/permission scoping) + include_distance: Include an aliased distance column in SELECT + include_similarity: Include ``1 - distance`` for cosine/l2 in SELECT + + Returns: + A ready-to-execute ``QueryExpression``. + """ + _validate_metric(metric) + if top_k <= 0: + raise ValueError("top_k must be positive") + + vector_col = Column(dialect, vector_column) + distance_expr = _METRIC_DISTANCE_FN[metric](dialect, vector_col, query_vector) + + if columns is None: + select: List[BaseExpression] = [WildcardExpression(dialect)] + else: + select = [ + Column(dialect, c) if isinstance(c, str) else c for c in columns + ] + + if include_distance: + select.append(distance_expr.as_(_METRIC_DISTANCE_ALIAS[metric])) + if include_similarity and metric in _METRIC_SIMILARITY_ALIAS: + similarity = BinaryArithmeticExpression( + dialect, "-", Literal(dialect, 1), distance_expr + ) + select.append(similarity.as_(_METRIC_SIMILARITY_ALIAS[metric])) + + return QueryExpression( + dialect=dialect, + select=select, + from_=TableExpression(dialect, table), + where=where, + order_by=OrderByClause(dialect, [distance_expr]), + limit_offset=LimitOffsetClause(dialect, limit=top_k), + ) + + +def create_vector_index( + dialect: "SQLDialectBase", + table_name: str, + column_name: str = "embedding", + metric: str = "cosine", + index_type: str = "hnsw", + *, + index_name: Optional[str] = None, + m: Optional[int] = None, + ef_construction: Optional[int] = None, + lists: Optional[int] = None, + if_not_exists: bool = True, +) -> CreateIndexExpression: + """Build a pgvector index creation expression. + + Args: + dialect: The SQL dialect instance + table_name: Table name + column_name: Vector column name (default 'embedding') + metric: Distance metric - 'cosine' | 'l2' | 'ip' (maps to opclass) + index_type: Index type - 'hnsw' | 'ivfflat' + index_name: Optional index name (auto-generated if not provided) + m: HNSW max connections per layer + ef_construction: HNSW ef_construction + lists: IVFFlat number of lists + if_not_exists: Add IF NOT EXISTS + + Returns: + A ready-to-execute ``CreateIndexExpression``. + + Note: + IVFFlat requires the table to contain data before the index can be + created (HNSW has no such requirement). Create the index after + inserting rows when using ``index_type='ivfflat'``. + """ + _validate_metric(metric) + index_type_l = index_type.lower() + if index_type_l not in ("hnsw", "ivfflat"): + raise ValueError(f"Unsupported vector index type '{index_type}'; " + "expected 'hnsw' or 'ivfflat'") + + idx_name = index_name or f"idx_{table_name}_{column_name}_{index_type_l}" + with_options: Dict[str, int] = {} + if index_type_l == "hnsw": + if m is not None: + with_options["m"] = m + if ef_construction is not None: + with_options["ef_construction"] = ef_construction + else: + if lists is not None: + with_options["lists"] = lists + + return CreateIndexExpression( + dialect=dialect, + index_name=idx_name, + table_name=table_name, + columns=[column_name], + index_type=index_type_l.upper(), + if_not_exists=if_not_exists, + dialect_options={ + "opclasses": {column_name: _METRIC_OPCLASS[metric]}, + "with": with_options, + }, + ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py b/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py index 3ba187d1..ea4371bb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py @@ -71,7 +71,22 @@ class PostgreSQLIntrospectorMixin(IntrospectorMixin): """ def _get_default_schema(self) -> str: - """Return 'public' as PostgreSQL default schema.""" + """Return the actual schema from backend config or search_path. + + Priority: + 1. config.default_schema + 2. First entry of config.search_path (e.g. 'broker, public' → 'broker') + 3. Fallback to 'public' + """ + config = getattr(self._backend, "config", None) if hasattr(self, "_backend") else None + if config is not None: + if hasattr(config, "default_schema") and getattr(config, "default_schema"): + return str(getattr(config, "default_schema")) + if hasattr(config, "search_path") and getattr(config, "search_path"): + sp = str(getattr(config, "search_path")) + first = sp.split(",")[0].strip().strip('"').strip("'") + if first: + return first return "public" def _get_database_name(self) -> str: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pgvector.py index 08dd6985..7b33b05f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pgvector.py @@ -43,6 +43,7 @@ def format_create_vector_index_statement( ef_construction: Optional[int] = None, lists: Optional[int] = None, schema: Optional[str] = None, + opclass: str = "vector_cosine_ops", ) -> Tuple[str, tuple]: """Format CREATE INDEX statement for vector column. @@ -55,6 +56,9 @@ def format_create_vector_index_statement( ef_construction: HNSW ef_construction parameter lists: IVFFlat number of lists schema: Optional schema name + opclass: pgvector operator class for the index; defaults to + ``vector_cosine_ops``. Use ``vector_l2_ops`` or ``vector_ip_ops`` + for L2/inner-product distance indexes. Returns: Tuple of (SQL statement, parameters) @@ -70,13 +74,13 @@ def format_create_vector_index_statement( with_clause = f" WITH ({', '.join(with_clauses)})" if with_clauses else "" sql = ( f"CREATE INDEX {index_name} ON {full_table} " - f"USING hnsw ({column_name} vector_cosine_ops){with_clause}" + f"USING hnsw ({column_name} {opclass}){with_clause}" ) else: # ivfflat lists_clause = f" WITH (lists = {lists})" if lists else "" sql = ( f"CREATE INDEX {index_name} ON {full_table} " - f"USING ivfflat ({column_name} vector_cosine_ops){lists_clause}" + f"USING ivfflat ({column_name} {opclass}){lists_clause}" ) return (sql, ()) @@ -88,6 +92,7 @@ def format_create_hnsw_index_statement( index_name: Optional[str] = None, m: Optional[int] = None, ef_construction: Optional[int] = None, + opclass: str = "vector_cosine_ops", ) -> str: """Format CREATE INDEX statement with HNSW index for vector column. @@ -97,6 +102,8 @@ def format_create_hnsw_index_statement( index_name: Optional index name (auto-generated if not provided) m: HNSW M parameter (max connections per layer) ef_construction: HNSW ef_construction parameter + opclass: pgvector operator class for the index; defaults to + ``vector_cosine_ops``. Returns: SQL CREATE INDEX statement @@ -110,5 +117,5 @@ def format_create_hnsw_index_statement( with_clause = f" WITH ({', '.join(with_clauses)})" if with_clauses else "" return ( f"CREATE INDEX {idx_name} ON {table_name} " - f"USING hnsw ({column_name} vector_cosine_ops){with_clause}" + f"USING hnsw ({column_name} {opclass}){with_clause}" ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/introspection.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/introspection.py index 1df9610e..f489329b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/introspection.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/introspection.py @@ -107,11 +107,22 @@ def get_supported_introspection_scopes(self) -> List[IntrospectionScope]: def _get_default_schema(self) -> str: """Get default schema name. - PostgreSQL's default schema is 'public'. - - Returns: - The default schema name 'public'. + PostgreSQL's default schema is 'public', but may be overridden by + the connection config's `default_schema` or the first entry of + `search_path`. Returns 'public' if neither is set. """ + config = getattr(self, "_config", None) or getattr(self, "_backend", None) + cfg = config.config if hasattr(config, "config") else config + if cfg is not None: + default_schema = getattr(cfg, "default_schema", None) + if default_schema: + return str(default_schema) + search_path = getattr(cfg, "search_path", None) + if search_path: + # search_path 形如 "broker", public → 取第一个有效 schema + first = str(search_path).split(",")[0].strip().strip('"').strip("'") + if first: + return first return "public" # ========== Query Formatting Methods ========== diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py index 8273b0c4..e94b21fc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py @@ -8,6 +8,10 @@ class PostgresSchemaMixin: All features are native, using version number for detection. """ + def supports_schema(self) -> bool: + """PostgreSQL models named schema namespaces natively.""" + return True + def supports_create_schema(self) -> bool: return True @@ -22,3 +26,6 @@ def supports_schema_if_exists(self) -> bool: def supports_schema_cascade(self) -> bool: return True + + def supports_schema_authorization(self) -> bool: + return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py index 83bebc5d..e3d33eae 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py @@ -97,7 +97,11 @@ def format_column_definition(self, col_def) -> Tuple[str, tuple]: col_sql += f" GENERATED {identity.upper()} AS IDENTITY" else: raise ValueError(f"Invalid identity option '{identity}': must be 'ALWAYS' or 'BY DEFAULT'") + auto_identity_added = False for constraint in col_def.constraints: + if not auto_identity_added and getattr(constraint, 'is_auto_increment', False): + col_sql += " GENERATED BY DEFAULT AS IDENTITY" + auto_identity_added = True suffix, params = self.format_column_constraint(constraint) col_sql += suffix all_params.extend(params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 73c155f6..2cfb4833 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -80,6 +80,8 @@ PostgresUUIDType, PostgresVarBitType, PostgresVectorType, + PostgresHalfvecType, + PostgresSparsevecType, PostgresXID8Type, PostgresXIDType, PostgresXMLType, @@ -307,6 +309,14 @@ def format_data_type_geography(self, data_type) -> Tuple[str, tuple]: def format_data_type_vector(self, data_type) -> Tuple[str, tuple]: return f"VECTOR({data_type.dim})", () + @DDLTypeMixin.handles(PostgresHalfvecType) + def format_data_type_halfvec(self, data_type) -> Tuple[str, tuple]: + return f"HALFVEC({data_type.dim})", () + + @DDLTypeMixin.handles(PostgresSparsevecType) + def format_data_type_sparsevec(self, data_type) -> Tuple[str, tuple]: + return f"SPARSEVEC({data_type.dim})", () + @DDLTypeMixin.handles(PostgresCitextType) def format_data_type_citext(self, data_type) -> Tuple[str, tuple]: return "CITEXT", () @@ -809,10 +819,27 @@ def parse_type(self, raw: str) -> DataType: if upper.startswith("GEOMETRY"): return PostgresGeometryType() - # Vector type (pgvector) - if upper.startswith("VECTOR"): + # Vector types (pgvector) + vector_match = re.match(r"^(VECTOR|HALFVEC|SPARSEVEC)\b", upper) + if vector_match: + if ( + hasattr(self, "_extensions") + and "vector" in self._extensions + and not self._extensions["vector"].installed + ): + raise RuntimeError( + "Cannot use pgvector types: the 'vector' (pgvector) extension " + "is not installed. Run: CREATE EXTENSION IF NOT EXISTS vector;" + ) nums = re.findall(r"\d+", stripped) dim = int(nums[0]) if nums else 0 + vector_kind = vector_match.group(1) + if vector_kind == "HALFVEC": + from ...expression.types import PostgresHalfvecType + return PostgresHalfvecType(dim) + if vector_kind == "SPARSEVEC": + from ...expression.types import PostgresSparsevecType + return PostgresSparsevecType(dim) from ...expression.types import PostgresVectorType return PostgresVectorType(dim) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/extensions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/extensions/pgvector.py index f208adf7..53146cee 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/extensions/pgvector.py @@ -60,6 +60,7 @@ def format_create_vector_index_statement( ef_construction: Optional[int] = None, lists: Optional[int] = None, schema: Optional[str] = None, + opclass: str = "vector_cosine_ops", ) -> Tuple[str, tuple]: """Format CREATE INDEX statement for vector column.""" ... @@ -71,6 +72,7 @@ def format_create_hnsw_index_statement( index_name: Optional[str] = None, m: Optional[int] = None, ef_construction: Optional[int] = None, + opclass: str = "vector_cosine_ops", ) -> str: """Format CREATE INDEX statement with HNSW index for vector column.""" ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py index 0356671e..31723b24 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py @@ -8,6 +8,8 @@ class PostgresSchemaSupport(Protocol): """PostgreSQL schema feature support protocol.""" + def supports_schema(self)-> bool: ... + def supports_create_schema(self)-> bool: ... def supports_drop_schema(self)-> bool: ... @@ -17,3 +19,5 @@ def supports_schema_if_not_exists(self)-> bool: ... def supports_schema_if_exists(self)-> bool: ... def supports_schema_cascade(self)-> bool: ... + + def supports_schema_authorization(self)-> bool: ... diff --git a/tests/conftest.py b/tests/conftest.py index 34dcd084..4d68a27f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -105,12 +105,18 @@ def pytest_addoption(parser): default=False, help='Scenario parallel mode: distribute scenarios across workers, keep each scenario on one worker.', ) - parser.addoption( - '--scenarios', - default=None, - help='Comma-separated list of scenario names to run (e.g., --scenarios=postgres_16,postgres_17). ' - 'Compatible with pytest -k "" as a native alternative.', - ) + # The generic --scenarios option is registered by the testsuite conftest + # (loaded via ``addopts``); register it here only when it is not already + # present so the two conftests can coexist. + try: + parser.addoption( + '--scenarios', + default=None, + help='Comma-separated list of scenario names to run (e.g., --scenarios=postgres_16,postgres_17). ' + 'Compatible with pytest -k "" as a native alternative.', + ) + except ValueError: + pass def pytest_configure(config): diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 964849be..bcd7be81 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -350,6 +350,11 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = N else: raise ValueError("No scenarios registered") config_dict = SCENARIO_MAP[scenario_name] + from .pooling import resolve_database_name + + pooled_db = resolve_database_name(scenario_name) + if pooled_db: + config_dict = {**config_dict, "database": pooled_db} return { 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', 'backend_class_name': backend_class_name, diff --git a/tests/providers/pooling.py b/tests/providers/pooling.py new file mode 100644 index 00000000..40186779 --- /dev/null +++ b/tests/providers/pooling.py @@ -0,0 +1,132 @@ +# tests/providers/pooling.py +"""Database pooling helpers for the PostgreSQL test providers. + +Under parallel (pytest-xdist) runs with a positive pool size the providers +reuse a per-worker pooled database ``{database}_{index}`` on the scenario's +PostgreSQL server instead of the shared scenario ``database`` schema, so +scenario variants of the same test can run concurrently on different workers +without conflicting. The pool name prefix is derived from the scenario's +configured ``database`` (the YAML ``database`` field), so e.g. +``database: test_db`` produces pooled databases ``test_db_0``, ``test_db_1``, +... Serial runs (no ``-n``) keep the previous behaviour: the provider connects +to the scenario's configured ``database``. + +The scenario name selects the server (host/port); the pool index selects the +database name. The two are deliberately unrelated. +""" + +import psycopg + +from rhosocial.activerecord.testsuite.core.pool import ( + pooled_database_name, + register_base_database, + register_pool_reset_handler, +) + +from .scenarios import SCENARIO_MAP, get_scenario_raw + +# Derive each scenario's pooled-database base name from its configured +# ``database`` (YAML ``database`` field). Registered at import time so any +# caller of pooled_database_name() / resolve_database_name() resolves names +# consistent with the scenario configuration. +for _scenario_name, _scenario_config in SCENARIO_MAP.items(): + register_base_database(_scenario_name, _scenario_config["database"]) + + +def resolve_database_name(scenario_name: str): + """ + Return the pooled database name (e.g. ``test_db_3``) used by a test for + the given scenario, or ``None`` when pooling is inactive (callers then fall + back to the scenario's configured database). + """ + return pooled_database_name(scenario_name) + + +def _escape_identifier(name: str) -> str: + """Escape a PostgreSQL identifier for use inside double quotes.""" + return name.replace('"', '""') + + +def _connect_maintenance(config) -> psycopg.Connection: + """Connect to a maintenance database (``postgres`` or the configured one).""" + for dbname in ("postgres", config.database): + try: + return psycopg.connect( + host=config.host, + port=config.port, + dbname=dbname, + user=config.username, + password=config.password, + connect_timeout=10, + autocommit=True, + ) + except psycopg.Error: + continue + return None + + +def _reset_postgres_database(scenario_name: str, db_name: str) -> None: + """Ensure the pooled database exists and is empty on the scenario's server. + + Connects to the server selected by ``scenario_name``, creates the pooled + ``db_name`` database if missing, and drops all leftover non-extension + tables so the test starts from a clean state. Extension-owned tables are + preserved (e.g. PostGIS metadata). Errors are swallowed: a failed reset + must not hide the underlying test failure. + """ + if scenario_name not in SCENARIO_MAP: + return + _, config = get_scenario_raw(scenario_name) + try: + admin_conn = _connect_maintenance(config) + if admin_conn is None: + return + try: + with admin_conn.cursor() as cursor: + cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s", (db_name,)) + if cursor.fetchone() is None: + cursor.execute(f'CREATE DATABASE "{_escape_identifier(db_name)}"') + finally: + admin_conn.close() + except Exception: + return + + try: + conn = psycopg.connect( + host=config.host, + port=config.port, + dbname=db_name, + user=config.username, + password=config.password, + connect_timeout=10, + autocommit=True, + ) + try: + with conn.cursor() as cursor: + cursor.execute( + """ + SELECT n.nspname, c.relname + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r', 'p', 'm') + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + AND NOT EXISTS ( + SELECT 1 FROM pg_depend d + JOIN pg_extension e ON d.refobjid = e.oid + WHERE d.classid = 'pg_class'::regclass + AND d.objid = c.oid + AND d.deptype = 'e' + ) + """ + ) + for schema, table in cursor.fetchall(): + cursor.execute( + f'DROP TABLE IF EXISTS "{_escape_identifier(schema)}"."{_escape_identifier(table)}" CASCADE' + ) + finally: + conn.close() + except Exception: + pass + + +register_pool_reset_handler(_reset_postgres_database) diff --git a/tests/providers/query.py b/tests/providers/query.py index 209121c0..1e18c2a2 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -124,12 +124,83 @@ def _select_model_class(base_cls, py312_cls, py311_cls, py310_cls, model_name: s class QueryProviderBase: def __init__(self): - self._scenario_db_files = {} + self._active_backends = [] self._created_tables: Set[str] = set() + self._created_schemas: Set[str] = set() + self._mixed_schema_provisioned = False + + def supports_schema(self) -> bool: + """PostgreSQL models named schema namespaces natively.""" + return True def get_test_scenarios(self) -> List[str]: return list(get_enabled_scenarios().keys()) + def _provision_cross_schema(self, backend_instance) -> None: + """Create the two cross-schema fixture namespaces, tables and seeds.""" + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + SCHEMA_A, + SCHEMA_B, + ) + + opts = ExecutionOptions(stmt_type=StatementType.DDL) + statements = [] + for schema in (SCHEMA_A, SCHEMA_B): + statements.append(f'DROP SCHEMA IF EXISTS "{schema}" CASCADE') + statements.append(f'CREATE SCHEMA "{schema}"') + statements.append( + f'CREATE TABLE "{SCHEMA_A}".customers ' + f'(id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL)' + ) + statements.append( + f'CREATE TABLE "{SCHEMA_B}".orders ' + f'(id SERIAL PRIMARY KEY, customer_id INTEGER NOT NULL, amount INTEGER NOT NULL)' + ) + for sql in statements: + backend_instance.execute(sql, options=opts) + self._created_schemas.update((SCHEMA_A, SCHEMA_B)) + + def _provision_mixed_schema_sync(self, backend_instance) -> None: + """Provision an identical orders table in SCHEMA_A (default stays untouched).""" + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + SCHEMA_A, + ) + + opts = ExecutionOptions(stmt_type=StatementType.DDL) + statements = [ + f'DROP SCHEMA IF EXISTS "{SCHEMA_A}" CASCADE', + f'CREATE SCHEMA "{SCHEMA_A}"', + f'CREATE TABLE "{SCHEMA_A}".orders (' + "id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, " + "order_number VARCHAR(255) NOT NULL, " + "total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, " + "status VARCHAR(50) NOT NULL DEFAULT 'pending', " + "created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE)", + ] + for sql in statements: + backend_instance.execute(sql, options=opts) + self._mixed_schema_provisioned = True + + async def _provision_mixed_schema_async(self, backend_instance) -> None: + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + SCHEMA_A, + ) + + opts = ExecutionOptions(stmt_type=StatementType.DDL) + statements = [ + f'DROP SCHEMA IF EXISTS "{SCHEMA_A}" CASCADE', + f'CREATE SCHEMA "{SCHEMA_A}"', + f'CREATE TABLE "{SCHEMA_A}".orders (' + "id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, " + "order_number VARCHAR(255) NOT NULL, " + "total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, " + "status VARCHAR(50) NOT NULL DEFAULT 'pending', " + "created_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE)", + ] + for sql in statements: + await backend_instance.execute(sql, options=opts) + self._mixed_schema_provisioned = True + def _track_backend(self, backend_instance, collection): if backend_instance not in collection: collection.append(backend_instance) @@ -278,6 +349,40 @@ def setup_order_item_model(self, scenario_name: str) -> Type[ActiveRecord]: self._created_tables.add("order_items") return CompositeOrderItemBase + def setup_schema_fixtures(self, scenario_name: str): + """Two models in two distinct schemas (see testsuite schema_models).""" + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + SchemaCustomer, + SchemaOrder, + ) + + backend_class, config = get_scenario(scenario_name) + SchemaCustomer.configure(config, backend_class) + SchemaOrder.configure(config, backend_class) + shared_backend = SchemaCustomer.__backend__ + self._track_backend(shared_backend, self._active_backends) + self._provision_cross_schema(shared_backend) + return SchemaCustomer, SchemaOrder + + def setup_mixed_schema_fixtures(self, scenario_name: str): + """(User, Order, MixedSchemaOrder): default users/orders plus orders in SCHEMA_A.""" + from rhosocial.activerecord.testsuite.feature.query.cross_schema.mixed_schema_models import ( + MixedSchemaOrder, + ) + from rhosocial.activerecord.testsuite.feature.query.fixtures.models import Order, User + + user_model = self._setup_model(User, scenario_name, "users") + shared_backend = user_model.__backend__ + order_model = self._setup_model(Order, scenario_name, "orders", shared_backend) + + backend_class, config = get_scenario(scenario_name) + MixedSchemaOrder.__connection_config__ = config + MixedSchemaOrder.__backend_class__ = backend_class + MixedSchemaOrder.__backend__ = shared_backend + self._track_backend(shared_backend, self._active_backends) + self._provision_mixed_schema_sync(shared_backend) + return user_model, order_model, MixedSchemaOrder + def _get_schema_sql_for_fixture_type(self, fixture_type: str) -> dict: schemas = {} if fixture_type == 'order': @@ -306,6 +411,11 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = ' else: raise ValueError("No scenarios registered") config_dict = SCENARIO_MAP[scenario_name] + from .pooling import resolve_database_name + + pooled_db = resolve_database_name(scenario_name) + if pooled_db: + config_dict = {**config_dict, "database": pooled_db} return { 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', 'backend_class_name': backend_class_name, @@ -331,8 +441,24 @@ def cleanup_after_test(self, scenario_name: str): backend_instance.disconnect() except Exception: pass + for schema_name in list(self._created_schemas): + try: + backend_instance.execute(f'DROP SCHEMA IF EXISTS "{schema_name}" CASCADE') + except Exception: + pass + if self._mixed_schema_provisioned: + try: + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + SCHEMA_A, + ) + + backend_instance.execute(f'DROP SCHEMA IF EXISTS "{SCHEMA_A}" CASCADE') + except Exception: + pass + self._mixed_schema_provisioned = False self._active_backends.clear() self._created_tables.clear() + self._created_schemas.clear() class QueryAsyncProvider(QueryProviderBase, IQueryAsyncProvider): @@ -476,6 +602,34 @@ async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRe self._created_tables.add("order_items") return AsyncCompositeOrderItemBase + async def setup_schema_fixtures(self, scenario_name: str): + """Two async models in two distinct schemas.""" + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + AsyncSchemaCustomer, + AsyncSchemaOrder, + ) + + from rhosocial.activerecord.backend.impl.postgres import ( + AsyncPostgresBackend, + PostgresBackend, + ) + + _, config = get_scenario(scenario_name) + + # Provision DDL through a short-lived synchronous connection. + ddl_backend = PostgresBackend(connection_config=config) + ddl_backend.connect() + try: + self._provision_cross_schema(ddl_backend) + finally: + ddl_backend.disconnect() + + await AsyncSchemaCustomer.configure(config, AsyncPostgresBackend) + await AsyncSchemaOrder.configure(config, AsyncPostgresBackend) + shared_backend = AsyncSchemaCustomer.__backend__ + self._track_backend(shared_backend, self._active_async_backends) + return AsyncSchemaCustomer, AsyncSchemaOrder + async def cleanup_after_test(self, scenario_name: str): for backend_instance in self._active_async_backends: try: @@ -489,5 +643,46 @@ async def cleanup_after_test(self, scenario_name: str): await backend_instance.disconnect() except Exception: pass + for schema_name in list(self._created_schemas): + try: + await backend_instance.execute( + f'DROP SCHEMA IF EXISTS "{schema_name}" CASCADE') + except Exception: + pass + if self._mixed_schema_provisioned: + try: + from rhosocial.activerecord.testsuite.feature.query.fixtures.schema_models import ( + SCHEMA_A, + ) + + await backend_instance.execute(f'DROP SCHEMA IF EXISTS "{SCHEMA_A}" CASCADE') + except Exception: + pass + self._mixed_schema_provisioned = False self._active_async_backends.clear() self._created_tables.clear() + self._created_schemas.clear() + + 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.testsuite.feature.query.cross_schema.mixed_schema_models import ( + AsyncMixedSchemaOrder, + ) + from rhosocial.activerecord.testsuite.feature.query.fixtures.async_models import ( + AsyncOrder, + AsyncUser, + ) + + _, config = get_scenario(scenario_name) + user_model = await self._setup_async_model(AsyncUser, scenario_name, "users") + shared_backend = user_model.__backend__ + self._track_backend(shared_backend, self._active_async_backends) + order_model = await self._setup_async_model(AsyncOrder, scenario_name, "orders", shared_backend) + + AsyncMixedSchemaOrder.__connection_config__ = config + AsyncMixedSchemaOrder.__backend_class__ = AsyncPostgresBackend + AsyncMixedSchemaOrder.__backend__ = shared_backend + self._track_backend(shared_backend, self._active_async_backends) + await self._provision_mixed_schema_async(shared_backend) + return AsyncUser, order_model, AsyncMixedSchemaOrder diff --git a/tests/providers/registry.py b/tests/providers/registry.py index 32738870..a7d424c0 100644 --- a/tests/providers/registry.py +++ b/tests/providers/registry.py @@ -19,6 +19,10 @@ from .query_benchmark import QueryBenchmarkProvider from .transaction_benchmark import TransactionBenchmarkProvider +# Importing the pooling module registers the PostgreSQL-specific pool reset +# handler (a side effect of the import), mirroring MySQL's tests/providers/pooling.py. +from . import pooling # noqa: F401 + # Create a single, global instance of the ProviderRegistry for this backend. provider_registry = ProviderRegistry() diff --git a/tests/providers/scenarios.py b/tests/providers/scenarios.py index bdedbd04..2d1a04c4 100644 --- a/tests/providers/scenarios.py +++ b/tests/providers/scenarios.py @@ -2,9 +2,11 @@ """postgres backend test scenario configuration mapping table""" 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.config import PostgresConnectionConfig +from rhosocial.activerecord.testsuite.core.pool import pooled_database_name # Scenario name -> configuration dictionary mapping table (postgres only) SCENARIO_MAP: Dict[str, Dict[str, Any]] = {} @@ -15,10 +17,12 @@ def register_scenario(name: str, config: Dict[str, Any]): SCENARIO_MAP[name] = config -def get_scenario(name: str) -> Tuple[Type[PostgresBackend], PostgresConnectionConfig]: - """ - Retrieves the backend class and a connection configuration object for a given - scenario name. This is called by the provider to set up the database for a test. +def get_scenario_raw(name: str) -> Tuple[Type[PostgresBackend], PostgresConnectionConfig]: + """Return the backend class and connection config for a scenario. + + The config uses the scenario's configured ``database``; no pool override is + applied. Used by the pool reset handler, which must reach the server + regardless of whether a pooled database exists yet. """ if name not in SCENARIO_MAP: # If the specified scenario is not found, use the first available one as a fallback @@ -32,6 +36,22 @@ def get_scenario(name: str) -> Tuple[Type[PostgresBackend], PostgresConnectionCo return PostgresBackend, config +def get_scenario(name: str) -> Tuple[Type[PostgresBackend], PostgresConnectionConfig]: + """ + Retrieves the backend class and a connection configuration object for a given + scenario name. This is called by the provider to set up the database for a test. + + Under an active database pool the config's ``database`` is replaced by the + worker's pooled database name (``{database}_{index}``) so concurrent workers + never share a schema. Serial runs keep the scenario's configured database. + """ + backend_class, config = get_scenario_raw(name) + pooled_db = pooled_database_name(name) + if pooled_db: + config = replace(config, database=pooled_db) + return backend_class, config + + def get_enabled_scenarios() -> Dict[str, Any]: """ Returns the map of all currently enabled scenarios. The testsuite's conftest diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py index cf90c588..ba63d63b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py @@ -73,7 +73,14 @@ def get_scenario_config(name: str) -> Dict[str, Any]: name = next(iter(SCENARIO_MAP)) else: raise ValueError("No scenarios registered or enabled") - return SCENARIO_MAP[name] + config_dict = SCENARIO_MAP[name].copy() + # Apply the pooled database name so parallel workers never share a schema. + from providers.pooling import resolve_database_name + + pooled_db = resolve_database_name(name) + if pooled_db: + config_dict["database"] = pooled_db + return config_dict # --- Provider Logic for Fixtures --- diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_schema_support.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_schema_support.py new file mode 100644 index 00000000..b8c4a03e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_schema_support.py @@ -0,0 +1,66 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_schema_support.py +"""Tests for the SchemaSupport capability declared on the PostgreSQL dialect. + +PostgreSQL models named schema namespaces natively (database -> schema -> table), +so the dialect must report ``supports_schema()`` as True together with the full +set of granular schema DDL capabilities. +""" +from rhosocial.activerecord.backend.dialect.protocols import SchemaSupport +from rhosocial.activerecord.backend.expression.statements.ddl_schema import ( + CreateSchemaExpression, + DropSchemaExpression, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +class TestSchemaCapability: + """Umbrella flag and granular schema DDL capability bits.""" + + def _dialect(self) -> PostgresDialect: + return PostgresDialect() + + def test_supports_schema_is_true(self): + assert self._dialect().supports_schema() is True + + def test_implements_schema_support_protocol(self): + assert isinstance(self._dialect(), SchemaSupport) + + def test_granular_schema_ddl_capabilities(self): + d = self._dialect() + assert d.supports_create_schema() is True + assert d.supports_drop_schema() is True + assert d.supports_schema_if_not_exists() is True + assert d.supports_schema_if_exists() is True + assert d.supports_schema_cascade() is True + + def test_schema_authorization_capability(self): + assert self._dialect().supports_schema_authorization() is True + + +class TestSchemaDDLFormatting: + """CREATE/DROP SCHEMA rendering through the standard core formatters.""" + + def _dialect(self) -> PostgresDialect: + return PostgresDialect() + + def test_create_schema(self): + sql, params = CreateSchemaExpression(self._dialect(), "app").to_sql() + assert sql == 'CREATE SCHEMA "app"' + assert params == () + + def test_create_schema_if_not_exists(self): + sql, _ = CreateSchemaExpression(self._dialect(), "app", if_not_exists=True).to_sql() + assert sql == 'CREATE SCHEMA IF NOT EXISTS "app"' + + def test_create_schema_authorization(self): + sql, _ = CreateSchemaExpression(self._dialect(), "app", authorization="app_user").to_sql() + assert sql == 'CREATE SCHEMA "app" AUTHORIZATION "app_user"' + + def test_drop_schema(self): + sql, params = DropSchemaExpression(self._dialect(), "app").to_sql() + assert sql == 'DROP SCHEMA "app"' + assert params == () + + def test_drop_schema_if_exists_cascade(self): + sql, _ = DropSchemaExpression(self._dialect(), "app", if_exists=True, cascade=True).to_sql() + assert sql == 'DROP SCHEMA IF EXISTS "app" CASCADE' diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/examples_compile_test.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/examples_compile_test.py new file mode 100644 index 00000000..837afc53 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/examples_compile_test.py @@ -0,0 +1,36 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/examples_compile_test.py +"""Guard against syntax/API drift in the runnable examples. + +Every script under ``examples/`` must at least compile. This test runs +without a database and fails fast if an example introduces a syntax error. +(API drift that only manifests at runtime is caught by ``scripts/run_examples.py`` +against a live database in CI.) +""" +import py_compile +from pathlib import Path + +import pytest + +EXAMPLES_ROOT = ( + Path(__file__).resolve().parents[5] + / "src" / "rhosocial" / "activerecord" / "backend" / "impl" / "postgres" + / "examples" +) + + +def _collect_examples(): + if not EXAMPLES_ROOT.exists(): + return [] + return sorted(EXAMPLES_ROOT.rglob("*.py")) + + +def test_all_examples_compile(): + examples = _collect_examples() + assert examples, f"no examples found under {EXAMPLES_ROOT}" + failures = [] + for path in examples: + try: + py_compile.compile(str(path), doraise=True) + except py_compile.PyCompileError as exc: # pragma: no cover + failures.append(str(exc)) + assert not failures, "example compile failures:\n" + "\n".join(failures) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/vector_search_test.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/vector_search_test.py new file mode 100644 index 00000000..54f7fbd4 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/vector_search_test.py @@ -0,0 +1,198 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/expression/vector_search_test.py +"""SQL-snapshot tests for the pgvector similarity-search helpers. + +These tests build expressions with a bare ``PostgresDialect`` (no DB +connection) and assert on the generated SQL, so they run without a server. +""" +import pytest + +from rhosocial.activerecord.backend.expression import Column, Literal, predicates +from rhosocial.activerecord.backend.impl.postgres.functions import ( + create_vector_index, + vector_distance, + vector_search, +) + + +class TestVectorSearch: + """Tests for the vector_search helper.""" + + def test_default_select_star(self, postgres_dialect): + expr = vector_search(postgres_dialect, "documents", [1.0, 0.5, 0.2]) + sql, params = expr.to_sql() + assert "SELECT *" in sql + assert 'AS "cosine_distance"' in sql + assert "ORDER BY" in sql and "<=>" in sql + assert "LIMIT %s" in sql + assert params[-1] == 10 + + def test_columns_and_similarity(self, postgres_dialect): + expr = vector_search( + postgres_dialect, + "documents", + [1.0, 0.55, 0.18], + columns=["content", "tenant"], + include_similarity=True, + top_k=3, + ) + sql, params = expr.to_sql() + assert "SELECT \"content\", \"tenant\"" in sql + assert 'AS "cosine_distance"' in sql + assert 'AS "cosine_similarity"' in sql + assert "LIMIT %s" in sql and params[-1] == 3 + + def test_where_filter(self, postgres_dialect): + tenant_pred = predicates.ComparisonPredicate( + postgres_dialect, "=", Column(postgres_dialect, "tenant"), + Literal(postgres_dialect, "acme"), + ) + expr = vector_search( + postgres_dialect, "documents", [1.0, 0.5, 0.2], + where=tenant_pred, + ) + sql, params = expr.to_sql() + assert 'WHERE "tenant" = %s' in sql + assert "acme" in params + + def test_l2_metric(self, postgres_dialect): + expr = vector_search( + postgres_dialect, "documents", [0.1, 0.2, 0.9], + metric="l2", columns=["content"], + ) + sql, params = expr.to_sql() + assert 'AS "l2_distance"' in sql + assert "<->" in sql + + def test_ip_metric(self, postgres_dialect): + expr = vector_search( + postgres_dialect, "documents", [0.1, 0.2, 0.9], + metric="ip", columns=["content"], + ) + sql, params = expr.to_sql() + assert 'AS "inner_product"' in sql + assert "<#>" in sql + + def test_ip_metric_with_similarity_requested(self, postgres_dialect): + """ip metric has no 1-distance similarity alias, so none is added.""" + expr = vector_search( + postgres_dialect, "documents", [0.1, 0.2, 0.9], + metric="ip", columns=["content"], include_similarity=True, + ) + sql, params = expr.to_sql() + assert 'AS "inner_product"' in sql + assert "similarity" not in sql + + def test_include_distance_false(self, postgres_dialect): + expr = vector_search( + postgres_dialect, "documents", [0.1, 0.2, 0.9], + columns=["content"], include_distance=False, + ) + sql, params = expr.to_sql() + assert "cosine_distance" not in sql + + def test_l2_similarity_alias(self, postgres_dialect): + expr = vector_search( + postgres_dialect, "documents", [0.1, 0.2, 0.9], + metric="l2", include_similarity=True, + ) + sql, params = expr.to_sql() + assert 'AS "l2_similarity"' in sql + + def test_invalid_metric(self, postgres_dialect): + with pytest.raises(ValueError, match="Unsupported vector metric"): + vector_search(postgres_dialect, "documents", [1.0], metric="hamming") + + def test_non_positive_top_k(self, postgres_dialect): + with pytest.raises(ValueError, match="top_k must be positive"): + vector_search(postgres_dialect, "documents", [1.0], top_k=0) + + +class TestVectorDistance: + """Tests for the vector_distance column-name convenience.""" + + def test_column_name_wrapped_as_column(self, postgres_dialect): + expr = vector_distance(postgres_dialect, "embedding", [1.0, 0.5, 0.2]) + sql, params = expr.to_sql() + # Column name must render as identifier, not a string literal parameter. + assert 'AS' not in sql + assert '"embedding" <=> %s::vector(3)' in sql + assert "embedding" not in params + + def test_l2_column(self, postgres_dialect): + expr = vector_distance(postgres_dialect, "embedding", [1.0], metric="l2") + sql, params = expr.to_sql() + assert '"embedding" <-> %s::vector(1)' in sql + + def test_invalid_metric(self, postgres_dialect): + with pytest.raises(ValueError, match="Unsupported vector metric"): + vector_distance(postgres_dialect, "embedding", [1.0], metric="bad") + + +class TestCreateVectorIndex: + """Tests for the create_vector_index helper.""" + + def test_hnsw_default_cosine(self, postgres_dialect): + expr = create_vector_index(postgres_dialect, "documents", "embedding") + sql, params = expr.to_sql() + assert "CREATE INDEX IF NOT EXISTS" in sql + assert 'USING HNSW ("embedding" vector_cosine_ops)' in sql + + def test_hnsw_with_options(self, postgres_dialect): + expr = create_vector_index( + postgres_dialect, "documents", "embedding", + index_type="hnsw", m=16, ef_construction=64, + ) + sql, params = expr.to_sql() + assert "WITH (m = 16, ef_construction = 64)" in sql + + def test_ivfflat_l2(self, postgres_dialect): + expr = create_vector_index( + postgres_dialect, "documents", "embedding", + metric="l2", index_type="ivfflat", lists=4, + ) + sql, params = expr.to_sql() + assert 'USING IVFFLAT ("embedding" vector_l2_ops)' in sql + assert "WITH (lists = 4)" in sql + + def test_ivfflat_without_lists(self, postgres_dialect): + expr = create_vector_index( + postgres_dialect, "documents", "embedding", + metric="l2", index_type="ivfflat", + ) + sql, params = expr.to_sql() + assert 'USING IVFFLAT ("embedding" vector_l2_ops)' in sql + assert "WITH" not in sql + + def test_custom_index_name(self, postgres_dialect): + expr = create_vector_index( + postgres_dialect, "documents", "embedding", + index_name="doc_emb_hnsw", + ) + sql, params = expr.to_sql() + assert "doc_emb_hnsw" in sql + + def test_auto_index_name(self, postgres_dialect): + expr = create_vector_index( + postgres_dialect, "documents", "embedding", + index_type="hnsw", + ) + sql, params = expr.to_sql() + assert "idx_documents_embedding_hnsw" in sql + + def test_ip_opclass(self, postgres_dialect): + expr = create_vector_index( + postgres_dialect, "documents", "embedding", + metric="ip", index_type="hnsw", + ) + sql, params = expr.to_sql() + assert 'vector_ip_ops' in sql + + def test_invalid_metric(self, postgres_dialect): + with pytest.raises(ValueError, match="Unsupported vector metric"): + create_vector_index(postgres_dialect, "documents", "embedding", + metric="hamming") + + def test_invalid_index_type(self, postgres_dialect): + with pytest.raises(ValueError, match="Unsupported vector index type"): + create_vector_index(postgres_dialect, "documents", "embedding", + index_type="btree") 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 new file mode 100644 index 00000000..54e20116 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py @@ -0,0 +1,84 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py +"""Offline tests for ``_get_default_schema`` config-aware resolution. + +Both introspection stacks gained config-aware default-schema resolution: + +1. ``PostgreSQLIntrospectorMixin._get_default_schema`` (introspector) reads + ``self._backend.config``. +2. ``PostgresIntrospectionCapabilityMixin._get_default_schema`` (dialect SQL + generation) reads ``self._config`` or ``self._backend``. + +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.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.introspection import ( + SyncPostgreSQLIntrospector, +) + + +def make_config(**overrides) -> PostgresConnectionConfig: + params = dict( + host="localhost", + port=5432, + database="test", + username="postgres", + password="", + ) + params.update(overrides) + return PostgresConnectionConfig(**params) + + +def make_introspector(config: PostgresConnectionConfig) -> SyncPostgreSQLIntrospector: + backend = PostgresBackend(connection_config=config) + return SyncPostgreSQLIntrospector(backend, executor=object()) + + +class TestIntrospectorDefaultSchema: + """PostgreSQLIntrospectorMixin._get_default_schema (self._backend.config).""" + + def test_returns_public_when_not_configured(self): + introspector = make_introspector(make_config()) + assert introspector._get_default_schema() == "public" + + def test_returns_default_schema_from_config(self): + introspector = make_introspector(make_config(default_schema="broker")) + assert introspector._get_default_schema() == "broker" + + def test_returns_first_entry_of_search_path(self): + introspector = make_introspector(make_config(search_path="broker, public")) + assert introspector._get_default_schema() == "broker" + + def test_strips_quotes_from_search_path_entry(self): + introspector = make_introspector(make_config(search_path='"broker", public')) + assert introspector._get_default_schema() == "broker" + + def test_default_schema_takes_priority_over_search_path(self): + introspector = make_introspector( + make_config(default_schema="app", search_path="broker, public") + ) + assert introspector._get_default_schema() == "app" + + +class TestDialectMixinDefaultSchema: + """PostgresIntrospectionCapabilityMixin._get_default_schema (dialect).""" + + def test_returns_public_when_not_configured(self): + assert PostgresDialect()._get_default_schema() == "public" + + def test_returns_default_schema_from_config(self): + dialect = PostgresDialect() + dialect._config = make_config(default_schema="broker") + assert dialect._get_default_schema() == "broker" + + def test_returns_first_entry_of_search_path(self): + dialect = PostgresDialect() + dialect._config = make_config(search_path="broker, public") + assert dialect._get_default_schema() == "broker" + + def test_returns_schema_from_backend(self): + dialect = PostgresDialect() + backend = PostgresBackend(connection_config=make_config(search_path="warehouse")) + dialect._backend = backend + assert dialect._get_default_schema() == "warehouse" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_walinspect_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_walinspect_integration.py index 1b54c6b3..c23576c7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_walinspect_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_walinspect_integration.py @@ -115,14 +115,21 @@ def test_pg_get_wal_records_info_with_lsn(self, pg_walinspect_env): opts = ExecutionOptions(stmt_type=StatementType.DQL) try: - # Get current WAL position to use as LSN values - current_lsn = _get_current_wal_lsn(backend, dialect) + # Get current WAL position as the range start. + start_lsn = _get_current_wal_lsn(backend, dialect) - # Query WAL records info with the obtained LSN + # Generate a small amount of WAL so the range is non-empty. + backend.execute("CREATE TABLE IF NOT EXISTS _wal_probe (id int)") + backend.execute("DROP TABLE IF EXISTS _wal_probe") + end_lsn = _get_current_wal_lsn(backend, dialect) + if start_lsn == end_lsn: + pytest.skip("No WAL activity generated between LSN reads") + + # Query WAL records info with the obtained LSN range wal_func = pg_get_wal_records_info( dialect, - current_lsn, - current_lsn, + start_lsn, + end_lsn, ) query = QueryExpression( dialect=dialect, @@ -136,7 +143,8 @@ def test_pg_get_wal_records_info_with_lsn(self, pg_walinspect_env): if ("permission denied" in error_msg or "must be superuser" in error_msg or "could not find a valid record" in error_msg - or "future start lsn" in error_msg): + or "future start lsn" in error_msg + or "must be less than" in error_msg): pytest.skip( "pg_walinspect requires superuser privileges or valid WAL range" ) @@ -193,14 +201,21 @@ async def test_async_pg_get_wal_records_info_with_lsn(self, async_pg_walinspect_ opts = ExecutionOptions(stmt_type=StatementType.DQL) try: - # Get current WAL position to use as LSN values - current_lsn = await _async_get_current_wal_lsn(backend, dialect) + # Get current WAL position as the range start. + start_lsn = await _async_get_current_wal_lsn(backend, dialect) + + # Generate a small amount of WAL so the range is non-empty. + await backend.execute("CREATE TABLE IF NOT EXISTS _wal_probe (id int)") + await backend.execute("DROP TABLE IF EXISTS _wal_probe") + end_lsn = await _async_get_current_wal_lsn(backend, dialect) + if start_lsn == end_lsn: + pytest.skip("No WAL activity generated between LSN reads") - # Query WAL records info with the obtained LSN + # Query WAL records info with the obtained LSN range wal_func = pg_get_wal_records_info( dialect, - current_lsn, - current_lsn, + start_lsn, + end_lsn, ) query = QueryExpression( dialect=dialect, @@ -214,7 +229,8 @@ async def test_async_pg_get_wal_records_info_with_lsn(self, async_pg_walinspect_ if ("permission denied" in error_msg or "must be superuser" in error_msg or "could not find a valid record" in error_msg - or "future start lsn" in error_msg): + or "future start lsn" in error_msg + or "must be less than" in error_msg): pytest.skip( "pg_walinspect requires superuser privileges or valid WAL range" ) 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 new file mode 100644 index 00000000..335b57e9 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py @@ -0,0 +1,154 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py +""" +Unit tests for PostgresConnectionConfig autocommit option. + +The backend reads ``config.autocommit`` to decide whether an explicit COMMIT +should be issued after statements executed outside a managed transaction. +The field must therefore exist on the config (previously it was only read +via ``getattr`` with a fallback, and passing it raised a TypeError). + +Covers: + - Default value and explicit assignment of ``autocommit`` + - Sync/Async backend ``requires_manual_commit()`` reflecting the config +""" + +import pytest + +from rhosocial.activerecord.backend.impl.postgres import ( + AsyncPostgresBackend, + PostgresBackend, + PostgresConnectionConfig, +) + + +class FakeConnection: + def __init__(self): + self.commit_count = 0 + + def commit(self): + self.commit_count += 1 + + def rollback(self): + pass + + +class FakeAsyncConnection: + def __init__(self): + self.commit_count = 0 + + async def commit(self): + self.commit_count += 1 + + async def rollback(self): + pass + + +def make_config(**overrides) -> PostgresConnectionConfig: + params = dict( + host="localhost", + port=5432, + database="test", + username="postgres", + password="", + ) + params.update(overrides) + return PostgresConnectionConfig(**params) + + +class TestAutocommitConfig: + """autocommit must be a first-class config field.""" + + def test_default_autocommit_is_false(self): + config = make_config() + assert config.autocommit is False + + def test_autocommit_can_be_enabled(self): + config = make_config(autocommit=True) + assert config.autocommit is True + + def test_autocommit_not_in_connection_string(self): + """autocommit is not a libpq conninfo keyword, so the URI must not include it.""" + conn_str = make_config(autocommit=True).to_connection_string() + assert "autocommit" not in conn_str + + +class TestRequiresManualCommit: + """Backends expose requires_manual_commit() based on config.autocommit.""" + + def test_sync_backend_requires_commit_by_default(self): + backend = PostgresBackend(connection_config=make_config()) + assert backend.requires_manual_commit() is True + + def test_sync_backend_skips_commit_when_autocommit(self): + backend = PostgresBackend(connection_config=make_config(autocommit=True)) + assert backend.requires_manual_commit() is False + + @pytest.mark.asyncio + async def test_async_backend_requires_commit_by_default(self): + backend = AsyncPostgresBackend(connection_config=make_config()) + assert backend.requires_manual_commit() is True + + @pytest.mark.asyncio + async def test_async_backend_skips_commit_when_autocommit(self): + backend = AsyncPostgresBackend(connection_config=make_config(autocommit=True)) + assert backend.requires_manual_commit() is False + + +class TestAutoCommitHandlers: + """The backend auto-commit handlers honor config.autocommit.""" + + def test_handle_auto_commit_commits_when_disabled(self): + backend = PostgresBackend(connection_config=make_config()) + backend._connection = FakeConnection() + backend._handle_auto_commit() + assert backend._connection.commit_count == 1 + + def test_handle_auto_commit_skips_when_enabled(self): + backend = PostgresBackend(connection_config=make_config(autocommit=True)) + backend._connection = FakeConnection() + backend._handle_auto_commit() + assert backend._connection.commit_count == 0 + + def test_handle_auto_commit_noop_without_connection(self): + backend = PostgresBackend(connection_config=make_config()) + backend._handle_auto_commit() + + def test_handle_auto_commit_if_needed_commits_when_disabled(self): + backend = PostgresBackend(connection_config=make_config()) + backend._connection = FakeConnection() + backend._handle_auto_commit_if_needed() + assert backend._connection.commit_count == 1 + + def test_handle_auto_commit_if_needed_skips_when_enabled(self): + backend = PostgresBackend(connection_config=make_config(autocommit=True)) + backend._connection = FakeConnection() + backend._handle_auto_commit_if_needed() + assert backend._connection.commit_count == 0 + + @pytest.mark.asyncio + async def test_async_handle_auto_commit_commits_when_disabled(self): + backend = AsyncPostgresBackend(connection_config=make_config()) + backend._connection = FakeAsyncConnection() + await backend._handle_auto_commit() + assert backend._connection.commit_count == 1 + + @pytest.mark.asyncio + async def test_async_handle_auto_commit_skips_when_enabled(self): + backend = AsyncPostgresBackend(connection_config=make_config(autocommit=True)) + backend._connection = FakeAsyncConnection() + await backend._handle_auto_commit() + assert backend._connection.commit_count == 0 + + @pytest.mark.asyncio + async def test_async_handle_auto_commit_if_needed_commits_when_disabled(self): + backend = AsyncPostgresBackend(connection_config=make_config()) + backend._connection = FakeAsyncConnection() + await backend._handle_auto_commit_if_needed() + assert backend._connection.commit_count == 1 + + @pytest.mark.asyncio + async def test_async_handle_auto_commit_if_needed_skips_when_enabled(self): + backend = AsyncPostgresBackend(connection_config=make_config(autocommit=True)) + backend._connection = FakeAsyncConnection() + await backend._handle_auto_commit_if_needed() + assert backend._connection.commit_count == 0 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 new file mode 100644 index 00000000..ae7ae482 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py @@ -0,0 +1,226 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py +"""Column qualifier resolution under JOINs (regression tests). + +History: the query layer used to force ``alias=alias or table_name`` on every +JOIN range while field accessors baked ``"schema"."table"."column"`` into +Column expressions at access time. PostgreSQL forbids schema-qualified +references to aliased ranges, so plain join queries failed with +``invalid reference to FROM-clause entry``. + +The forced aliasing was removed (JoinQueryMixin / AsyncJoinQueryMixin); these +tests pin down the fixed behaviour: + +* R1 plain field accessors under JOIN must execute +* R2a mixed aliased/plain accessors must execute +* R3 pre-built predicates remain reusable across plain/join contexts +* R4 dynamic ``__table_name__`` / ``__schema_name__``: the supported usage — + change the override, then build expressions — produces fully consistent + DQL across FROM / WHERE / SELECT (qualifiers are captured when column + expressions are constructed; rebuild conditions after a change) +* Control: fully alias-consistent accessors keep working, and the generated + FROM carries no implicit aliases. +""" + +from typing import ClassVar, Optional + +import pytest + +from rhosocial.activerecord.backend.impl.postgres import ( + PostgresBackend, + PostgresConnectionConfig, +) +from rhosocial.activerecord.backend.options import ExecutionOptions +from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.base.field_proxy import FieldProxy +from rhosocial.activerecord.model import ActiveRecord + +RISK_SCHEMA = "tenant_qualified_ref" + + +class Author(ActiveRecord): + __table_name__ = "authors" + __schema_name__ = RISK_SCHEMA + __primary_key__ = "id" + c: ClassVar[FieldProxy] = FieldProxy() + + id: Optional[int] = None + name: str + active: Optional[bool] = None + + +class Book(ActiveRecord): + __table_name__ = "books" + __schema_name__ = RISK_SCHEMA + __primary_key__ = "id" + c: ClassVar[FieldProxy] = FieldProxy() + + id: Optional[int] = None + title: str + author_id: Optional[int] = None + + +@pytest.fixture +def risk_env(postgres_backend_single): + """Provision schema/tables, bind models to the scenario config, capture SQL. + + Yields a namespace with: + backend -- the fixture backend (raw SQL setup/teardown) + config -- connection config shared by the models + captured -- list of SQL strings executed by the models' backends + """ + backend = postgres_backend_single + ddl = ExecutionOptions(stmt_type=StatementType.DDL) + + backend.execute(f'DROP SCHEMA IF EXISTS "{RISK_SCHEMA}" CASCADE', options=ddl) + backend.execute(f'CREATE SCHEMA "{RISK_SCHEMA}"', options=ddl) + for table in ("authors", "authors_archive", "books"): + if table == "books": + cols = "id SERIAL PRIMARY KEY, title TEXT, author_id INTEGER" + else: + cols = "id SERIAL PRIMARY KEY, name TEXT, active BOOLEAN DEFAULT TRUE" + backend.execute(f'CREATE TABLE "{RISK_SCHEMA}".{table} ({cols})', options=ddl) + backend.execute( + f"INSERT INTO \"{RISK_SCHEMA}\".authors (name) VALUES ('a1'), ('a2')", + options=ExecutionOptions(stmt_type=StatementType.DML), + ) + backend.execute( + f"INSERT INTO \"{RISK_SCHEMA}\".authors_archive (name) VALUES ('old1')", + options=ExecutionOptions(stmt_type=StatementType.DML), + ) + + # A second schema for dynamic __schema_name__ switching tests. + backend.execute(f'DROP SCHEMA IF EXISTS "{RISK_SCHEMA}_alt" CASCADE', options=ddl) + backend.execute(f'CREATE SCHEMA "{RISK_SCHEMA}_alt"', options=ddl) + backend.execute( + f'CREATE TABLE "{RISK_SCHEMA}_alt".authors ' + f'(id SERIAL PRIMARY KEY, name TEXT, active BOOLEAN DEFAULT TRUE)', + options=ddl, + ) + backend.execute( + f"INSERT INTO \"{RISK_SCHEMA}_alt\".authors (name) VALUES ('alt1')", + options=ExecutionOptions(stmt_type=StatementType.DML), + ) + + config: PostgresConnectionConfig = backend.config + captured: list = [] + original_execute = PostgresBackend.execute + + def spying_execute(self, sql, params=None, **kwargs): + captured.append(sql) + return original_execute(self, sql, params, **kwargs) + + PostgresBackend.execute = spying_execute + try: + Author.configure(config, PostgresBackend) + Book.configure(config, PostgresBackend) + yield type("Env", (), {"backend": backend, "config": config, "captured": captured}) + finally: + PostgresBackend.execute = original_execute + Author.__table_name__ = "authors" + Author.__schema_name__ = RISK_SCHEMA + backend.execute(f'DROP SCHEMA IF EXISTS "{RISK_SCHEMA}" CASCADE', options=ddl) + backend.execute(f'DROP SCHEMA IF EXISTS "{RISK_SCHEMA}_alt" CASCADE', options=ddl) + + +class TestQualifiedReferenceContext: + """See module docstring for the risk catalogue.""" + + def test_join_with_plain_accessors(self, risk_env): + """R1: natural join form with unaliased field accessors.""" + rows = ( + Author.query() + .join(Book, on=Author.c.id == Book.c.author_id) + .select(Author.c.name) + .all() + ) + assert isinstance(rows, list) + + def test_join_with_partial_aliases(self, risk_env): + """R2a: mixing a table-aliased accessor with plain accessors.""" + rows = ( + Author.query() + .join(Book, on=Author.c.id == Book.c.with_table_alias("books").author_id) + .select(Author.c.name.as_("n")) + .all() + ) + assert isinstance(rows, list) + + def test_prebuilt_predicate_reuse_under_join(self, risk_env): + """R3: predicates stay valid when reused across query contexts.""" + predicate = Author.c.active == True # noqa: E712 + assert len(Author.query().where(predicate).all()) == 2 + rows = ( + Author.query() + .join(Book, on=Author.c.id == Book.c.author_id) + .where(predicate) + .all() + ) + assert isinstance(rows, list) + + def test_explicit_alias_join(self, risk_env): + """Explicit join aliases keep working via matching accessors.""" + rows = ( + Author.query() + .join(Book, on=Author.c.id == Book.c.with_table_alias("b").author_id, alias="b") + .select(Author.c.name.as_("n")) + .all() + ) + assert isinstance(rows, list) + + def test_dynamic_table_name_full_dql_consistency(self, risk_env): + """R4: dynamic __table_name__ used the supported way. + + Change the override first, then build expressions — field proxies + pick up the new name and FROM / WHERE / SELECT all stay consistent. + """ + Author.__table_name__ = "authors_archive" + try: + rows = Author.query().where(Author.c.active == True).all() # noqa: E712 + assert len(rows) == 1 + select_sql = next( + (s for s in risk_env.captured + if s.startswith("SELECT") and "authors_archive" in s), + "", + ) + assert 'FROM "tenant_qualified_ref"."authors_archive"' in select_sql + assert '"authors_archive"."active"' in select_sql + finally: + Author.__table_name__ = "authors" + + def test_dynamic_schema_switch_full_dql_consistency(self, risk_env): + """R4: same contract for dynamic __schema_name__ (tenant switching).""" + Author.__schema_name__ = f"{RISK_SCHEMA}_alt" + try: + rows = Author.query().where(Author.c.name == "alt1").all() + assert len(rows) == 1 and rows[0].name == "alt1" + select_sql = next( + (s for s in risk_env.captured if s.startswith("SELECT") and "alt" in s), + "", + ) + assert f'FROM "{RISK_SCHEMA}_alt"."authors"' in select_sql + finally: + Author.__schema_name__ = RISK_SCHEMA + + def test_baseline_plain_query_with_predicate(self, risk_env): + """Sanity: prebuilt predicates work in their construction context.""" + predicate = Author.c.active == True # noqa: E712 + rows = Author.query().where(predicate).all() + assert len(rows) == 2 + + def test_control_fully_alias_consistent(self, risk_env): + """Control: FROM ranges carry no implicit aliases; references resolve.""" + rows = ( + Author.query() + .join(Book, on=Author.c.with_table_alias("authors").id + == Book.c.with_table_alias("books").author_id) + .select(Author.c.with_table_alias("authors").name.as_("n")) + .all() + ) + assert isinstance(rows, list) + select_sql = next((s for s in risk_env.captured if s.startswith("SELECT")), "") + # Schema-qualified FROM ranges, no forced AS aliases ... + assert 'FROM "tenant_qualified_ref"."authors"' in select_sql + assert 'JOIN "tenant_qualified_ref"."books"' in select_sql + assert " AS " not in select_sql.split("FROM", 1)[1] + # ... and table-name-qualified column references. + assert '"authors"."id"' in select_sql and '"books"."author_id"' in select_sql 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 e3215a19..639e509e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py @@ -1,348 +1,228 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py +"""Offline adapter round-trip coverage for the PostgreSQL backend.""" +import uuid +from decimal import Decimal +from typing import Any, Tuple -import ipaddress -import json # noqa: F401 import pytest -from psycopg.types.json import Jsonb - -from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJSONBAdapter, \ - PostgresNetworkAddressAdapter - -# Tests for PostgresJSONBAdapter -def test_jsonb_adapter_supported_types(): - adapter = PostgresJSONBAdapter() - supported = adapter.supported_types - assert dict in supported - assert Jsonb in supported[dict] - - -def test_jsonb_to_database(): - adapter = PostgresJSONBAdapter() - my_dict = {"a": 1, "b": [2, 3]} - my_list = [1, "test", {"c": 4}] - - # Test with dict - result_dict = adapter.to_database(my_dict, dict) - assert isinstance(result_dict, Jsonb) - assert result_dict.obj == my_dict - - # Test with list - result_list = adapter.to_database(my_list, list) - assert isinstance(result_list, Jsonb) - assert result_list.obj == my_list - - # Test with None - assert adapter.to_database(None, dict) is None - - -def test_jsonb_from_database(): - adapter = PostgresJSONBAdapter() - my_dict = {"key": "value"} - my_list = ["item1", "item2"] - json_string = '{"key": "value_from_string"}' - - # Test with value already being a dict or list - assert adapter.from_database(my_dict, dict) is my_dict - assert adapter.from_database(my_list, list) is my_list - - # Test with a JSON string representation - result = adapter.from_database(json_string, dict) - assert isinstance(result, dict) - assert result["key"] == "value_from_string" - - # Test with None - assert adapter.from_database(None, dict) is None - - -# Tests for PostgresNetworkAddressAdapter -def test_network_adapter_supported_types(): - adapter = PostgresNetworkAddressAdapter() - supported = adapter.supported_types - assert ipaddress.IPv4Address in supported - assert str in supported[ipaddress.IPv4Address] - assert ipaddress.IPv6Address in supported - assert str in supported[ipaddress.IPv6Address] - assert ipaddress.IPv4Network in supported - assert str in supported[ipaddress.IPv4Network] - assert ipaddress.IPv6Network in supported - assert str in supported[ipaddress.IPv6Network] - - - -@pytest.mark.parametrize("ip_obj, expected_str", [ - (ipaddress.ip_address("192.168.1.1"), "192.168.1.1"), - (ipaddress.ip_address("2001:db8::1"), "2001:db8::1"), - (ipaddress.ip_network("192.168.0.0/24"), "192.168.0.0/24"), - (ipaddress.ip_network("2001:db8::/32"), "2001:db8::/32"), -]) -def test_network_to_database(ip_obj, expected_str): - adapter = PostgresNetworkAddressAdapter() - result = adapter.to_database(ip_obj, type(ip_obj)) - assert result == expected_str - - -def test_network_to_database_none(): - adapter = PostgresNetworkAddressAdapter() - assert adapter.to_database(None, ipaddress.IPv4Address) is None - - -@pytest.mark.parametrize("ip_str, expected_type", [ - ("192.168.1.1", ipaddress.IPv4Address), - ("2001:db8::1", ipaddress.IPv6Address), - ("192.168.0.0/24", ipaddress.IPv4Network), - ("2001:db8::/32", ipaddress.IPv6Network), -]) -def test_network_from_database(ip_str, expected_type): - adapter = PostgresNetworkAddressAdapter() - result = adapter.from_database(ip_str, expected_type) - assert isinstance(result, expected_type) - assert str(result) == ip_str - - -def test_network_from_database_fallback(): - """Test fallback behavior for invalid network strings.""" - adapter = PostgresNetworkAddressAdapter() - invalid_str = "not-an-ip" - result = adapter.from_database(invalid_str, str) - assert result == invalid_str - - -def test_network_from_database_none(): - adapter = PostgresNetworkAddressAdapter() - assert adapter.from_database(None, ipaddress.IPv4Address) is None - - -# ============================================================================= -# Batch Operation Tests -# ============================================================================= - -from rhosocial.activerecord.backend.impl.postgres.adapters.base import ( # noqa: E402 - PostgresListAdapter, - PostgresJSONBAdapter, # noqa: F811 - PostgresNetworkAddressAdapter, # noqa: F811 +from rhosocial.activerecord.backend.impl.postgres.adapters.bit_string import PostgresBitStringAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.hstore import PostgresHstoreAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJSONBAdapter, PostgresJsonPathAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.pg_lsn import PostgresLsnAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.monetary import PostgresMoneyAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.network_address import PostgresMacaddrAdapter, PostgresNetworkAddressAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.object_identifier import PostgresOidAdapter, PostgresTidAdapter, PostgresXidAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.text_search import PostgresTsQueryAdapter, PostgresTsVectorAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.uuid import PostgresUUIDAdapter +from rhosocial.activerecord.backend.impl.postgres.adapters.xml import PostgresXMLAdapter +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 ( + PostgresRange as PG_RANGE, + PostgresMultirange as PG_MULTIRANGE, ) -class TestPostgresListAdapterBatch: - """Test PostgresListAdapter batch methods.""" - - def test_to_database_batch_pass_through(self): - """Test that to_database_batch returns the same list (pass-through).""" - adapter = PostgresListAdapter() - test_data = [[1, 2, 3], [4, 5, 6], None, [7, 8, 9]] - result = adapter.to_database_batch(test_data, list) - # Should be the same object (no copy) - assert result is test_data - - def test_to_database_batch_empty_list(self): - """Test batch conversion with empty list.""" - adapter = PostgresListAdapter() - result = adapter.to_database_batch([], list) - assert result == [] - - def test_from_database_batch_pass_through(self): - """Test that from_database_batch returns the same list.""" - adapter = PostgresListAdapter() - test_data = [[1, 2], [3, 4], None] - result = adapter.from_database_batch(test_data, list) - assert result is test_data - - def test_from_database_batch_empty_list(self): - """Test batch conversion from database with empty list.""" - adapter = PostgresListAdapter() - result = adapter.from_database_batch([], list) - assert result == [] - - -class TestPostgresJSONBAdapterBatch: - """Test PostgresJSONBAdapter batch methods.""" - - def test_to_database_batch(self): - """Test batch conversion to database format.""" - adapter = PostgresJSONBAdapter() - test_data = [ - {'a': 1}, - {'b': 2}, - None, - {'c': [1, 2, 3]} - ] - result = adapter.to_database_batch(test_data, dict) - - assert len(result) == 4 - assert isinstance(result[0], Jsonb) - assert result[0].obj == {'a': 1} - assert isinstance(result[1], Jsonb) - assert result[1].obj == {'b': 2} - assert result[2] is None - assert isinstance(result[3], Jsonb) - assert result[3].obj == {'c': [1, 2, 3]} - - def test_to_database_batch_empty_list(self): - """Test batch conversion with empty list.""" - adapter = PostgresJSONBAdapter() - result = adapter.to_database_batch([], dict) - assert result == [] - - def test_from_database_batch_to_dict(self): - """Test batch conversion from database to dict.""" - adapter = PostgresJSONBAdapter() - test_data = [ - {'a': 1}, - None, - {'b': 2} - ] - result = adapter.from_database_batch(test_data, dict) - - assert len(result) == 3 - assert result[0] == {'a': 1} - assert result[1] is None - assert result[2] == {'b': 2} - - def test_from_database_batch_to_str(self): - """Test batch conversion from database to JSON string.""" - adapter = PostgresJSONBAdapter() - test_data = [ - {'a': 1}, - None, - {'b': [2, 3]} - ] - result = adapter.from_database_batch(test_data, str) - - assert len(result) == 3 - assert result[0] == '{"a": 1}' - assert result[1] is None - assert result[2] == '{"b": [2, 3]}' - - def test_from_database_batch_empty_list(self): - """Test batch conversion from database with empty list.""" - adapter = PostgresJSONBAdapter() - result = adapter.from_database_batch([], dict) - assert result == [] - - -class TestPostgresNetworkAddressAdapterBatch: - """Test PostgresNetworkAddressAdapter batch methods.""" - - def test_to_database_batch(self): - """Test batch conversion to database format.""" - adapter = PostgresNetworkAddressAdapter() - test_data = [ - ipaddress.IPv4Address('192.168.1.1'), - ipaddress.IPv6Address('::1'), - None, - ipaddress.IPv4Network('10.0.0.0/24') - ] - result = adapter.to_database_batch(test_data, str) - - assert len(result) == 4 - assert result[0] == '192.168.1.1' - assert result[1] == '::1' - assert result[2] is None - assert result[3] == '10.0.0.0/24' - - def test_to_database_batch_empty_list(self): - """Test batch conversion with empty list.""" - adapter = PostgresNetworkAddressAdapter() - result = adapter.to_database_batch([], str) - assert result == [] - - def test_from_database_batch(self): - """Test batch conversion from database.""" - adapter = PostgresNetworkAddressAdapter() - test_data = [ - '192.168.1.1', - None, - '10.0.0.0/24', - '::1' - ] - result = adapter.from_database_batch(test_data, str) - - assert len(result) == 4 - assert isinstance(result[0], ipaddress.IPv4Address) - assert str(result[0]) == '192.168.1.1' - assert result[1] is None - assert isinstance(result[2], ipaddress.IPv4Network) - assert str(result[2]) == '10.0.0.0/24' - assert isinstance(result[3], ipaddress.IPv6Address) - assert str(result[3]) == '::1' - - def test_from_database_batch_invalid_values(self): - """Test batch conversion with invalid values falls back to string.""" - adapter = PostgresNetworkAddressAdapter() - test_data = [ - '192.168.1.1', - 'not-an-ip', - '10.0.0.0/24' - ] - result = adapter.from_database_batch(test_data, str) - - assert len(result) == 3 - assert isinstance(result[0], ipaddress.IPv4Address) - assert result[1] == 'not-an-ip' # Falls back to original string - assert isinstance(result[2], ipaddress.IPv4Network) - - def test_from_database_batch_empty_list(self): - """Test batch conversion from database with empty list.""" - adapter = PostgresNetworkAddressAdapter() - result = adapter.from_database_batch([], str) - assert result == [] - - -class TestPostgresEnumAdapterBatch: - """Test PostgresEnumAdapter batch methods.""" - - def test_to_database_batch(self): - """Test batch conversion to database format.""" - from rhosocial.activerecord.backend.impl.postgres.adapters.base import PostgresEnumAdapter - from enum import Enum - - class TestEnum(Enum): - A = 1 - B = 2 - C = 3 - - adapter = PostgresEnumAdapter() - test_data = [ - TestEnum.A, - 'existing_string', - None, - TestEnum.C - ] - result = adapter.to_database_batch(test_data, str) - - assert len(result) == 4 - assert result[0] == 'A' - assert result[1] == 'existing_string' - assert result[2] is None - assert result[3] == 'C' - - def test_from_database_batch(self): - """Test batch conversion from database.""" - from rhosocial.activerecord.backend.impl.postgres.adapters.base import PostgresEnumAdapter - - adapter = PostgresEnumAdapter() - test_data = ['value1', None, 'value2'] - result = adapter.from_database_batch(test_data, str) - - assert len(result) == 3 - assert result[0] == 'value1' - assert result[1] is None - assert result[2] == 'value2' - - def test_from_database_batch_to_enum(self): - """Test batch conversion from database to Python Enum.""" - from rhosocial.activerecord.backend.impl.postgres.adapters.base import PostgresEnumAdapter - from enum import Enum - - class TestEnum(Enum): - A = 1 - B = 2 - - adapter = PostgresEnumAdapter() - test_data = ['A', 'B', None] - result = adapter.from_database_batch(test_data, str, {'enum_class': TestEnum}) - - assert len(result) == 3 - assert result[0] == TestEnum.A - assert result[1] == TestEnum.B - assert result[2] is None +@pytest.fixture +def uuid_a(): return PostgresUUIDAdapter() +@pytest.fixture +def jsonb(): return PostgresJSONBAdapter() +@pytest.fixture +def json_path(): return PostgresJsonPathAdapter() +@pytest.fixture +def range_a(): return PostgresRangeAdapter() +@pytest.fixture +def multi_range(): return PostgresMultirangeAdapter() +@pytest.fixture +def net_addr(): return PostgresNetworkAddressAdapter() +@pytest.fixture +def mac(): return PostgresMacaddrAdapter() +@pytest.fixture +def lsn(): return PostgresLsnAdapter() +@pytest.fixture +def money(): return PostgresMoneyAdapter() +@pytest.fixture +def hstore(): return PostgresHstoreAdapter() +@pytest.fixture +def tsvec(): return PostgresTsVectorAdapter() +@pytest.fixture +def tsquery(): return PostgresTsQueryAdapter() +@pytest.fixture +def xml_a(): return PostgresXMLAdapter() +@pytest.fixture +def bit(): return PostgresBitStringAdapter() +@pytest.fixture +def oid(): return PostgresOidAdapter() +@pytest.fixture +def tid(): return PostgresTidAdapter() +@pytest.fixture +def xid(): return PostgresXidAdapter() +@pytest.fixture +def list_a(): return PostgresListAdapter() +@pytest.fixture +def enum_a(): return PostgresEnumAdapter() +@pytest.fixture +def vec(): return PostgresVectorAdapter() + + +class TestUUID: + def test_roundtrip(self, uuid_a): + u = uuid.uuid4() + assert uuid_a.to_database(u, str) == str(u) + assert uuid_a.from_database(str(u), uuid.UUID) == u + def test_none(self, uuid_a): + assert uuid_a.to_database(None, str) is None + +class TestJSONB: + def test_to_database_returns_jsonb(self, jsonb): + val = {"k": 1, "v": [2, 3]} + result = jsonb.to_database(val, str) + assert str(result) # serializable to JSON string + +class TestJsonPath: + def test_roundtrip(self, json_path): + p = '$.store.book[*].author' + assert json_path.from_database(p, str) == p + +class TestRange: + def test_to_database(self, range_a): + r = PG_RANGE(1, 10, lower_inc=True, upper_inc=False) + s = r.to_postgres_string() + assert isinstance(s, str) + assert range_a.to_database(r, str) == s + +class TestMultirange: + def test_to_database(self, multi_range): + mr = PG_MULTIRANGE([]) + assert multi_range.to_database(mr, str) is not None + +class TestNetworkAddress: + def test_inet_roundtrip(self, net_addr): + assert net_addr.to_database("192.168.1.1", str) == "192.168.1.1" + parsed = net_addr.from_database("192.168.1.1", str) + assert str(parsed) == "192.168.1.1" # IPv4Address/IPInterface repr + def test_none(self, net_addr): + assert net_addr.to_database(None, str) is None + +class TestMacaddr: + def test_roundtrip(self, mac): + m = "08:00:2b:01:02:03" + assert mac.to_database(m, str) == m + assert mac.from_database(m, str) == m + +class TestLsn: + def test_roundtrip(self, lsn): + s = "0/3000000" + assert lsn.from_database(s, str) == s + +class TestHstore: + def test_roundtrip(self, hstore): + d = {"key": "val", "k2": "v2"} + s = hstore.to_database(d, str) + assert isinstance(s, str) + assert hstore.from_database(s, dict) == d + +class TestTsVector: + def test_from_database(self, tsvec): + result = tsvec.from_database("hello world", str) + assert result is not None + assert hasattr(result, "lexemes") + +class TestTsQuery: + def test_from_database(self, tsquery): + result = tsquery.from_database("hello & world", str) + assert result is not None + +class TestXML: + def test_roundtrip(self, xml_a): + s = "1" + assert xml_a.from_database(s, str) == s + +class TestBitString: + def test_roundtrip(self, bit): + s = "101010" + assert bit.from_database(s, str) == s + +class TestOid: + def test_roundtrip(self, oid): + assert oid.from_database(42, int) == 42 + +class TestXid: + def test_roundtrip(self, xid): + assert xid.from_database(123, int) == 123 + +class TestTid: + def test_roundtrip(self, tid): + s = "(0,1)" + assert tid.from_database(s, str) == s + +class TestMoney: + def test_roundtrip(self, money): + assert money.from_database(Decimal("100.50"), Decimal) == Decimal("100.50") + def test_none(self, money): + assert money.from_database(None, Decimal) is None + +class TestEnum: + def test_roundtrip(self, enum_a): + s = "draft" + assert enum_a.to_database(s, str) == s + assert enum_a.from_database(s, str) == s + +class TestList: + def test_to_database(self, list_a): + arr = [1, 2, 3] + assert list_a.to_database(arr, list) == arr + + def test_roundtrip(self, list_a): + arr = [1, 2, 3] + assert list_a.from_database(arr, list) == arr + + def test_from_database_json_string(self, list_a): + assert list_a.from_database("[1, 2, 3]", list) == [1, 2, 3] + + def test_from_database_json_string_with_whitespace(self, list_a): + assert list_a.from_database(' ["a", "b"] ', list) == ["a", "b"] + + def test_from_database_pg_array_literal(self, list_a): + assert list_a.from_database("{1,2,3}", list) == ["1", "2", "3"] + + def test_from_database_pg_array_literal_quoted(self, list_a): + assert list_a.from_database('{"a","b c"}', list) == ["a", "b c"] + + def test_from_database_pg_array_literal_null(self, list_a): + assert list_a.from_database("{1,NULL,3}", list) == ["1", None, "3"] + + def test_from_database_invalid_string_raises(self, list_a): + with pytest.raises(TypeError, match="Cannot convert str to list"): + list_a.from_database("not a list", list) + + def test_from_database_invalid_json_string_fallthrough(self, list_a): + with pytest.raises(TypeError, match="Cannot convert str to list"): + list_a.from_database("[1, 2", list) + + def test_from_database_invalid_array_literal_fallthrough(self, list_a): + with pytest.raises(TypeError, match="Cannot convert str to list"): + list_a.from_database("{}}", list) # extra brace triggers DataError + + def test_from_database_non_string_raises(self, list_a): + with pytest.raises(TypeError, match="Cannot convert int to list"): + list_a.from_database(123, list) + + def test_parse_array_literal_invalid_raises(self): + with pytest.raises(ValueError, match="Not a PostgreSQL array literal"): + PostgresListAdapter._parse_array_literal("not-an-array") + + def test_to_database_batch(self, list_a): + assert list_a.to_database_batch([[1], [2, 3]], list) == [[1], [2, 3]] + + def test_from_database_batch(self, list_a): + assert list_a.from_database_batch([[1], [2, 3]], list) == [[1], [2, 3]] + +class TestVector: + def test_roundtrip(self, vec): + v = [1.0, 2.5, 3.0] + s = vec.to_database(v, str) + assert isinstance(s, str) + assert vec.from_database(s, list) == v \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py new file mode 100644 index 00000000..840f5139 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py @@ -0,0 +1,83 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py +"""Regression tests for DDL compilation of auto-increment, standard types, +boolean defaults, and timestamps. + +Covers bugs found in rbac cross-backend testing: +- is_auto_increment=True on PRIMARY_KEY must produce GENERATED BY DEFAULT + AS IDENTITY (was missing — caused INSERT id=NULL NOT NULL violation). +- format_create_table_statement must return a string for all types. +- BooleanType with DEFAULT True must be accepted. +- TimestampType must map to TIMESTAMP. +""" + +import pytest + +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + BooleanType, IntegerType, TextType, TimestampType, VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +@pytest.fixture +def dialect(): + return PostgresDialect() + + +def _pk(): + return [ColumnConstraint(ColumnConstraintType.PRIMARY_KEY, is_auto_increment=True)] + + +def _build_table(dialect): + return CreateTableExpression( + dialect=dialect, table="test_tbl", + columns=[ + ColumnDefinition("id", IntegerType(), constraints=_pk()), + ColumnDefinition("name", TextType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("flag", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition("code", VarCharType(16)), + ColumnDefinition("created_at", TimestampType()), + ], + ) + + +class TestAutoIncrementDDL: + + def test_auto_increment_in_ddl(self, dialect): + sql, _ = dialect.format_create_table_statement(_build_table(dialect)) + assert "GENERATED BY DEFAULT AS IDENTITY" in sql.upper(), \ + f"auto-increment missing: {sql}" + + def test_all_standard_types_compile(self, dialect): + sql, params = dialect.format_create_table_statement(_build_table(dialect)) + assert isinstance(sql, str) + assert isinstance(params, tuple) + + def test_boolean_default_true(self, dialect): + expr = CreateTableExpression( + dialect=dialect, table="bool_test", + columns=[ + ColumnDefinition("id", IntegerType(), constraints=_pk()), + ColumnDefinition("flag", BooleanType(), + constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), + ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ], + ) + sql, _ = dialect.format_create_table_statement(expr) + assert "BOOLEAN" in sql.upper() + + def test_timestamp_type(self, dialect): + expr = CreateTableExpression( + dialect=dialect, table="ts_test", + columns=[ + ColumnDefinition("id", IntegerType(), constraints=_pk()), + ColumnDefinition("created_at", TimestampType()), + ], + ) + sql, _ = dialect.format_create_table_statement(expr) + assert "TIMESTAMP" in sql.upper() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli_connection_args.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli_connection_args.py new file mode 100644 index 00000000..fccee4ec --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli_connection_args.py @@ -0,0 +1,46 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli_connection_args.py +"""Offline tests for ``resolve_connection_config_from_args`` (SSL mapping, etc.).""" +import argparse + +from rhosocial.activerecord.backend.impl.postgres.cli.connection import ( + add_connection_args, + resolve_connection_config_from_args, +) + + +def _parse(extra=None): + parser = argparse.ArgumentParser() + add_connection_args(parser) + return parser.parse_args(extra or []) + + +class TestCLISSLMapping: + """SSL parameter mapping in resolve_connection_config_from_args.""" + + def test_default_ssl_maps_to_prefer(self): + config = resolve_connection_config_from_args(_parse()) + assert config.sslmode == "prefer" + + def test_ssl_auto(self): + config = resolve_connection_config_from_args(_parse(["--ssl", "auto"])) + assert config.sslmode == "prefer" + + def test_ssl_require(self): + config = resolve_connection_config_from_args(_parse(["--ssl", "require"])) + assert config.sslmode == "require" + + def test_ssl_verify_ca(self): + config = resolve_connection_config_from_args(_parse(["--ssl", "verify-ca"])) + assert config.sslmode == "verify-ca" + + def test_ssl_verify_full(self): + config = resolve_connection_config_from_args(_parse(["--ssl", "verify-full"])) + assert config.sslmode == "verify-full" + + def test_ssl_disabled(self): + config = resolve_connection_config_from_args(_parse(["--ssl", "disabled"])) + assert config.sslmode == "disable" + + def test_is_async_flag(self): + assert _parse(["--async"]).is_async is True + assert _parse([]).is_async is False \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py new file mode 100644 index 00000000..9234a7a3 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py @@ -0,0 +1,80 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py +""" +Functional serialization coverage for PostgreSQL expression classes. + +Every expression class defined in ``rhosocial.activerecord.backend.impl +.postgres.expression`` must round-trip losslessly through dict / JSON / XML +encodings, and produce identical ``to_sql()`` where the PostgreSQL dialect +supports it. +""" + +import pytest + +from rhosocial.activerecord.testsuite.utils.expression import ( + collect_expression_classes, + make_instance, + register_all, + register_special_constructor, + roundtrip_expression, + sql_consistent, +) + +PG_EXPR_PKG = "rhosocial.activerecord.backend.impl.postgres.expression" + +CLASSES = collect_expression_classes(PG_EXPR_PKG) +register_all(CLASSES) + + +def _register_postgres_specials(): + from rhosocial.activerecord.backend.expression.core import Column, Literal + + def for_update(d): + from rhosocial.activerecord.backend.impl.postgres.expression.locking import ( + PostgresForUpdateExpression, + ) + return PostgresForUpdateExpression(d, columns=[Column(d, "id")]) + + def advisory(d): + from rhosocial.activerecord.backend.impl.postgres.expression.advisory.lock import ( + PostgresAdvisoryLockExpression, + ) + return PostgresAdvisoryLockExpression(d, key=1) + + register_special_constructor("locking.PostgresForUpdateExpression", for_update) + register_special_constructor("advisory.lock.PostgresAdvisoryLockExpression", advisory) + + +_register_postgres_specials() + + +@pytest.fixture(params=[fqn for fqn in sorted(CLASSES)], ids=sorted(CLASSES)) +def postgres_expr_case(request, postgres_dialect): + fqn = request.param + cls = CLASSES[fqn] + instance, source = make_instance(cls, postgres_dialect) + if instance is None: + pytest.skip(f"{fqn}: {source}") + return fqn, instance + + +class TestPostgresExpressionRoundtrip: + """All constructible PostgreSQL expression classes round-trip losslessly.""" + + def test_get_params_roundtrip(self, postgres_expr_case, postgres_dialect): + fqn, instance = postgres_expr_case + roundtrip_expression(fqn, instance, postgres_dialect) + + def test_to_sql_consistent(self, postgres_expr_case, postgres_dialect): + fqn, instance = postgres_expr_case + sql_consistent(fqn, instance, postgres_dialect) + + +def test_core_expressions_also_roundtrip(postgres_dialect): + from rhosocial.activerecord.backend.expression.core import Column, Literal + from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate + + expr = ComparisonPredicate( + postgres_dialect, "=", Column(postgres_dialect, "a"), Literal(postgres_dialect, 1) + ) + roundtrip_expression("core", expr, postgres_dialect) + sql_consistent("core", expr, postgres_dialect) \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py index dca80715..bd13795b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py @@ -494,4 +494,64 @@ def test_format_identifier_naive_vs_proper_malicious(dialect): def test_format_identifier_empty_string(dialect): """Empty identifier produces empty double quotes.""" - assert dialect.format_identifier("") == '""' \ No newline at end of file + assert dialect.format_identifier("") == '""' + + +# ── format_binary_operator % escaping ───────────────────────────────── + + +def test_format_binary_operator_percent_escaped(): + """format_binary_operator escapes % to %% for psycopg compatibility.""" + from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + d = PostgresDialect((16, 0, 0)) + + # % operator → %% (psycopg requires %% for literal %) + sql, params = d.format_binary_operator("%", "a", "b", (), ()) + assert sql == "a %% b", f"Expected escaped %%, got: {sql}" + assert params == () + + # %% is pre-escaped → should NOT be double-escaped to %%%% + # (callers must pass raw operator, not pre-escaped) + sql, params = d.format_binary_operator("%%", "a", "b", (), ()) + assert sql == "a %%%% b", f"raw %% should be escaped to %%%%, got: {sql}" + + # Operator without % → unchanged + sql, params = d.format_binary_operator("=", "a", "b", (), ()) + assert sql == "a = b" + assert params == () + + # ? operator (hstore/jsonb) → preserved as-is, not treated as placeholder + sql, params = d.format_binary_operator("?", "data", "%s", (), ("key",)) + assert sql == "data ? %s", f"? operator preserved: {sql}" + assert params == ("key",) + + # ?| and ?& operators → preserved + sql, params = d.format_binary_operator("?|", "data", "%s", (), ("key",)) + assert sql == "data ?| %s", f"?| operator preserved: {sql}" + + # %# (pg_trgm) → %# (unchanged, no % to escape) + sql, params = d.format_binary_operator("%#", "a", "b", (), ()) + assert sql == "a %%# b", f"%# operator: {sql}" + + +def test_hstore_operator_not_double_escaped(): + """hstore_to_array_operator uses raw %, not pre-escaped %%.""" + hstore_path = "src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py" + # Try to find the file relative to the repo root + import os + for candidate in [ + os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "..", "..", hstore_path), + os.path.join(os.getcwd(), hstore_path), + ]: + candidate = os.path.normpath(candidate) + if os.path.exists(candidate): + hstore_path = candidate + break + with open(hstore_path) as f: + src = f.read() + assert ( + "BinaryExpression(" in src and '"%",' in src + ), "hstore operator must pass raw % (not pre-escaped %%)" + import re + for m in re.finditer(r'BinaryExpression\([^)]*?["\']%%["\']', src): + pytest.fail(f"Found pre-escaped %% operator: {m.group()[:80]}") \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py index c7c286f9..ef74389f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py @@ -86,6 +86,8 @@ PostgresTsTzRangeType, PostgresUUIDType, PostgresVarBitType, + PostgresHalfvecType, + PostgresSparsevecType, PostgresVectorType, PostgresXID8Type, PostgresXIDType, @@ -152,6 +154,8 @@ def dialect(): (PostgresGeometryType(), "GEOMETRY"), (PostgresGeographyType(), "GEOGRAPHY"), (PostgresVectorType(dim=384), "VECTOR(384)"), + (PostgresHalfvecType(dim=128), "HALFVEC(128)"), + (PostgresSparsevecType(dim=16), "SPARSEVEC(16)"), (PostgresCitextType(), "CITEXT"), (PostgresCubeType(), "CUBE"), (PostgresLtreeType(), "LTREE"), @@ -346,6 +350,10 @@ def test_format_unregistered_type_raises(dialect): ("GEOMETRY", PostgresGeometryType, {}), ("VECTOR", PostgresVectorType, {"dim": 0}), ("VECTOR(768)", PostgresVectorType, {"dim": 768}), + ("HALFVEC", PostgresHalfvecType, {"dim": 0}), + ("HALFVEC(128)", PostgresHalfvecType, {"dim": 128}), + ("SPARSEVEC", PostgresSparsevecType, {"dim": 0}), + ("SPARSEVEC(16)", PostgresSparsevecType, {"dim": 16}), ] @@ -412,6 +420,54 @@ def test_parse_type_case_insensitive(dialect): assert result.length == 25 +def test_parse_vector_requires_extension(dialect): + """parse_type must reject pgvector types when the extension is not installed.""" + from rhosocial.activerecord.backend.impl.postgres.protocols.base import ( + PostgresExtensionInfo, + ) + dialect._extensions = { + "vector": PostgresExtensionInfo(name="vector", installed=False, available=True) + } + with pytest.raises(RuntimeError, match="vector.*extension"): + dialect.parse_type("VECTOR(3)") + with pytest.raises(RuntimeError, match="vector.*extension"): + dialect.parse_type("HALFVEC(3)") + with pytest.raises(RuntimeError, match="vector.*extension"): + dialect.parse_type("SPARSEVEC(3)") + + +def test_parse_vector_allowed_when_extension_installed(dialect): + from rhosocial.activerecord.backend.impl.postgres.protocols.base import ( + PostgresExtensionInfo, + ) + dialect._extensions = { + "vector": PostgresExtensionInfo(name="vector", installed=True) + } + assert type(dialect.parse_type("VECTOR(3)")) is PostgresVectorType + assert type(dialect.parse_type("HALFVEC(3)")) is PostgresHalfvecType + assert type(dialect.parse_type("SPARSEVEC(3)")) is PostgresSparsevecType + + +def test_vector_types_equality_and_hash(): + assert PostgresVectorType(3) == PostgresVectorType(3) + assert PostgresVectorType(3) != PostgresVectorType(4) + assert PostgresVectorType(3) != PostgresHalfvecType(3) + assert PostgresVectorType(3) != object() + assert hash(PostgresVectorType(3)) == hash(PostgresVectorType(3)) + + assert PostgresHalfvecType(3) == PostgresHalfvecType(3) + assert PostgresHalfvecType(3) != PostgresHalfvecType(4) + assert PostgresHalfvecType(3) != PostgresSparsevecType(3) + assert PostgresHalfvecType(3) != object() + assert hash(PostgresHalfvecType(3)) == hash(PostgresHalfvecType(3)) + + assert PostgresSparsevecType(5) == PostgresSparsevecType(5) + assert PostgresSparsevecType(5) != PostgresSparsevecType(6) + assert PostgresSparsevecType(5) != PostgresVectorType(5) + assert PostgresSparsevecType(5) != object() + assert hash(PostgresSparsevecType(5)) == hash(PostgresSparsevecType(5)) + + # --------------------------------------------------------------------------- # supports_data_types # --------------------------------------------------------------------------- diff --git a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py index 0d4b8bd2..7935cb2a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py @@ -91,7 +91,14 @@ def get_scenario_config(name: str) -> Dict[str, Any]: name = next(iter(SCENARIO_MAP)) else: raise ValueError("No scenarios registered or enabled") - return SCENARIO_MAP[name] + config_dict = SCENARIO_MAP[name].copy() + # Apply the pooled database name so parallel workers never share a schema. + from providers.pooling import resolve_database_name + + pooled_db = resolve_database_name(name) + if pooled_db: + config_dict["database"] = pooled_db + return config_dict def get_scenario_names(): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/connection/test_connection_pool_concurrent.py b/tests/rhosocial/activerecord_postgres_test/feature/connection/test_connection_pool_concurrent.py index a3b41ee4..af16ce11 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/connection/test_connection_pool_concurrent.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/connection/test_connection_pool_concurrent.py @@ -39,47 +39,42 @@ def test_concurrent_connections_are_distinct(self, postgres_pool: BackendPool): simultaneously, each thread gets a different connection instance. """ num_threads = 5 - connection_ids: List[int] = [] - connection_ids_lock = threading.Lock() + held_ids: set = set() + held_lock = threading.Lock() + double_checkouts: List[int] = [] barrier = threading.Barrier(num_threads) errors: List[Exception] = [] errors_lock = threading.Lock() def acquire_and_record(thread_id: int): try: - # Synchronize all threads to start at the same time barrier.wait() - # Acquire connection backend = postgres_pool.acquire() try: conn_id = id(backend) - with connection_ids_lock: - connection_ids.append((thread_id, conn_id)) + with held_lock: + if conn_id in held_ids: + double_checkouts.append(conn_id) + held_ids.add(conn_id) - # Hold connection briefly to ensure overlap time.sleep(0.1) finally: + with held_lock: + held_ids.discard(id(backend)) postgres_pool.release(backend) except Exception as e: with errors_lock: errors.append(e) - # Run concurrent acquires with ThreadPoolExecutor(max_workers=num_threads) as executor: futures = [executor.submit(acquire_and_record, i) for i in range(num_threads)] for future in as_completed(futures): future.result() - # Verify no errors assert len(errors) == 0, f"Errors occurred: {errors}" - - # Verify all connections are distinct - assert len(connection_ids) == num_threads - unique_ids = set(conn_id for _, conn_id in connection_ids) - assert len(unique_ids) == num_threads, ( - f"Expected {num_threads} distinct connections, " - f"but got {len(unique_ids)}: {connection_ids}" + assert not double_checkouts, ( + f"Pool handed the same connection to two threads concurrently: {double_checkouts}" ) def test_concurrent_operations_no_deadlock(self, postgres_pool_with_tables: BackendPool): @@ -359,33 +354,32 @@ class TestAsyncBackendPoolConcurrent: async def test_concurrent_connections_are_distinct(self, async_postgres_pool: AsyncBackendPool): """Verify that concurrently acquired async connections are distinct.""" num_concurrent = 5 - connection_ids: List[int] = [] - connection_ids_lock = asyncio.Lock() + held_ids: set = set() + held_lock = asyncio.Lock() + double_checkouts: List[int] = [] start_event = asyncio.Event() async def acquire_and_record(task_id: int): - # Wait for all tasks to be ready await start_event.wait() async with async_postgres_pool.connection() as backend: conn_id = id(backend) - async with connection_ids_lock: - connection_ids.append((task_id, conn_id)) - - # Hold connection briefly - await asyncio.sleep(0.1) + async with held_lock: + if conn_id in held_ids: + double_checkouts.append(conn_id) + held_ids.add(conn_id) + try: + await asyncio.sleep(0.1) + finally: + async with held_lock: + held_ids.discard(conn_id) - # Start all tasks simultaneously tasks = [acquire_and_record(i) for i in range(num_concurrent)] - start_event.set() # Release all tasks at once + start_event.set() await asyncio.gather(*tasks) - # Verify all connections are distinct - assert len(connection_ids) == num_concurrent - unique_ids = set(conn_id for _, conn_id in connection_ids) - assert len(unique_ids) == num_concurrent, ( - f"Expected {num_concurrent} distinct connections, " - f"but got {len(unique_ids)}" + assert not double_checkouts, ( + f"Pool handed the same connection to two tasks concurrently: {double_checkouts}" ) @pytest.mark.asyncio diff --git a/tests/rhosocial/activerecord_test/feature/backend/cli/test_cli_blackbox.py b/tests/rhosocial/activerecord_test/feature/backend/cli/test_cli_blackbox.py new file mode 100644 index 00000000..210f89ad --- /dev/null +++ b/tests/rhosocial/activerecord_test/feature/backend/cli/test_cli_blackbox.py @@ -0,0 +1,103 @@ +# tests/rhosocial/activerecord_test/feature/backend/cli/test_cli_blackbox.py +"""Black-box CLI tests for the PostgreSQL backend (live scenario server).""" + +import io +import json +from contextlib import redirect_stderr, redirect_stdout + +import pytest + +from rhosocial.activerecord.backend.impl.postgres.__main__ import main +from providers.scenarios import get_scenario_raw + +import socket +COMMANDS = [ + "info", "query", "introspect", "status", + "named-expression", "named-procedure", "named-procedure-graph", + "named-migration", "named-connection", +] + + +@pytest.fixture(scope="module") +def conn_args(): + backend_cls, config = get_scenario_raw("postgres_exts_18") + _s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _s.settimeout(2) + try: + _s.connect((config.host, int(config.port))) + except OSError: + pytest.skip(f"Scenario server unreachable: {config.host}:{config.port}") + finally: + _s.close() + return [ + "--host", config.host, + "--port", str(config.port), + "--database", config.database, + "--user", config.username, + "--password", config.password, + ] + + +def run_cli(argv): + out, err = io.StringIO(), io.StringIO() + exc = None + with redirect_stdout(out), redirect_stderr(err): + try: + main(argv) + except SystemExit as e: + exc = e + return out.getvalue(), err.getvalue(), exc + + +class TestCommandSurface: + def test_help_lists_all_commands(self): + out, _, _ = run_cli(["--help"]) + for cmd in COMMANDS: + assert cmd in out + + def test_missing_command_errors(self): + _, _, exc = run_cli([]) + assert exc is not None and exc.code == 1 + + +class TestQuery: + def test_query_json(self, conn_args): + out, err, exc = run_cli(["query"] + conn_args + ["SELECT 1 AS one", "-o", "json"]) + assert exc is None, f"stderr: {err}\nstdout: {out}" + assert json.loads(out) == [{"one": 1}] + + def test_query_async(self, conn_args): + out, _, exc = run_cli(["query"] + conn_args + ["SELECT 1 AS one", "-o", "json", "--async"]) + assert exc is None + assert json.loads(out) == [{"one": 1}] + + +class TestStatus: + def test_status(self, conn_args): + out, err, exc = run_cli(["status"] + conn_args + ["-o", "json"]) + assert exc is None, f"stderr: {err}\nstdout: {out}" + assert json.loads(out) + + def test_status_async(self, conn_args): + out, err, exc = run_cli(["status"] + conn_args + ["-o", "json", "--async"]) + assert exc is None, f"stderr: {err}\nstdout: {out}" + assert json.loads(out) + + +class TestIntrospect: + def test_introspect(self, conn_args): + out, err, exc = run_cli(["introspect", "tables"] + conn_args + ["-o", "json"]) + assert exc is None, f"stderr: {err}\nstdout: {out}" + assert isinstance(json.loads(out), list) + + def test_introspect_async(self, conn_args): + out, err, exc = run_cli(["introspect", "tables"] + conn_args + ["-o", "json", "--async"]) + assert exc is None, f"stderr: {err}\nstdout: {out}" + assert isinstance(json.loads(out), list) + + +class TestInfo: + def test_info(self): + out, _, exc = run_cli(["info"]) + assert exc is None + assert "PostgreSQL" in out or "postgres" in out From cbd3cf20c450fae8c28a830aa76794311db45333 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 11 Sep 2026 01:40:02 +0800 Subject: [PATCH 03/22] feat(postgres): adapt to node-based expression system and declarative format_method - Backend types carry postgres_-prefixed generic names; dispatch by naming convention (format_data_type_); @DDLTypeMixin.handles registration removed. - Formatting functions take (self, expr); the ::type cast syntax is the CastExpression formatter. - Ownership of the expression source tree moves to the core library. - CI installs rhosocial-activerecord and the testsuite from their active branches. --- .github/workflows/test.yml | 16 +- .../backend/impl/postgres/dialect.py | 48 ++- .../impl/postgres/expression/advisory/lock.py | 52 +-- .../impl/postgres/expression/ddl/cluster.py | 12 +- .../impl/postgres/expression/ddl/collation.py | 24 +- .../impl/postgres/expression/ddl/comment.py | 12 +- .../impl/postgres/expression/ddl/domain.py | 36 +- .../impl/postgres/expression/ddl/extension.py | 24 +- .../postgres/expression/ddl/foreign_table.py | 24 +- .../impl/postgres/expression/ddl/index.py | 18 +- .../impl/postgres/expression/ddl/mv.py | 12 +- .../impl/postgres/expression/ddl/partition.py | 59 +-- .../postgres/expression/ddl/pg_partman.py | 47 +-- .../impl/postgres/expression/ddl/policy.py | 36 +- .../postgres/expression/ddl/publication.py | 48 +-- .../postgres/expression/ddl/rls_config.py | 24 +- .../impl/postgres/expression/ddl/routine.py | 48 +-- .../postgres/expression/ddl/statistics.py | 24 +- .../postgres/expression/ddl/table_settings.py | 12 +- .../impl/postgres/expression/ddl/type.py | 72 ++-- .../impl/postgres/expression/ddl/vacuum.py | 24 +- .../backend/impl/postgres/expression/types.py | 232 ++++++----- .../backend/impl/postgres/mixins/table.py | 2 +- .../mixins/types/data_type_formatting.py | 351 +++++++--------- .../impl/postgres/mixins/types/enum.py | 157 +++----- .../postgres/mixins/types/jsonb_enhanced.py | 4 - .../impl/postgres/protocols/types/enum.py | 63 ++- .../backend/impl/postgres/types/enum.py | 25 +- tests/providers/fixtures/basic.py | 378 +++++++++--------- tests/providers/fixtures/events.py | 46 +-- tests/providers/fixtures/mixins.py | 82 ++-- tests/providers/fixtures/query.py | 318 +++++++-------- tests/providers/partition.py | 10 +- .../dialect/test_alter_table_if_exists.py | 14 +- .../extensions/test_bloom_integration.py | 36 +- .../extensions/test_btree_gin_integration.py | 24 +- .../extensions/test_btree_gist_integration.py | 24 +- .../extensions/test_citext_integration.py | 92 ++--- .../extensions/test_cube_integration.py | 48 +-- .../test_earthdistance_integration.py | 16 +- .../test_fuzzystrmatch_integration.py | 32 +- .../extensions/test_hstore_integration.py | 14 +- .../extensions/test_hypopg_integration.py | 20 +- .../extensions/test_intarray_integration.py | 80 ++-- .../extensions/test_ltree_integration.py | 84 ++-- .../extensions/test_orafce_integration.py | 32 +- .../extensions/test_pg_trgm_integration.py | 64 +-- .../extensions/test_pgcrypto_integration.py | 36 +- .../extensions/test_pgrouting_integration.py | 44 +- .../extensions/test_pgvector_integration.py | 8 +- .../extensions/test_postgis_integration.py | 80 ++-- .../test_postgis_raster_integration.py | 20 +- .../extensions/test_tablefunc_integration.py | 44 +- .../extensions/test_uuid_ossp_integration.py | 36 +- .../postgres/test_core_type_mappings.py | 68 ++-- .../backend/postgres/test_ddl_alter_config.py | 7 +- .../backend/postgres/test_ddl_expressions.py | 38 +- .../test_dialect_security_integration.py | 1 + .../backend/postgres/test_enum_types.py | 17 +- .../feature/backend/postgres/test_index_pg.py | 7 +- .../test_property_graph_query_scenarios.py | 74 ++-- .../test_recursive_cte_graph_traversal.py | 46 +-- .../backend/postgres/test_type_casting.py | 28 +- .../backend/test_auto_increment_ddl.py | 32 +- .../feature/backend/test_create_table_like.py | 14 +- .../backend/test_expression_roundtrip_all.py | 2 +- .../test_partition_advanced_operations.py | 28 +- .../feature/backend/test_partition_explain.py | 16 +- .../backend/test_partition_operations.py | 16 +- .../backend/test_postgres_dialect_security.py | 45 ++- .../types/test_data_type_formatting.py | 16 +- .../basic/fields/test_derived_field.py | 12 +- .../basic/fields/test_derived_field_async.py | 12 +- .../feature/relation/fixtures/models.py | 28 +- 74 files changed, 1709 insertions(+), 1986 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1f9792b..2b5dc6c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,8 +68,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/parameter-inline" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl" - name: Create postgres scenario config run: | @@ -279,8 +279,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/parameter-inline" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl" - name: Create postgres scenario config run: | @@ -391,8 +391,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/parameter-inline" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl" - name: Create postgres scenario config run: | @@ -490,8 +490,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/parameter-inline" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/activerecord-derived-ddl" - name: Create postgres scenario config run: | diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 08cc6723..291ca007 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -676,7 +676,7 @@ def format_datetime_diff_expression(self, expr: "Any") -> Tuple[str, Tuple]: f"EXTRACT(YEAR FROM {start_sql}))" ) params = end_params + start_params - return self._apply_value_expression_modifiers(sql, params, expr) + return self.apply_alias(sql, params, expr) def validate_collation_name(self, expr: "CollateExpression") -> str: """Validate PostgreSQL collation names and return their SQL representation.""" @@ -846,9 +846,7 @@ def format_identifier(self, identifier: str) -> str: escaped = identifier.replace('"', '""') return f'"{escaped}"' - def format_column(self, name: str, table: Optional[str] = None, - alias: Optional[str] = None, - schema_name: Optional[str] = None) -> Tuple[str, Tuple]: + def format_column(self, expr) -> Tuple[str, Tuple]: """Format column reference for PostgreSQL. PostgreSQL rules for column references: @@ -860,6 +858,10 @@ def format_column(self, name: str, table: Optional[str] = None, full three-segment form: ``"schema"."table"."column"``. - Otherwise use the standard two-segment or single-segment form. """ + name = expr.name + table = expr.table + alias = expr.alias + schema_name = expr.schema_name if table: # In PostgreSQL, when a table has an alias, column references # must use the alias — schema_name is irrelevant in this context. @@ -1343,56 +1345,52 @@ def format_create_table_statement(self, expr: "CreateTableExpression") -> Tuple[ # endregion # region Type Casting Support (PostgreSQL-specific) - def format_cast_expression( - self, expr_sql: str, target_type: str, expr_params: tuple, alias: Optional[str] = None - ) -> Tuple[str, Tuple]: + def format_cast_expression(self, expr) -> Tuple[str, Tuple]: """Format type cast expression using PostgreSQL :: syntax. PostgreSQL supports both standard CAST(expr AS type) syntax and the PostgreSQL-specific expr::type syntax. This method uses the more concise :: syntax which is idiomatic in PostgreSQL. + ``expr.expression`` renders through its own ``to_sql()``; the cast + target is the node's ``target_type`` construction parameter. + Args: - expr_sql: SQL expression string to be cast - target_type: Target PostgreSQL type name (e.g., 'integer', 'varchar(100)') - expr_params: Parameters tuple for the expression - alias: Optional alias for the result + expr: :class:`~...expression.core.CastExpression` instance. Returns: Tuple of (SQL string, parameters) Example: - >>> dialect.format_cast_expression('price', 'numeric', ()) - # Returns: ('price::numeric', ()) - >>> dialect.format_cast_expression('amount', 'money', ()) - # Returns: ('amount::money', ()) - >>> dialect.format_cast_expression('value', 'integer', (), 'int_val') - # Returns: ('value::integer AS "int_val"', ()) + >>> Column(dialect, 'price').cast('numeric').to_sql() + # Returns: ('"price"::numeric', ()) Note: For chained type conversions, each ::type is appended: >>> col.cast('money').cast('numeric').cast('float8') - # Generates: col::money::numeric::float8 + # Generates: "col"::money::numeric::float8 """ - sql = f"{expr_sql}::{target_type}" - if alias: - sql = f"{sql} AS {self.format_identifier(alias)}" - return sql, expr_params + expr_sql, params = expr.expression.to_sql() + sql = f"{expr_sql}::{expr.target_type}" + if expr.alias: + sql = f"{sql} AS {self.format_identifier(expr.alias)}" + return sql, params # endregion # region Operator Formatting (PostgreSQL-specific) - def format_binary_operator( - self, op: str, left_sql: str, right_sql: str, left_params: tuple, right_params: tuple - ) -> Tuple[str, Tuple]: + def format_binary_operator(self, expr) -> Tuple[str, Tuple]: """Format binary operator with psycopg placeholder escaping. psycopg uses %s as parameter placeholder. When the SQL operator itself contains % (e.g., pg_trgm similarity operator), it must be escaped as %% to prevent psycopg from interpreting it as a placeholder prefix. """ + left_sql, left_params = expr.left.to_sql() + right_sql, right_params = expr.right.to_sql() + op = expr.op # Escape % in operators for psycopg compatibility escaped_op = op.replace('%', '%%') if '%' in op else op sql = f"{left_sql} {escaped_op} {right_sql}" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/lock.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/lock.py index d42f20b0..d49cd207 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/lock.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/lock.py @@ -85,15 +85,10 @@ def __init__( self.shared = shared self.session = session - def to_sql(self) -> SQLQueryAndParams: - """ - Generate SQL for acquiring advisory lock. - - Returns: - Tuple of (SQL string, parameters tuple) - - """ - return self.dialect.format_advisory_lock(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_advisory_lock" class PostgresAdvisoryUnlockExpression(BaseExpression): @@ -133,15 +128,10 @@ def __init__( self.key = key self.shared = shared - def to_sql(self) -> SQLQueryAndParams: - """ - Generate SQL for releasing advisory lock. - - Returns: - Tuple of (SQL string, parameters tuple) - - """ - return self.dialect.format_advisory_unlock(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_advisory_unlock" class PostgresAdvisoryUnlockAllExpression(BaseExpression): @@ -157,15 +147,10 @@ def __init__(self, dialect: "SQLDialectBase"): """Initialize advisory unlock all expression.""" super().__init__(dialect) - def to_sql(self) -> SQLQueryAndParams: - """ - Generate SQL for releasing all advisory locks. - - Returns: - Tuple of (SQL string, parameters tuple) - - """ - return self.dialect.format_advisory_unlock_all(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_advisory_unlock_all" class PostgresTryAdvisoryLockExpression(BaseExpression): @@ -211,12 +196,7 @@ def __init__( self.shared = shared self.session = session - def to_sql(self) -> SQLQueryAndParams: - """ - Generate SQL for non-blocking advisory lock acquisition. - - Returns: - Tuple of (SQL string, parameters tuple) - - """ - return self.dialect.format_try_advisory_lock(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_try_advisory_lock" \ No newline at end of file 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 91b80782..3b0999e3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py @@ -59,11 +59,7 @@ def __init__( self.using_index = using_index self.verbose = verbose - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the CLUSTER statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_cluster_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_cluster_statement" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/collation.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/collation.py index 5427436c..85c2e009 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/collation.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/collation.py @@ -64,14 +64,10 @@ def __init__( self.if_not_exists = if_not_exists self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the CREATE COLLATION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_collation_ddl_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_collation_ddl_statement" class PostgresDropCollationExpression(BaseExpression): @@ -103,11 +99,7 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "Tuple[str, tuple]": - """Return the DROP COLLATION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_collation_ddl_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_collation_ddl_statement" \ No newline at end of file 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 d3301a74..f1025d50 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py @@ -83,11 +83,7 @@ def __init__( self.schema = schema self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate COMMENT ON SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_comment_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_comment_statement" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/domain.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/domain.py index 5c155ced..cf2f462b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/domain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/domain.py @@ -73,14 +73,10 @@ def __init__( self.constraints = constraints or [] self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate CREATE DOMAIN SQL statement. - - Returns: - Tuple of (SQL string, params tuple). - - """ - return self.dialect.format_create_domain_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_domain_statement" class PostgresAlterDomainExpression(BaseExpression): @@ -119,14 +115,10 @@ def __init__( self.new_name = new_name self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the ALTER DOMAIN SQL statement. - - Returns: - Tuple of (SQL string, params tuple). - - """ - return self.dialect.format_postgres_alter_domain_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_postgres_alter_domain_statement" class PostgresDropDomainExpression(BaseExpression): @@ -158,11 +150,7 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the DROP DOMAIN SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_domain_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_domain_statement" \ No newline at end of file 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 fa8fbc8c..0639a43b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py @@ -78,14 +78,10 @@ def __init__( self.cascade = cascade self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate CREATE EXTENSION SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_extension(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_extension" class PostgresDropExtensionExpression(BaseExpression): @@ -134,11 +130,7 @@ def __init__( self.restrict = restrict self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate DROP EXTENSION SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_extension(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_extension" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/foreign_table.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/foreign_table.py index 66558f59..4231b97d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/foreign_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/foreign_table.py @@ -58,14 +58,10 @@ def __init__( self.if_not_exists = if_not_exists self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the CREATE FOREIGN TABLE statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_foreign_table_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_foreign_table_statement" class PostgresDropForeignTableExpression(BaseExpression): @@ -97,11 +93,7 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "Tuple[str, tuple]": - """Return the DROP FOREIGN TABLE statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_foreign_table_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_foreign_table_statement" \ No newline at end of file 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 24118b79..9a5be396 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py @@ -140,8 +140,10 @@ def __init__( self.nowait = nowait self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - return self.dialect.format_alter_index_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_index_statement" class PostgresReindexExpression(BaseExpression): @@ -211,11 +213,7 @@ def __init__( self.verbose = verbose self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate REINDEX SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_reindex_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_reindex_statement" 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 489280c7..f904d52d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py @@ -96,11 +96,7 @@ def concurrently(self): """Alias for concurrent (backward compatibility).""" return self.concurrent - def to_sql(self) -> Tuple[str, tuple]: - """Generate REFRESH MATERIALIZED VIEW SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_refresh_materialized_view_pg_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_refresh_materialized_view_pg_statement" \ No newline at end of file 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 8999a8b8..c71501fe 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py @@ -83,15 +83,10 @@ def __init__( ) self.value = value - def to_sql(self) -> Tuple[str, tuple]: - """Generate SQL for a partition bound value. - - Delegates to the dialect's format_partition_value() method. - - Returns: - Tuple of (SQL string, empty params tuple). - """ - return self.dialect.format_partition_value(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_partition_value" class PostgresCreatePartitionExpression(BaseExpression): @@ -159,14 +154,10 @@ def __init__( self.if_not_exists = if_not_exists self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate CREATE TABLE PARTITION SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_partition_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_partition_statement" class PostgresDetachPartitionExpression(BaseExpression): @@ -216,14 +207,10 @@ def __init__( self.finalize = finalize self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate DETACH PARTITION SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_detach_partition_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_detach_partition_statement" class PostgresAttachPartitionExpression(BaseExpression): @@ -280,17 +267,10 @@ def __init__( self.concurrently = concurrently self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate ATTACH PARTITION SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - Raises: - ValueError: If concurrently is used on PostgreSQL < 14. - - """ - return self.dialect.format_attach_partition_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_attach_partition_statement" class PostgresPartitionMetadataExpression(BaseExpression): @@ -319,6 +299,7 @@ def __init__( self.include_partitions = include_partitions self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate PostgreSQL partition metadata query SQL.""" - return self.dialect.format_partition_metadata_query(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_partition_metadata_query" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py index 0e08ae26..2026e3f3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py @@ -82,13 +82,10 @@ def __init__( self.jobmon = jobmon self.schema = schema - def to_sql(self) -> Tuple[str, tuple]: - """Generate SQL for pg_partman create_parent(). - - Returns: - Tuple of (SELECT function_call SQL, params tuple). - """ - return self.dialect.format_pg_partman_create_parent(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_pg_partman_create_parent" class PostgresPgPartmanRunMaintenanceExpression(BaseExpression): @@ -115,13 +112,10 @@ def __init__( self.parent_table = parent_table self.schema = schema - def to_sql(self) -> Tuple[str, tuple]: - """Generate SQL for pg_partman run_maintenance(). - - Returns: - Tuple of (SELECT function_call SQL, params tuple). - """ - return self.dialect.format_pg_partman_run_maintenance(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_pg_partman_run_maintenance" class PostgresPgPartmanUpdateConfigExpression(BaseExpression): @@ -167,16 +161,10 @@ def __init__( self.retention_keep_index = retention_keep_index self.schema = schema - def to_sql(self) -> Tuple[str, tuple]: - """Generate SQL for updating pg_partman part_config. - - Returns: - Tuple of (UPDATE SQL, params tuple). - - Raises: - ValueError: If no config options are specified. - """ - return self.dialect.format_pg_partman_update_config(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_pg_partman_update_config" class PostgresPgPartmanDeleteConfigExpression(BaseExpression): @@ -202,10 +190,7 @@ def __init__( self.parent_table = parent_table self.schema = schema - def to_sql(self) -> Tuple[str, tuple]: - """Generate SQL for deleting a pg_partman part_config row. - - Returns: - Tuple of (DELETE SQL, params tuple). - """ - return self.dialect.format_pg_partman_delete_config(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_pg_partman_delete_config" 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 42b907e2..2feabfbd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py @@ -126,14 +126,10 @@ def __init__( self.using = using self.with_check = with_check - def to_sql(self) -> "SQLQueryAndParams": - """Generate CREATE POLICY SQL statement. - - Returns: - Tuple of (SQL string, params tuple). - - """ - return self.dialect.format_create_policy_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_policy_statement" class PostgresAlterPolicyExpression(BaseExpression): @@ -192,14 +188,10 @@ def mode(self) -> AlterPolicyMode: return AlterPolicyMode.RENAME return AlterPolicyMode.REPLACE - def to_sql(self) -> "SQLQueryAndParams": - """Generate ALTER POLICY SQL statement. - - Returns: - Tuple of (SQL string, params tuple). - - """ - return self.dialect.format_alter_policy_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_policy_statement" class PostgresDropPolicyExpression(BaseExpression): @@ -239,11 +231,7 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "SQLQueryAndParams": - """Generate DROP POLICY SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_policy_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_policy_statement" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/publication.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/publication.py index 3ce661a1..a6a8024f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/publication.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/publication.py @@ -61,14 +61,10 @@ def __init__( self.options = options or [] self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the CREATE PUBLICATION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_publication_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_publication_statement" class PostgresDropPublicationExpression(BaseExpression): @@ -94,14 +90,10 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "Tuple[str, tuple]": - """Return the DROP PUBLICATION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_publication_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_publication_statement" class PostgresCreateSubscriptionExpression(BaseExpression): @@ -132,14 +124,10 @@ def __init__( self.options = options or [] self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the CREATE SUBSCRIPTION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_subscription_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_subscription_statement" class PostgresDropSubscriptionExpression(BaseExpression): @@ -164,11 +152,7 @@ def __init__( self.if_exists = if_exists self.cascade = cascade - def to_sql(self) -> "Tuple[str, tuple]": - """Return the DROP SUBSCRIPTION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_subscription_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_subscription_statement" \ No newline at end of file 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 92b36c8c..133fd743 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 @@ -83,14 +83,10 @@ def __init__( self.mode = mode self.always = always - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the ALTER TABLE ... ROW LEVEL SECURITY statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_alter_table_rls_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_table_rls_statement" class PostgresForceRlsExpression(BaseExpression): @@ -120,11 +116,7 @@ def __init__( self.schema = schema self.force = force - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the FORCE/NO FORCE ROW LEVEL SECURITY statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_force_rls_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_force_rls_statement" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/routine.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/routine.py index 00d41ec1..ca7cf395 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/routine.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/routine.py @@ -82,14 +82,10 @@ def __init__( self.strict = strict self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the CREATE FUNCTION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_function_ddl_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_function_ddl_statement" class PostgresDropFunctionExpression(BaseExpression): @@ -125,14 +121,10 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "Tuple[str, tuple]": - """Return the DROP FUNCTION statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_function_ddl_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_function_ddl_statement" class PostgresCreateAggregateExpression(BaseExpression): @@ -169,14 +161,10 @@ def __init__( self.initcond = initcond self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Return the CREATE AGGREGATE statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_aggregate_ddl_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_aggregate_ddl_statement" class PostgresDropAggregateExpression(BaseExpression): @@ -212,11 +200,7 @@ def __init__( self.cascade = cascade self.restrict = restrict - def to_sql(self) -> "Tuple[str, tuple]": - """Return the DROP AGGREGATE statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_aggregate_ddl_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_aggregate_ddl_statement" \ No newline at end of file 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 9c625ad4..afdd2cd9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py @@ -85,14 +85,10 @@ def __init__( self.if_not_exists = if_not_exists self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate CREATE STATISTICS SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_statistics_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_statistics_statement" class PostgresDropStatisticsExpression(BaseExpression): @@ -133,11 +129,7 @@ def __init__( self.if_exists = if_exists self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate DROP STATISTICS SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_statistics_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_statistics_statement" \ No newline at end of file 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 a763afdd..67142b67 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 @@ -80,11 +80,7 @@ def __init__( self.access_method = access_method self.dialect_options = dialect_options or {} - def to_sql(self) -> "Tuple[str, tuple]": - """Generate the ALTER TABLE ... SET statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_alter_table_settings_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_table_settings_statement" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py index 1bf99059..a783a5bb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py @@ -91,14 +91,10 @@ def __init__( self.if_not_exists = if_not_exists self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate CREATE TYPE AS ENUM SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_enum_type(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_enum_type" class PostgresDropEnumTypeExpression(BaseExpression): @@ -151,14 +147,10 @@ def __init__( self.cascade = cascade self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate DROP TYPE SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_drop_enum_type(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_enum_type" class PostgresAlterEnumAddValueExpression(BaseExpression): @@ -215,14 +207,10 @@ def __init__( self.after = after self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate ALTER TYPE ADD VALUE SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_alter_enum_add_value(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_enum_add_value" class PostgresAlterEnumTypeAddValueExpression(BaseExpression): @@ -265,14 +253,10 @@ def __init__( self.before = before self.after = after - def to_sql(self) -> Tuple[str, tuple]: - """Generate ALTER TYPE ADD VALUE SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_alter_enum_type_add_value(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_enum_type_add_value" class PostgresAlterEnumTypeRenameValueExpression(BaseExpression): @@ -312,14 +296,10 @@ def __init__( self.new_value = new_value self.schema = schema - def to_sql(self) -> Tuple[str, tuple]: - """Generate ALTER TYPE RENAME VALUE SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_alter_enum_type_rename_value(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_enum_type_rename_value" class PostgresCreateRangeTypeExpression(BaseExpression): @@ -390,11 +370,7 @@ def __init__( self.subtype_diff = subtype_diff self.if_not_exists = if_not_exists - def to_sql(self) -> Tuple[str, tuple]: - """Generate CREATE TYPE AS RANGE SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_create_range_type(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_range_type" 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 18062612..0a28b660 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py @@ -101,14 +101,10 @@ def __init__( self.columns = columns self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate VACUUM SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_vacuum_statement(self) + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_vacuum_statement" class PostgresAnalyzeExpression(BaseExpression): @@ -164,11 +160,7 @@ def __init__( self.columns = columns self.dialect_options = dialect_options or {} - def to_sql(self) -> Tuple[str, tuple]: - """Generate ANALYZE SQL statement. - - Returns: - Tuple of (SQL string, empty params tuple). - - """ - return self.dialect.format_analyze_statement(self) \ No newline at end of file + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_analyze_statement" \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index d4dbefa2..c2e7e1cc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import Optional, Set +from typing import Optional, Set, TYPE_CHECKING from rhosocial.activerecord.backend.expression.types import ( ArrayType, @@ -26,14 +26,18 @@ VarCharType, ) +if TYPE_CHECKING: + from rhosocial.activerecord.backend.dialect import SQLDialectBase + # --------------------------------------------------------------------------- # Character varying alias # --------------------------------------------------------------------------- -class PostgresCharacterVaryingType(VarCharType, backend="postgres"): +class PostgresCharacterVaryingType(VarCharType): """PostgreSQL ``CHARACTER VARYING(n)`` — alias for ``VARCHAR(n)``.""" + name = "postgres_character_varying" @classmethod def synonyms(cls) -> Set[str]: return {'VarCharType'} @@ -43,9 +47,10 @@ def synonyms(cls) -> Set[str]: # Binary data: BYTEA # --------------------------------------------------------------------------- -class PostgresByteaType(BlobType, backend="postgres"): +class PostgresByteaType(BlobType): """PostgreSQL ``BYTEA`` — variable-length binary string.""" + name = "postgres_bytea" @classmethod def synonyms(cls) -> Set[str]: return {'BlobType'} @@ -55,25 +60,28 @@ def synonyms(cls) -> Set[str]: # Serial (auto-increment) types # --------------------------------------------------------------------------- -class PostgresSmallSerialType(DataType, backend="postgres"): +class PostgresSmallSerialType(DataType): """PostgreSQL ``SMALLSERIAL`` — auto-incrementing SMALLINT (2 bytes).""" + name = "postgres_smallserial" @classmethod def synonyms(cls) -> Set[str]: return {'SmallSerialType', 'SmallIntType'} -class PostgresSerialType(DataType, backend="postgres"): +class PostgresSerialType(DataType): """PostgreSQL ``SERIAL`` — auto-incrementing INTEGER (4 bytes).""" + name = "postgres_serial" @classmethod def synonyms(cls) -> Set[str]: return {'SerialType', 'IntegerType'} -class PostgresBigSerialType(DataType, backend="postgres"): +class PostgresBigSerialType(DataType): """PostgreSQL ``BIGSERIAL`` — auto-incrementing BIGINT (8 bytes).""" + name = "postgres_bigserial" @classmethod def synonyms(cls) -> Set[str]: return {'BigSerialType', 'BigIntType'} @@ -83,49 +91,50 @@ def synonyms(cls) -> Set[str]: # UUID # --------------------------------------------------------------------------- -class PostgresUUIDType(DataType, backend="postgres"): +class PostgresUUIDType(DataType): """PostgreSQL ``UUID`` — universally unique identifier.""" - + name = "postgres_uuid" # --------------------------------------------------------------------------- # XML # --------------------------------------------------------------------------- -class PostgresXMLType(DataType, backend="postgres"): +class PostgresXMLType(DataType): """PostgreSQL ``XML`` — XML data type.""" - + name = "postgres_xml" # --------------------------------------------------------------------------- # Text search # --------------------------------------------------------------------------- -class PostgresTSVectorType(DataType, backend="postgres"): +class PostgresTSVectorType(DataType): """PostgreSQL ``TSVECTOR`` — text search document.""" - -class PostgresTSQueryType(DataType, backend="postgres"): + name = "postgres_tsvector" +class PostgresTSQueryType(DataType): """PostgreSQL ``TSQUERY`` — text search query.""" - + name = "postgres_tsquery" # --------------------------------------------------------------------------- # JSON path # --------------------------------------------------------------------------- -class PostgresJsonPathType(DataType, backend="postgres"): +class PostgresJsonPathType(DataType): """PostgreSQL ``JSONPATH`` — SQL/JSON path expression (PG 12+).""" - + name = "postgres_jsonpath" # --------------------------------------------------------------------------- # Bit string types # --------------------------------------------------------------------------- -class PostgresBitType(DataType, backend="postgres"): +class PostgresBitType(DataType): """PostgreSQL ``BIT(n)`` — fixed-length bit string.""" + name = "postgres_bit" n: Optional[int] = None - def __init__(self, n: Optional[int] = None): - super().__init__() + def __init__(self, n: Optional[int] = None, dialect: Optional["SQLDialectBase"] = None): + super().__init__(dialect) self.n = n def __eq__(self, other: object) -> bool: @@ -137,13 +146,14 @@ def __hash__(self) -> int: return hash((type(self), self.n)) -class PostgresVarBitType(DataType, backend="postgres"): +class PostgresVarBitType(DataType): """PostgreSQL ``VARBIT(n)`` — variable-length bit string.""" + name = "postgres_varbit" n: Optional[int] = None - def __init__(self, n: Optional[int] = None): - super().__init__() + def __init__(self, n: Optional[int] = None, dialect: Optional["SQLDialectBase"] = None): + super().__init__(dialect) self.n = n def __eq__(self, other: object) -> bool: @@ -159,204 +169,205 @@ def __hash__(self) -> int: # Network address types # --------------------------------------------------------------------------- -class PostgresInetType(DataType, backend="postgres"): +class PostgresInetType(DataType): """PostgreSQL ``INET`` — IPv4 or IPv6 address.""" - -class PostgresCidrType(DataType, backend="postgres"): + name = "postgres_inet" +class PostgresCidrType(DataType): """PostgreSQL ``CIDR`` — IPv4 or IPv6 network.""" - -class PostgresMacAddrType(DataType, backend="postgres"): + name = "postgres_cidr" +class PostgresMacAddrType(DataType): """PostgreSQL ``MACADDR`` — MAC address (EUI-48).""" - -class PostgresMacAddr8Type(DataType, backend="postgres"): + name = "postgres_macaddr" +class PostgresMacAddr8Type(DataType): """PostgreSQL ``MACADDR8`` — MAC address (EUI-64, PG 10+).""" - + name = "postgres_macaddr8" # --------------------------------------------------------------------------- # Geometric types # --------------------------------------------------------------------------- -class PostgresPointType(DataType, backend="postgres"): +class PostgresPointType(DataType): """PostgreSQL ``POINT`` — geometric point (x, y).""" - -class PostgresLineType(DataType, backend="postgres"): + name = "postgres_point" +class PostgresLineType(DataType): """PostgreSQL ``LINE`` — infinite line.""" - -class PostgresLineSegmentType(DataType, backend="postgres"): + name = "postgres_line" +class PostgresLineSegmentType(DataType): """PostgreSQL ``LSEG`` — line segment.""" - -class PostgresBoxType(DataType, backend="postgres"): + name = "postgres_line_segment" +class PostgresBoxType(DataType): """PostgreSQL ``BOX`` — rectangular box.""" - -class PostgresPathType(DataType, backend="postgres"): + name = "postgres_box" +class PostgresPathType(DataType): """PostgreSQL ``PATH`` — open or closed geometric path.""" - -class PostgresPolygonType(DataType, backend="postgres"): + name = "postgres_path" +class PostgresPolygonType(DataType): """PostgreSQL ``POLYGON`` — closed geometric polygon.""" - -class PostgresCircleType(DataType, backend="postgres"): + name = "postgres_polygon" +class PostgresCircleType(DataType): """PostgreSQL ``CIRCLE`` — circle (center + radius).""" - + name = "postgres_circle" # --------------------------------------------------------------------------- # Monetary type # --------------------------------------------------------------------------- -class PostgresMoneyType(DataType, backend="postgres"): +class PostgresMoneyType(DataType): """PostgreSQL ``MONEY`` — currency amount.""" - + name = "postgres_money" # --------------------------------------------------------------------------- # Range types # --------------------------------------------------------------------------- -class PostgresInt4RangeType(DataType, backend="postgres"): +class PostgresInt4RangeType(DataType): """PostgreSQL ``INT4RANGE`` — range of integer.""" - -class PostgresInt8RangeType(DataType, backend="postgres"): + name = "postgres_int4range" +class PostgresInt8RangeType(DataType): """PostgreSQL ``INT8RANGE`` — range of bigint.""" - -class PostgresNumRangeType(DataType, backend="postgres"): + name = "postgres_int8range" +class PostgresNumRangeType(DataType): """PostgreSQL ``NUMRANGE`` — range of numeric.""" - -class PostgresTsRangeType(DataType, backend="postgres"): + name = "postgres_numrange" +class PostgresTsRangeType(DataType): """PostgreSQL ``TSRANGE`` — range of timestamp without time zone.""" - -class PostgresTsTzRangeType(DataType, backend="postgres"): + name = "postgres_tsrange" +class PostgresTsTzRangeType(DataType): """PostgreSQL ``TSTZRANGE`` — range of timestamp with time zone.""" - -class PostgresDateRangeType(DataType, backend="postgres"): + name = "postgres_tstzrange" +class PostgresDateRangeType(DataType): """PostgreSQL ``DATERANGE`` — range of date.""" - + name = "postgres_daterange" # --------------------------------------------------------------------------- # Multirange types (PG 14+) # --------------------------------------------------------------------------- -class PostgresInt4MultirangeType(DataType, backend="postgres"): +class PostgresInt4MultirangeType(DataType): """PostgreSQL ``INT4MULTIRANGE`` — multirange of integer (PG 14+).""" - -class PostgresInt8MultirangeType(DataType, backend="postgres"): + name = "postgres_int4multirange" +class PostgresInt8MultirangeType(DataType): """PostgreSQL ``INT8MULTIRANGE`` — multirange of bigint (PG 14+).""" - -class PostgresNumMultirangeType(DataType, backend="postgres"): + name = "postgres_int8multirange" +class PostgresNumMultirangeType(DataType): """PostgreSQL ``NUMMULTIRANGE`` — multirange of numeric (PG 14+).""" - -class PostgresTsMultirangeType(DataType, backend="postgres"): + name = "postgres_nummultirange" +class PostgresTsMultirangeType(DataType): """PostgreSQL ``TSMULTIRANGE`` — multirange of timestamp (PG 14+).""" - -class PostgresTsTzMultirangeType(DataType, backend="postgres"): + name = "postgres_tsmultirange" +class PostgresTsTzMultirangeType(DataType): """PostgreSQL ``TSTZMULTIRANGE`` — multirange of timestamptz (PG 14+).""" - -class PostgresDateMultirangeType(DataType, backend="postgres"): + name = "postgres_tstzmultirange" +class PostgresDateMultirangeType(DataType): """PostgreSQL ``DATEMULTIRANGE`` — multirange of date (PG 14+).""" - + name = "postgres_datemultirange" # --------------------------------------------------------------------------- # Object identifier types # --------------------------------------------------------------------------- -class PostgresOIDType(DataType, backend="postgres"): +class PostgresOIDType(DataType): """PostgreSQL ``OID`` — object identifier.""" - -class PostgresRegClassType(DataType, backend="postgres"): + name = "postgres_oid" +class PostgresRegClassType(DataType): """PostgreSQL ``REGCLASS`` — relation name (OID alias).""" - -class PostgresRegTypeType(DataType, backend="postgres"): + name = "postgres_regclass" +class PostgresRegTypeType(DataType): """PostgreSQL ``REGTYPE`` — type name (OID alias).""" - -class PostgresXIDType(DataType, backend="postgres"): + name = "postgres_regtype" +class PostgresXIDType(DataType): """PostgreSQL ``XID`` — transaction ID.""" - -class PostgresXID8Type(DataType, backend="postgres"): + name = "postgres_xid" +class PostgresXID8Type(DataType): """PostgreSQL ``XID8`` — 64-bit transaction ID (PG 13+).""" - -class PostgresCIDType(DataType, backend="postgres"): + name = "postgres_xid8" +class PostgresCIDType(DataType): """PostgreSQL ``CID`` — command ID.""" - -class PostgresTIDType(DataType, backend="postgres"): + name = "postgres_cid" +class PostgresTIDType(DataType): """PostgreSQL ``TID`` — tuple ID (page, tuple).""" - + name = "postgres_tid" # --------------------------------------------------------------------------- # pg_lsn type # --------------------------------------------------------------------------- -class PostgresPgLSNType(DataType, backend="postgres"): +class PostgresPgLSNType(DataType): """PostgreSQL ``PG_LSN`` — WAL log sequence number.""" - + name = "postgres_pg_lsn" # --------------------------------------------------------------------------- # Extension-provided types (minimal DataType wrappers) # --------------------------------------------------------------------------- -class PostgresHstoreType(DataType, backend="postgres"): +class PostgresHstoreType(DataType): """PostgreSQL ``HSTORE`` — key-value store (hstore extension).""" - -class PostgresGeometryType(DataType, backend="postgres"): + name = "postgres_hstore" +class PostgresGeometryType(DataType): """PostGIS ``GEOMETRY`` — generic spatial geometry (PostGIS extension). For production use, subclass with SRID support as needed. """ - -class PostgresGeographyType(DataType, backend="postgres"): + name = "postgres_geometry" +class PostgresGeographyType(DataType): """PostGIS ``GEOGRAPHY`` — geodetic spatial type (PostGIS extension).""" - -class PostgresCitextType(DataType, backend="postgres"): + name = "postgres_geography" +class PostgresCitextType(DataType): """PostgreSQL ``CITEXT`` — case-insensitive text (citext extension).""" - -class PostgresCubeType(DataType, backend="postgres"): + name = "postgres_citext" +class PostgresCubeType(DataType): """PostgreSQL ``CUBE`` — multi-dimensional cube (cube extension).""" - -class PostgresLtreeType(DataType, backend="postgres"): + name = "postgres_cube" +class PostgresLtreeType(DataType): """PostgreSQL ``LTREE`` — label tree (ltree extension).""" - -class PostgresRasterType(DataType, backend="postgres"): + name = "postgres_ltree" +class PostgresRasterType(DataType): """PostgreSQL ``RASTER`` — raster (PostGIS raster extension).""" - -class PostgresVectorType(DataType, backend="postgres"): + name = "postgres_raster" +class PostgresVectorType(DataType): """pgvector ``VECTOR(n)`` — vector embedding (pgvector extension). Args: dim: Number of dimensions. """ + name = "postgres_vector" dim: int - def __init__(self, dim: int): - super().__init__() + def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None): + super().__init__(dialect) self.dim = dim def __eq__(self, other: object) -> bool: @@ -368,7 +379,7 @@ def __hash__(self) -> int: return hash((type(self), self.dim)) -class PostgresHalfvecType(DataType, backend="postgres"): +class PostgresHalfvecType(DataType): """pgvector ``HALFVEC(n)`` — half-precision vector (pgvector 0.5.0+). Stores each component as a half-precision float, halving memory compared @@ -378,10 +389,11 @@ class PostgresHalfvecType(DataType, backend="postgres"): dim: Number of dimensions. """ + name = "postgres_halfvec" dim: int - def __init__(self, dim: int): - super().__init__() + def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None): + super().__init__(dialect) self.dim = dim def __eq__(self, other: object) -> bool: @@ -393,7 +405,7 @@ def __hash__(self) -> int: return hash((type(self), self.dim)) -class PostgresSparsevecType(DataType, backend="postgres"): +class PostgresSparsevecType(DataType): """pgvector ``SPARSEVEC(n)`` — sparse vector (pgvector 0.7.0+). Stores only non-zero components as ``{idx:value,...}/dim``, suitable for @@ -403,10 +415,11 @@ class PostgresSparsevecType(DataType, backend="postgres"): dim: Number of dimensions. """ + name = "postgres_sparsevec" dim: int - def __init__(self, dim: int): - super().__init__() + def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None): + super().__init__(dialect) self.dim = dim def __eq__(self, other: object) -> bool: @@ -432,6 +445,7 @@ class PostgresArrayType(ArrayType): ``dimensions``. """ + name = "postgres_array" def is_equivalent(self, other: "DataType") -> bool: if not isinstance(other, ArrayType): return False diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py index e3d33eae..e58a33b7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py @@ -83,7 +83,7 @@ def format_create_table_like(self, expr) -> Tuple[str, tuple]: def format_column_definition(self, col_def) -> Tuple[str, tuple]: from rhosocial.activerecord.backend.dialect.base import SQLDialectBase all_params: List[Any] = [] - type_sql, _ = col_def.data_type.to_sql(self) + type_sql, _ = col_def.data_type.to_sql() if not re.fullmatch(r"[A-Za-z0-9\s(),\[\]]+", type_sql): raise ValueError( f"Invalid data type '{type_sql}': " diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 2cfb4833..01da5916 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -111,278 +111,206 @@ class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): # --- PostgreSQL-specific formatters --- - @DDLTypeMixin.handles(PostgresByteaType) - def format_data_type_bytea(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_bytea(self, data_type) -> Tuple[str, tuple]: return "BYTEA", () - @DDLTypeMixin.handles(PostgresSmallSerialType) - def format_data_type_small_serial(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_smallserial(self, data_type) -> Tuple[str, tuple]: return "SMALLSERIAL", () - @DDLTypeMixin.handles(PostgresSerialType) - def format_data_type_serial(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_serial(self, data_type) -> Tuple[str, tuple]: return "SERIAL", () - @DDLTypeMixin.handles(PostgresBigSerialType) - def format_data_type_big_serial(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_bigserial(self, data_type) -> Tuple[str, tuple]: return "BIGSERIAL", () - @DDLTypeMixin.handles(PostgresUUIDType) - def format_data_type_uuid(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_uuid(self, data_type) -> Tuple[str, tuple]: return "UUID", () - @DDLTypeMixin.handles(PostgresXMLType) - def format_data_type_xml(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_xml(self, data_type) -> Tuple[str, tuple]: return "XML", () - @DDLTypeMixin.handles(PostgresCharacterVaryingType) - def format_data_type_character_varying(self, data_type: PostgresCharacterVaryingType) -> Tuple[str, tuple]: + def format_data_type_postgres_character_varying(self, data_type: PostgresCharacterVaryingType) -> Tuple[str, tuple]: if data_type.length is not None: return f"CHARACTER VARYING({data_type.length})", () return "CHARACTER VARYING", () - @DDLTypeMixin.handles(PostgresTSVectorType) - def format_data_type_ts_vector(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tsvector(self, data_type) -> Tuple[str, tuple]: return "TSVECTOR", () - @DDLTypeMixin.handles(PostgresTSQueryType) - def format_data_type_ts_query(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tsquery(self, data_type) -> Tuple[str, tuple]: return "TSQUERY", () - @DDLTypeMixin.handles(PostgresJsonPathType) - def format_data_type_json_path(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_jsonpath(self, data_type) -> Tuple[str, tuple]: return "JSONPATH", () - @DDLTypeMixin.handles(PostgresBitType) - def format_data_type_bit(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_bit(self, data_type) -> Tuple[str, tuple]: if data_type.n is not None: return f"BIT({data_type.n})", () return "BIT", () - @DDLTypeMixin.handles(PostgresVarBitType) - def format_data_type_var_bit(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_varbit(self, data_type) -> Tuple[str, tuple]: if data_type.n is not None: return f"VARBIT({data_type.n})", () return "VARBIT", () - @DDLTypeMixin.handles(PostgresInetType) - def format_data_type_inet(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_inet(self, data_type) -> Tuple[str, tuple]: return "INET", () - @DDLTypeMixin.handles(PostgresCidrType) - def format_data_type_cidr(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_cidr(self, data_type) -> Tuple[str, tuple]: return "CIDR", () - @DDLTypeMixin.handles(PostgresMacAddrType) - def format_data_type_mac_addr(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_macaddr(self, data_type) -> Tuple[str, tuple]: return "MACADDR", () - @DDLTypeMixin.handles(PostgresMacAddr8Type) - def format_data_type_mac_addr8(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_macaddr8(self, data_type) -> Tuple[str, tuple]: return "MACADDR8", () - @DDLTypeMixin.handles(PostgresPointType) - def format_data_type_point(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_point(self, data_type) -> Tuple[str, tuple]: return "POINT", () - @DDLTypeMixin.handles(PostgresLineType) - def format_data_type_line(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_line(self, data_type) -> Tuple[str, tuple]: return "LINE", () - @DDLTypeMixin.handles(PostgresLineSegmentType) - def format_data_type_line_segment(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_line_segment(self, data_type) -> Tuple[str, tuple]: return "LSEG", () - @DDLTypeMixin.handles(PostgresBoxType) - def format_data_type_box(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_box(self, data_type) -> Tuple[str, tuple]: return "BOX", () - @DDLTypeMixin.handles(PostgresPathType) - def format_data_type_path(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_path(self, data_type) -> Tuple[str, tuple]: return "PATH", () - @DDLTypeMixin.handles(PostgresPolygonType) - def format_data_type_polygon(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_polygon(self, data_type) -> Tuple[str, tuple]: return "POLYGON", () - @DDLTypeMixin.handles(PostgresCircleType) - def format_data_type_circle(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_circle(self, data_type) -> Tuple[str, tuple]: return "CIRCLE", () - @DDLTypeMixin.handles(PostgresMoneyType) - def format_data_type_money(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_money(self, data_type) -> Tuple[str, tuple]: return "MONEY", () - @DDLTypeMixin.handles(PostgresInt4RangeType) - def format_data_type_int4_range(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_int4range(self, data_type) -> Tuple[str, tuple]: return "INT4RANGE", () - @DDLTypeMixin.handles(PostgresInt8RangeType) - def format_data_type_int8_range(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_int8range(self, data_type) -> Tuple[str, tuple]: return "INT8RANGE", () - @DDLTypeMixin.handles(PostgresNumRangeType) - def format_data_type_num_range(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_numrange(self, data_type) -> Tuple[str, tuple]: return "NUMRANGE", () - @DDLTypeMixin.handles(PostgresTsRangeType) - def format_data_type_ts_range(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tsrange(self, data_type) -> Tuple[str, tuple]: return "TSRANGE", () - @DDLTypeMixin.handles(PostgresTsTzRangeType) - def format_data_type_ts_tz_range(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tstzrange(self, data_type) -> Tuple[str, tuple]: return "TSTZRANGE", () - @DDLTypeMixin.handles(PostgresDateRangeType) - def format_data_type_date_range(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_daterange(self, data_type) -> Tuple[str, tuple]: return "DATERANGE", () - @DDLTypeMixin.handles(PostgresInt4MultirangeType) - def format_data_type_int4_multirange(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_int4multirange(self, data_type) -> Tuple[str, tuple]: return "INT4MULTIRANGE", () - @DDLTypeMixin.handles(PostgresInt8MultirangeType) - def format_data_type_int8_multirange(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_int8multirange(self, data_type) -> Tuple[str, tuple]: return "INT8MULTIRANGE", () - @DDLTypeMixin.handles(PostgresNumMultirangeType) - def format_data_type_num_multirange(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_nummultirange(self, data_type) -> Tuple[str, tuple]: return "NUMMULTIRANGE", () - @DDLTypeMixin.handles(PostgresTsMultirangeType) - def format_data_type_ts_multirange(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tsmultirange(self, data_type) -> Tuple[str, tuple]: return "TSMULTIRANGE", () - @DDLTypeMixin.handles(PostgresTsTzMultirangeType) - def format_data_type_ts_tz_multirange(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tstzmultirange(self, data_type) -> Tuple[str, tuple]: return "TSTZMULTIRANGE", () - @DDLTypeMixin.handles(PostgresDateMultirangeType) - def format_data_type_date_multirange(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_datemultirange(self, data_type) -> Tuple[str, tuple]: return "DATEMULTIRANGE", () - @DDLTypeMixin.handles(PostgresOIDType) - def format_data_type_oid(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_oid(self, data_type) -> Tuple[str, tuple]: return "OID", () - @DDLTypeMixin.handles(PostgresRegClassType) - def format_data_type_reg_class(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_regclass(self, data_type) -> Tuple[str, tuple]: return "REGCLASS", () - @DDLTypeMixin.handles(PostgresRegTypeType) - def format_data_type_reg_type(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_regtype(self, data_type) -> Tuple[str, tuple]: return "REGTYPE", () - @DDLTypeMixin.handles(PostgresXIDType) - def format_data_type_xid(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_xid(self, data_type) -> Tuple[str, tuple]: return "XID", () - @DDLTypeMixin.handles(PostgresXID8Type) - def format_data_type_xid8(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_xid8(self, data_type) -> Tuple[str, tuple]: return "XID8", () - @DDLTypeMixin.handles(PostgresCIDType) - def format_data_type_cid(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_cid(self, data_type) -> Tuple[str, tuple]: return "CID", () - @DDLTypeMixin.handles(PostgresTIDType) - def format_data_type_tid(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_tid(self, data_type) -> Tuple[str, tuple]: return "TID", () - @DDLTypeMixin.handles(PostgresPgLSNType) - def format_data_type_pg_lsn(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_pg_lsn(self, data_type) -> Tuple[str, tuple]: return "PG_LSN", () - @DDLTypeMixin.handles(PostgresHstoreType) - def format_data_type_hstore(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_hstore(self, data_type) -> Tuple[str, tuple]: return "HSTORE", () - @DDLTypeMixin.handles(PostgresGeometryType) - def format_data_type_geometry(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_geometry(self, data_type) -> Tuple[str, tuple]: return "GEOMETRY", () - @DDLTypeMixin.handles(PostgresGeographyType) - def format_data_type_geography(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_geography(self, data_type) -> Tuple[str, tuple]: return "GEOGRAPHY", () - @DDLTypeMixin.handles(PostgresVectorType) - def format_data_type_vector(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_vector(self, data_type) -> Tuple[str, tuple]: return f"VECTOR({data_type.dim})", () - @DDLTypeMixin.handles(PostgresHalfvecType) - def format_data_type_halfvec(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_halfvec(self, data_type) -> Tuple[str, tuple]: return f"HALFVEC({data_type.dim})", () - @DDLTypeMixin.handles(PostgresSparsevecType) - def format_data_type_sparsevec(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_sparsevec(self, data_type) -> Tuple[str, tuple]: return f"SPARSEVEC({data_type.dim})", () - @DDLTypeMixin.handles(PostgresCitextType) - def format_data_type_citext(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_citext(self, data_type) -> Tuple[str, tuple]: return "CITEXT", () - @DDLTypeMixin.handles(PostgresCubeType) - def format_data_type_cube(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_cube(self, data_type) -> Tuple[str, tuple]: return "CUBE", () - @DDLTypeMixin.handles(PostgresLtreeType) - def format_data_type_ltree(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_ltree(self, data_type) -> Tuple[str, tuple]: return "LTREE", () - @DDLTypeMixin.handles(PostgresRasterType) - def format_data_type_raster(self, data_type) -> Tuple[str, tuple]: + def format_data_type_postgres_raster(self, data_type) -> Tuple[str, tuple]: return "RASTER", () - @DDLTypeMixin.handles(ArrayType) - def format_data_type_array(self, data_type: ArrayType) -> Tuple[str, tuple]: - elem = data_type.element_type - elem_cls = type(elem) - if elem_cls not in self._type_formatters: - raise TypeError( - f"ArrayType element type {elem_cls.__name__!r} is not supported " - f"by {type(self).__name__}. Use a PostgreSQL-specific DataType " - f"as the element type (e.g. PostgresIntegerType)." - ) - element_sql, _ = self.format_data_type(elem) + def format_data_type_postgres_array(self, data_type: ArrayType) -> Tuple[str, tuple]: + element_sql, _ = self.format_data_type(data_type.element_type) return element_sql + "[]" * data_type.dimensions, () # --- Core formatters (PostgreSQL specialized) --- - @DDLTypeMixin.handles(TinyIntType) - def format_data_type_tiny_int(self, data_type: TinyIntType) -> Tuple[str, tuple]: + def format_data_type_tinyint(self, data_type: TinyIntType) -> Tuple[str, tuple]: return "SMALLINT", () - @DDLTypeMixin.handles(SmallIntType) - def format_data_type_small_int(self, data_type: SmallIntType) -> Tuple[str, tuple]: + def format_data_type_smallint(self, data_type: SmallIntType) -> Tuple[str, tuple]: return "SMALLINT", () - @DDLTypeMixin.handles(IntType) def format_data_type_int(self, data_type: IntType) -> Tuple[str, tuple]: return "INTEGER", () - @DDLTypeMixin.handles(IntegerType) def format_data_type_integer(self, data_type: IntegerType) -> Tuple[str, tuple]: return "INTEGER", () - @DDLTypeMixin.handles(BigIntType) - def format_data_type_big_int(self, data_type: BigIntType) -> Tuple[str, tuple]: + def format_data_type_bigint(self, data_type: BigIntType) -> Tuple[str, tuple]: return "BIGINT", () - @DDLTypeMixin.handles(FloatType) def format_data_type_float(self, data_type: FloatType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"FLOAT({data_type.precision})", () return "REAL", () - @DDLTypeMixin.handles(RealType) def format_data_type_real(self, data_type: RealType) -> Tuple[str, tuple]: return "REAL", () - @DDLTypeMixin.handles(DoubleType) def format_data_type_double(self, data_type: DoubleType) -> Tuple[str, tuple]: return "DOUBLE PRECISION", () - @DDLTypeMixin.handles(DecimalType) def format_data_type_decimal(self, data_type: DecimalType) -> Tuple[str, tuple]: if data_type.precision is not None and data_type.scale is not None: return f"DECIMAL({data_type.precision},{data_type.scale})", () @@ -390,76 +318,95 @@ def format_data_type_decimal(self, data_type: DecimalType) -> Tuple[str, tuple]: return f"DECIMAL({data_type.precision})", () return "DECIMAL", () - @DDLTypeMixin.handles(CharType) def format_data_type_char(self, data_type: CharType) -> Tuple[str, tuple]: if data_type.length is not None: return f"CHAR({data_type.length})", () return "CHAR", () - @DDLTypeMixin.handles(VarCharType) - def format_data_type_var_char(self, data_type: VarCharType) -> Tuple[str, tuple]: + def format_data_type_varchar(self, data_type: VarCharType) -> Tuple[str, tuple]: if data_type.length is not None: return f"VARCHAR({data_type.length})", () return "VARCHAR", () - @DDLTypeMixin.handles(TextType) def format_data_type_text(self, data_type: TextType) -> Tuple[str, tuple]: return "TEXT", () - @DDLTypeMixin.handles(BooleanType) def format_data_type_boolean(self, data_type: BooleanType) -> Tuple[str, tuple]: return "BOOLEAN", () - @DDLTypeMixin.handles(BlobType) def format_data_type_blob(self, data_type: BlobType) -> Tuple[str, tuple]: return "BYTEA", () - @DDLTypeMixin.handles(DateType) def format_data_type_date(self, data_type: DateType) -> Tuple[str, tuple]: return "DATE", () - @DDLTypeMixin.handles(TimeType) def format_data_type_time(self, data_type: TimeType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIME({data_type.precision})", () return "TIME", () - @DDLTypeMixin.handles(TimeTzType) - def format_data_type_time_tz(self, data_type: TimeTzType) -> Tuple[str, tuple]: + def format_data_type_timetz(self, data_type: TimeTzType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIME({data_type.precision}) WITH TIME ZONE", () return "TIME WITH TIME ZONE", () - @DDLTypeMixin.handles(DateTimeType) - def format_data_type_date_time(self, data_type: DateTimeType) -> Tuple[str, tuple]: + def format_data_type_datetime(self, data_type: DateTimeType) -> Tuple[str, tuple]: return "TIMESTAMP", () - @DDLTypeMixin.handles(TimestampType) def format_data_type_timestamp(self, data_type: TimestampType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIMESTAMP({data_type.precision})", () return "TIMESTAMP", () - @DDLTypeMixin.handles(TimestampTzType) - def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> Tuple[str, tuple]: + def format_data_type_timestamptz(self, data_type: TimestampTzType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE", () return "TIMESTAMP WITH TIME ZONE", () - @DDLTypeMixin.handles(IntervalType) def format_data_type_interval(self, data_type: IntervalType) -> Tuple[str, tuple]: if data_type.fields is not None: return f"INTERVAL {data_type.fields}", () return "INTERVAL", () - @DDLTypeMixin.handles(JsonType) def format_data_type_json(self, data_type: JsonType) -> Tuple[str, tuple]: return "JSON", () - @DDLTypeMixin.handles(JsonBType) - def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: + def format_data_type_jsonb(self, data_type: JsonBType) -> Tuple[str, tuple]: return "JSONB", () + # ------------------------------------------------------------------ + # Supported types advertisement + # ------------------------------------------------------------------ + + def supports_data_types(self) -> dict: + """Mapping ``{: concrete type class}`` of every type this + dialect renders. + + The formatters a dialect defines **are** the set of types it + supports (naming-convention dispatch has no registry), so this + derives the mapping from the ``format_data_type_`` methods. + """ + result = {} + for member_name in dir(type(self)): + match = re.match(r"^format_data_type_([a-z][a-z0-9_]*)$", member_name) + if not match: + continue + name = match.group(1) + cls = self._type_class_for(name) + if cls is not None: + result[name] = cls + return result + + def format_data_type_uuid(self, data_type) -> Tuple[str, tuple]: + return "UUID", () + + def format_data_type_custom(self, data_type) -> Tuple[str, tuple]: + return data_type.raw, () + + def format_data_type_array(self, data_type: ArrayType) -> Tuple[str, tuple]: + element_sql, _ = self.format_data_type(data_type.element_type) + return element_sql + "[]" * data_type.dimensions, () + # ------------------------------------------------------------------ # DDLTypeSupport — parsing # ------------------------------------------------------------------ @@ -548,7 +495,7 @@ def parse_type(self, raw: str) -> DataType: base_raw = stripped[:kw_match.start()] element_type = self.parse_type(base_raw) # PG normalises all array declarations to 1-D internally - return PostgresArrayType(element_type, dimensions=1) + return PostgresArrayType(element_type, dimensions=1, dialect=self) # Check for array bracket suffix: e.g. "INTEGER[]", "INTEGER[][]" remaining = stripped @@ -564,7 +511,7 @@ def parse_type(self, raw: str) -> DataType: if remaining != stripped: element_type = self.parse_type(remaining) # PG normalises all array declarations to 1-D internally - return PostgresArrayType(element_type, dimensions=1) + return PostgresArrayType(element_type, dimensions=1, dialect=self) # ---- Standard type dispatch ---- @@ -576,93 +523,93 @@ def parse_type(self, raw: str) -> DataType: PostgresSmallSerialType, ) if upper.startswith("BIGSERIAL") or upper.startswith("SERIAL8"): - return PostgresBigSerialType() + return PostgresBigSerialType(self) if upper.startswith("SMALLSERIAL") or upper.startswith("SERIAL2"): - return PostgresSmallSerialType() - return PostgresSerialType() + return PostgresSmallSerialType(self) + return PostgresSerialType(self) # Integer family if self._PG_INTEGER_TYPES.match(upper): if upper.startswith("SMALLINT") or upper.startswith("INT2"): - return SmallIntType() + return SmallIntType(self) if upper.startswith("BIGINT") or upper.startswith("INT8"): - return BigIntType() - return IntegerType() + return BigIntType(self) + return IntegerType(self) # Float family if self._PG_FLOAT_TYPES.match(upper): if upper.startswith("DOUBLE"): - return DoubleType() + return DoubleType(self) if upper.startswith("REAL") or upper.startswith("FLOAT4"): - return RealType() + return RealType(self) nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None - return FloatType(precision) + return FloatType(precision, self) # Decimal family if self._PG_DECIMAL_TYPES.match(upper): nums = re.findall(r"\d+", stripped) if len(nums) >= 2: - return DecimalType(int(nums[0]), int(nums[1])) + return DecimalType(int(nums[0]), int(nums[1]), self) if len(nums) == 1: - return DecimalType(int(nums[0])) - return DecimalType() + return DecimalType(int(nums[0]), self) + return DecimalType(self) # String family if self._PG_STRING_TYPES.match(upper): length_match = re.search(r"\((\d+)\)", stripped) length = int(length_match.group(1)) if length_match else None if upper.startswith("VARCHAR") or upper.startswith("CHARACTER VARYING"): - return VarCharType(length) + return VarCharType(length, self) if upper.startswith("CHAR") or upper.startswith("CHARACTER"): - return CharType(length) - return TextType() + return CharType(length, self) + return TextType(self) # Binary if self._PG_BINARY_TYPES.match(upper): from ...expression.types import PostgresByteaType - return PostgresByteaType() + return PostgresByteaType(self) # Date/time if self._PG_DATE_TYPES.match(upper): if upper.startswith("DATETIME"): - return DateTimeType() + return DateTimeType(self) if upper.startswith("DATE"): - return DateType() + return DateType(self) if upper.startswith("TIMESTAMP"): nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None if "WITH TIME ZONE" in upper or upper.startswith("TIMESTAMPTZ"): - return TimestampTzType(precision) - return TimestampType(precision) + return TimestampTzType(precision, self) + return TimestampType(precision, self) if upper.startswith("TIME"): nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None if "WITH TIME ZONE" in upper or upper.startswith("TIMETZ"): - return TimeTzType(precision) - return TimeType(precision) + return TimeTzType(precision, self) + return TimeType(precision, self) if upper.startswith("INTERVAL"): fields_match = re.search(r"INTERVAL\s+(.*)", upper) fields = fields_match.group(1).strip() if fields_match else None - return IntervalType(fields) + return IntervalType(fields, self) # JSON if self._PG_JSON_TYPES.match(upper): if upper.startswith("JSONB"): - return JsonBType() + return JsonBType(self) if upper.startswith("JSONPATH"): from ...expression.types import PostgresJsonPathType - return PostgresJsonPathType() - return JsonType() + return PostgresJsonPathType(self) + return JsonType(self) # UUID if self._PG_UUID_TYPES.match(upper): from ...expression.types import PostgresUUIDType - return PostgresUUIDType() + return PostgresUUIDType(self) # Boolean if upper.startswith("BOOLEAN") or upper.startswith("BOOL"): - return BooleanType() + return BooleanType(self) # Bit string if self._PG_BIT_TYPES.match(upper): @@ -670,9 +617,9 @@ def parse_type(self, raw: str) -> DataType: n = int(nums[0]) if nums else None if upper.startswith("BIT"): from ...expression.types import PostgresBitType - return PostgresBitType(n) + return PostgresBitType(n, self) from ...expression.types import PostgresVarBitType - return PostgresVarBitType(n) + return PostgresVarBitType(n, self) # Network address if self._PG_NET_TYPES.match(upper): @@ -683,12 +630,12 @@ def parse_type(self, raw: str) -> DataType: PostgresMacAddrType, ) if upper.startswith("CIDR"): - return PostgresCidrType() + return PostgresCidrType(self) if upper.startswith("MACADDR8"): - return PostgresMacAddr8Type() + return PostgresMacAddr8Type(self) if upper.startswith("MACADDR"): - return PostgresMacAddrType() - return PostgresInetType() + return PostgresMacAddrType(self) + return PostgresInetType(self) # Geometric if self._PG_GEOM_TYPES.match(upper): @@ -713,7 +660,7 @@ def parse_type(self, raw: str) -> DataType: for name, cls in geom_map.items(): if upper.startswith(name): return cls() - return PostgresPointType() + return PostgresPointType(self) # Range types if self._PG_RANGE_TYPES.match(upper): @@ -736,7 +683,7 @@ def parse_type(self, raw: str) -> DataType: for name, cls in range_map.items(): if upper.startswith(name): return cls() - return PostgresInt4RangeType() + return PostgresInt4RangeType(self) # Multirange types if self._PG_MULTIRANGE_TYPES.match(upper): @@ -759,7 +706,7 @@ def parse_type(self, raw: str) -> DataType: for name, cls in mr_map.items(): if upper.startswith(name): return cls() - return PostgresInt4MultirangeType() + return PostgresInt4MultirangeType(self) # OID types if self._PG_OID_TYPES.match(upper): @@ -784,7 +731,7 @@ def parse_type(self, raw: str) -> DataType: for name, cls in oid_map.items(): if upper.startswith(name): return cls() - return PostgresOIDType() + return PostgresOIDType(self) # Text search if self._PG_TS_TYPES.match(upper): @@ -793,8 +740,8 @@ def parse_type(self, raw: str) -> DataType: PostgresTSVectorType, ) if upper.startswith("TSVECTOR"): - return PostgresTSVectorType() - return PostgresTSQueryType() + return PostgresTSVectorType(self) + return PostgresTSQueryType(self) # Miscellaneous if self._PG_MISC_TYPES.match(upper): @@ -807,17 +754,17 @@ def parse_type(self, raw: str) -> DataType: PostgresXMLType, ) if upper.startswith("MONEY"): - return PostgresMoneyType() + return PostgresMoneyType(self) if upper.startswith("XML"): - return PostgresXMLType() + return PostgresXMLType(self) if upper.startswith("PG_LSN"): - return PostgresPgLSNType() + return PostgresPgLSNType(self) if upper.startswith("HSTORE"): - return PostgresHstoreType() + return PostgresHstoreType(self) if upper.startswith("GEOGRAPHY"): - return PostgresGeographyType() + return PostgresGeographyType(self) if upper.startswith("GEOMETRY"): - return PostgresGeometryType() + return PostgresGeometryType(self) # Vector types (pgvector) vector_match = re.match(r"^(VECTOR|HALFVEC|SPARSEVEC)\b", upper) @@ -836,13 +783,13 @@ def parse_type(self, raw: str) -> DataType: vector_kind = vector_match.group(1) if vector_kind == "HALFVEC": from ...expression.types import PostgresHalfvecType - return PostgresHalfvecType(dim) + return PostgresHalfvecType(dim, self) if vector_kind == "SPARSEVEC": from ...expression.types import PostgresSparsevecType - return PostgresSparsevecType(dim) + return PostgresSparsevecType(dim, self) from ...expression.types import PostgresVectorType - return PostgresVectorType(dim) + return PostgresVectorType(dim, self) # Fallback from rhosocial.activerecord.backend.expression.types import CustomType - return CustomType(stripped) \ No newline at end of file + return CustomType(stripped, self) \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py index af5381c0..accea382 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py @@ -5,7 +5,7 @@ enumerated type operations. """ -from typing import Optional, List, Tuple, TYPE_CHECKING, Union +from typing import Optional, List, Tuple, TYPE_CHECKING if TYPE_CHECKING: from rhosocial.activerecord.backend.impl.postgres.expression.ddl.type import ( @@ -188,135 +188,84 @@ def alter_enum_add_value( def format_create_enum_type( self, - expr_or_name: Union["PostgresCreateEnumTypeExpression", str], - values: Optional[List[str]] = None, - schema: Optional[str] = None, - if_not_exists: bool = False, - ) -> Union[str, Tuple[str, tuple]]: - """Format CREATE TYPE for either the legacy or expression-based API. - - **Expression mode** — if ``expr_or_name`` has a ``values`` attribute, - its ``.name``, ``.values``, ``.schema``, and ``.if_not_exists`` are - used and the result is wrapped in ``(sql, ())``. - - **Legacy mode** — ``expr_or_name`` is treated as a type name string; - ``values``, ``schema``, and ``if_not_exists`` must be supplied as - keyword arguments. + expr: "PostgresCreateEnumTypeExpression", + ) -> Tuple[str, tuple]: + """Format CREATE TYPE ... AS ENUM from its expression node. Args: - expr_or_name: Expression instance or type name string. - values: List of enum values (required in legacy mode). - schema: Optional schema name. - if_not_exists: Add ``IF NOT EXISTS``. + expr: :class:`PostgresCreateEnumTypeExpression` instance. Returns: - Tuple of (SQL string, params tuple) in expression mode, - plain SQL string in legacy mode. - - Raises: - TypeError: If ``values`` is ``None`` in legacy mode. + Tuple of (SQL string, params tuple). """ - if hasattr(expr_or_name, "values"): - sql = self.format_create_enum_type_raw( - expr_or_name.name, - expr_or_name.values, - expr_or_name.schema, - expr_or_name.if_not_exists, - ) - return (sql, ()) - - if values is None: - raise TypeError("values must be provided when formatting enum type by name") - - return self.format_create_enum_type_raw(expr_or_name, values, schema, if_not_exists) + sql = self.format_create_enum_type_raw( + expr.name, + expr.values, + expr.schema, + expr.if_not_exists, + ) + return (sql, ()) def format_drop_enum_type( self, - expr_or_name: Union["PostgresDropEnumTypeExpression", str], - schema: Optional[str] = None, - if_exists: bool = False, - cascade: bool = False, - ) -> Union[str, Tuple[str, tuple]]: - """Format DROP TYPE for either the legacy or expression-based API. - - **Expression mode** — if ``expr_or_name`` has an ``if_exists`` attribute, - its ``.name``, ``.schema``, ``.if_exists``, and ``.cascade`` are used - and the result is wrapped in ``(sql, ())``. - - **Legacy mode** — ``expr_or_name`` is treated as a type name string; - ``schema``, ``if_exists``, and ``cascade`` must be supplied as keyword - arguments. + expr: "PostgresDropEnumTypeExpression", + ) -> Tuple[str, tuple]: + """Format DROP TYPE from its expression node. Args: - expr_or_name: Expression instance or type name string. - schema: Optional schema name. - if_exists: Add ``IF EXISTS``. - cascade: Add ``CASCADE``. + expr: :class:`PostgresDropEnumTypeExpression` instance. Returns: - Tuple of (SQL string, params tuple) in expression mode, - plain SQL string in legacy mode. + Tuple of (SQL string, params tuple). """ - if hasattr(expr_or_name, "if_exists"): - sql = self.format_drop_enum_type_raw( - expr_or_name.name, - expr_or_name.schema, - expr_or_name.if_exists, - expr_or_name.cascade, - ) - return (sql, ()) - - return self.format_drop_enum_type_raw(expr_or_name, schema, if_exists, cascade) + sql = self.format_drop_enum_type_raw( + expr.name, + expr.schema, + expr.if_exists, + expr.cascade, + ) + return (sql, ()) def format_alter_enum_add_value( self, - expr_or_type_name: Union["PostgresAlterEnumAddValueExpression", str], - new_value: Optional[str] = None, - schema: Optional[str] = None, - before: Optional[str] = None, - after: Optional[str] = None, - ) -> Union[str, Tuple[str, tuple]]: - """Format ALTER TYPE ADD VALUE for either the legacy or expression-based API. + expr: "PostgresAlterEnumAddValueExpression", + ) -> Tuple[str, tuple]: + """Format ALTER TYPE ADD VALUE from its expression node. - **Expression mode** — if ``expr_or_type_name`` has a ``new_value`` attribute, - its ``.type_name``, ``.new_value``, ``.schema``, ``.before``, and ``.after`` - are used and the result is wrapped in ``(sql, ())``. + Args: + expr: :class:`PostgresAlterEnumAddValueExpression` instance. - **Legacy mode** — ``expr_or_type_name`` is treated as a type name string; - ``new_value``, ``schema``, ``before``, and ``after`` must be supplied as - keyword arguments. + Returns: + Tuple of (SQL string, params tuple). + + """ + sql = self.format_alter_enum_add_value_raw( + expr.type_name, + expr.new_value, + expr.schema, + expr.before, + expr.after, + ) + return (sql, ()) + + def format_enum_type_expression(self, expr) -> Tuple[str, tuple]: + """Format a :class:`PostgresEnumType` type reference expression. + + The reference is schema-qualified when the expression carries a + schema: ``"schema"."name"`` / ``"name"``. Args: - expr_or_type_name: Expression instance or type name string. - new_value: New enum value (required in legacy mode). - schema: Optional schema name. - before: Insert before this existing value. - after: Insert after this existing value. + expr: :class:`PostgresEnumType` instance. Returns: - Tuple of (SQL string, params tuple) in expression mode, - plain SQL string in legacy mode. - - Raises: - TypeError: If ``new_value`` is ``None`` in legacy mode. + Tuple of (SQL string, empty params tuple). """ - if hasattr(expr_or_type_name, "new_value"): - sql = self.format_alter_enum_add_value_raw( - expr_or_type_name.type_name, - expr_or_type_name.new_value, - expr_or_type_name.schema, - expr_or_type_name.before, - expr_or_type_name.after, - ) - return (sql, ()) - - if new_value is None: - raise TypeError("new_value must be provided when formatting enum value by name") - - return self.format_alter_enum_add_value_raw(expr_or_type_name, new_value, schema, before, after) + if expr.schema: + return (f"{expr.schema}.{expr.name}", ()) + return (expr.name, ()) def format_alter_enum_type_add_value(self, expr) -> Tuple[str, tuple]: """Format ALTER TYPE ADD VALUE statement from expression object. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py index 08b35287..53c2e236 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py @@ -34,10 +34,6 @@ def format_json_expression(self, expr: "JSONExpression"): sql = value_sql params = col_params + (expr.path,) - if expr.cast_types: - for target_type in expr.cast_types: - sql, params = self.format_cast_expression(sql, target_type, params, None) - if expr.alias: sql = f"{sql} AS {self.format_identifier(expr.alias)}" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/types/enum.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/types/enum.py index 03645bc8..19f44b14 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/types/enum.py @@ -4,7 +4,7 @@ This module defines the protocol for PostgreSQL-specific ENUM type management. """ -from typing import Protocol, runtime_checkable, List, Optional, Tuple, Union, TYPE_CHECKING +from typing import Protocol, runtime_checkable, List, Optional, Tuple, TYPE_CHECKING if TYPE_CHECKING: from ...expression.ddl.type import ( @@ -108,21 +108,15 @@ def format_enum_values(self, values: List[str]) -> str: def format_create_enum_type( self, - expr_or_name: Union["PostgresCreateEnumTypeExpression", str], - values: Optional[List[str]] = None, - schema: Optional[str] = None, - if_not_exists: bool = False, - ) -> Union[str, Tuple[str, tuple]]: - """Format CREATE TYPE for either the legacy or expression-based API. + expr: "PostgresCreateEnumTypeExpression", + ) -> Tuple[str, tuple]: + """Format CREATE TYPE from its expression node. Args: - expr_or_name: PostgresCreateEnumTypeExpression instance or type name string - values: List of allowed values (required when expr_or_name is a string) - schema: Optional schema name - if_not_exists: Add IF NOT EXISTS clause + expr: PostgresCreateEnumTypeExpression instance Returns: - SQL statement string or tuple of (SQL string, params tuple) + Tuple of (SQL string, params tuple) """ ... @@ -144,21 +138,15 @@ def format_create_enum_type_raw( def format_drop_enum_type( self, - expr_or_name: Union["PostgresDropEnumTypeExpression", str], - schema: Optional[str] = None, - if_exists: bool = False, - cascade: bool = False, - ) -> Union[str, Tuple[str, tuple]]: - """Format DROP TYPE for either the legacy or expression-based API. + expr: "PostgresDropEnumTypeExpression", + ) -> Tuple[str, tuple]: + """Format DROP TYPE from its expression node. Args: - expr_or_name: PostgresDropEnumTypeExpression instance or type name string - schema: Optional schema name - if_exists: Add IF EXISTS clause - cascade: Add CASCADE clause + expr: PostgresDropEnumTypeExpression instance Returns: - SQL statement string or tuple of (SQL string, params tuple) + Tuple of (SQL string, params tuple) """ ... @@ -180,23 +168,26 @@ def format_drop_enum_type_raw( def format_alter_enum_add_value( self, - expr_or_type_name: Union["PostgresAlterEnumAddValueExpression", str], - new_value: Optional[str] = None, - schema: Optional[str] = None, - before: Optional[str] = None, - after: Optional[str] = None, - ) -> Union[str, Tuple[str, tuple]]: - """Format ALTER TYPE ADD VALUE for either the legacy or expression-based API. + expr: "PostgresAlterEnumAddValueExpression", + ) -> Tuple[str, tuple]: + """Format ALTER TYPE ADD VALUE from its expression node. Args: - expr_or_type_name: PostgresAlterEnumAddValueExpression instance or type name string - new_value: New value to add (required when expr_or_type_name is a string) - schema: Optional schema name - before: Add before this value - after: Add after this value + expr: PostgresAlterEnumAddValueExpression instance + + Returns: + Tuple of (SQL string, params tuple) + """ + ... + + def format_enum_type_expression(self, expr) -> Tuple[str, tuple]: + """Format a PostgresEnumType type reference expression. + + Args: + expr: PostgresEnumType instance Returns: - SQL statement string or tuple of (SQL string, params tuple) + Tuple of (SQL string, params tuple) """ ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py b/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py index e4c98cea..c4aa4b6d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py @@ -113,23 +113,10 @@ def schema(self) -> Optional[str]: """Schema name, if any.""" return self._schema - def to_sql(self) -> Tuple[str, tuple]: - """Generate SQL type reference for use in column definitions. - - Returns: - Tuple of (SQL string, empty params tuple) - - Examples: - >>> status_enum.to_sql() - ('video_status', ()) - - >>> # With schema - >>> status_enum.to_sql() - ('app.video_status', ()) - """ - if self._schema: - return (f"{self._schema}.{self._name}", ()) - return (self._name, ()) + @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. @@ -167,7 +154,9 @@ def from_python_enum( return cls(dialect=dialect, name=type_name, values=values, schema=schema) def __str__(self) -> str: - return self.to_sql()[0] + if self._schema: + return f"{self._schema}.{self._name}" + return self._name def __repr__(self) -> str: return ( diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py index aaea9129..36e3b436 100644 --- a/tests/providers/fixtures/basic.py +++ b/tests/providers/fixtures/basic.py @@ -57,21 +57,21 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpress table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("username", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("age", IntegerType()), - ColumnDefinition("balance", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("is_active", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "balance", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "is_active", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], ) @@ -86,33 +86,33 @@ def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTa table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresUUIDType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("username", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("tiny_int", SmallIntType()), - ColumnDefinition("small_int", SmallIntType()), - ColumnDefinition("big_int", BigIntType()), - ColumnDefinition("float_val", FloatType()), - ColumnDefinition("double_val", DoubleType()), - ColumnDefinition("decimal_val", DecimalType(precision=10, scale=4)), - ColumnDefinition("char_val", VarCharType(255)), - ColumnDefinition("varchar_val", VarCharType(255)), - ColumnDefinition("text_val", TextType()), - ColumnDefinition("date_val", DateType()), - ColumnDefinition("time_val", TimeTzType()), - ColumnDefinition("timestamp_val", TimestampTzType()), - ColumnDefinition("blob_val", PostgresByteaType()), - ColumnDefinition("json_val", JsonBType()), - ColumnDefinition("array_val", ArrayType(IntegerType())), - ColumnDefinition("is_active", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition(dialect, "id", PostgresUUIDType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "tiny_int", SmallIntType(dialect=dialect)), + ColumnDefinition(dialect, "small_int", SmallIntType(dialect=dialect)), + ColumnDefinition(dialect, "big_int", BigIntType(dialect=dialect)), + ColumnDefinition(dialect, "float_val", FloatType(dialect=dialect)), + ColumnDefinition(dialect, "double_val", DoubleType(dialect=dialect)), + ColumnDefinition(dialect, "decimal_val", DecimalType(precision=10, scale=4, dialect=dialect)), + ColumnDefinition(dialect, "char_val", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "varchar_val", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "text_val", TextType(dialect=dialect)), + ColumnDefinition(dialect, "date_val", DateType(dialect=dialect)), + ColumnDefinition(dialect, "time_val", TimeTzType(dialect=dialect)), + ColumnDefinition(dialect, "timestamp_val", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "blob_val", PostgresByteaType(dialect=dialect)), + ColumnDefinition(dialect, "json_val", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "array_val", ArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, "is_active", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), ], table_constraints=[ - TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + TableConstraint(dialect, constraint_type=TableConstraintType.PRIMARY_KEY, columns=["id"]), ], ) @@ -128,25 +128,25 @@ def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTa table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresUUIDType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("string_field", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("int_field", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("float_field", FloatType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("decimal_field", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("bool_field", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("datetime_field", TimestampTzType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("json_field", JsonBType()), - ColumnDefinition("nullable_field", VarCharType(255)), + ColumnDefinition(dialect, "id", PostgresUUIDType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "string_field", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "int_field", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "float_field", FloatType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "decimal_field", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "bool_field", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "datetime_field", TimestampTzType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "json_field", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "nullable_field", VarCharType(255, dialect=dialect)), ], table_constraints=[ - TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + TableConstraint(dialect, constraint_type=TableConstraintType.PRIMARY_KEY, columns=["id"]), ], ) @@ -162,24 +162,24 @@ def create_validated_field_users_table(dialect, table_name: str = "validated_fie table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("username", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("age", IntegerType()), - ColumnDefinition("balance", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("credit_score", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=300)]), - ColumnDefinition("status", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="active")]), - ColumnDefinition("is_active", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "balance", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "credit_score", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=300)]), + ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="active")]), + ColumnDefinition(dialect, "is_active", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), ], ) @@ -194,13 +194,13 @@ def create_validated_users_table(dialect, table_name: str = "validated_users") - table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("username", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("age", IntegerType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "username", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), ], ) @@ -215,17 +215,17 @@ def create_pydantic_validated_models_table(dialect, table_name: str = "pydantic_ table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("code", VarCharType(32)), - ColumnDefinition("quantity", IntegerType()), - ColumnDefinition("step_count", IntegerType()), - ColumnDefinition("price", DecimalType(precision=10, scale=2)), - ColumnDefinition("start_at", TimestampTzType()), - ColumnDefinition("end_at", TimestampTzType()), - ColumnDefinition("status", VarCharType(32)), - ColumnDefinition("normalized_name", VarCharType(50)), - ColumnDefinition("created_token", VarCharType(255)), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "code", VarCharType(32, dialect=dialect)), + ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "step_count", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "price", DecimalType(precision=10, scale=2, dialect=dialect)), + ColumnDefinition(dialect, "start_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "end_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "status", VarCharType(32, dialect=dialect)), + ColumnDefinition(dialect, "normalized_name", VarCharType(50, dialect=dialect)), + ColumnDefinition(dialect, "created_token", VarCharType(255, dialect=dialect)), ], ) @@ -240,14 +240,14 @@ def create_bulk_users_table(dialect, table_name: str = "bulk_users") -> CreateTa table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("age", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="")]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "age", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="")]), ], ) @@ -262,21 +262,21 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("author", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("title", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("content", TextType()), - ColumnDefinition("published_at", TimestampTzType()), - ColumnDefinition("published", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "author", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "published_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "published", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -292,24 +292,24 @@ def create_comments_table(dialect, table_name: str = "comments") -> CreateTableE table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("post_ref", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("author", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("text", TextType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampTzType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("updated_at", TimestampTzType()), - ColumnDefinition("approved", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "post_ref", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "author", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "text", TextType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "approved", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=False)]), ], table_constraints=[ - ForeignKeyConstraint(columns=["post_ref"], foreign_key_table="posts", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["post_ref"], foreign_key_table="posts", foreign_key_columns=["id"], on_delete=_CASCADE), - ForeignKeyConstraint(columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -325,13 +325,13 @@ def create_column_mapping_items_table(dialect, table_name: str = "column_mapping table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("item_total", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("remarks", IntegerType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "item_total", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "remarks", IntegerType(dialect=dialect)), ], ) @@ -346,14 +346,14 @@ def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annota table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("tags", ArrayType(TextType())), - ColumnDefinition("meta", TextType()), - ColumnDefinition("description", TextType()), - ColumnDefinition("status", TextType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "tags", ArrayType(TextType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, "meta", TextType(dialect=dialect)), + ColumnDefinition(dialect, "description", TextType(dialect=dialect)), + ColumnDefinition(dialect, "status", TextType(dialect=dialect)), ], ) @@ -368,17 +368,17 @@ def create_type_adapter_tests_table(dialect, table_name: str = "type_adapter_tes table=table_name, if_not_exists=True, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("optional_name", VarCharType(255)), - ColumnDefinition("optional_age", IntegerType()), - ColumnDefinition("last_login", TimestampTzType()), - ColumnDefinition("is_premium", BooleanType()), - ColumnDefinition("unsupported_union", VarCharType(255)), - ColumnDefinition("custom_bool", VarCharType(10)), - ColumnDefinition("optional_custom_bool", VarCharType(10)), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "optional_name", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "optional_age", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "last_login", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "is_premium", BooleanType(dialect=dialect)), + ColumnDefinition(dialect, "unsupported_union", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "custom_bool", VarCharType(10, dialect=dialect)), + ColumnDefinition(dialect, "optional_custom_bool", VarCharType(10, dialect=dialect)), ], ) @@ -393,18 +393,18 @@ def create_composite_pk_order_items_table(dialect, table_name: str = "order_item table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("order_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("product_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("quantity", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), - ColumnDefinition("unit_price", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "order_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "product_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition(dialect, "unit_price", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], table_constraints=[ - TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + TableConstraint(dialect, constraint_type=TableConstraintType.PRIMARY_KEY, columns=["order_id", "product_id"]), ], ) @@ -420,18 +420,18 @@ def create_store_inventory_table(dialect, table_name: str = "store_inventory") - table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("store_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("product_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("batch_id", VarCharType(64), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("stock", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition(dialect, "store_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "product_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "batch_id", VarCharType(64, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "stock", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0)]), ], table_constraints=[ - TableConstraint(constraint_type=TableConstraintType.PRIMARY_KEY, + TableConstraint(dialect, constraint_type=TableConstraintType.PRIMARY_KEY, columns=["store_id", "product_id", "batch_id"]), ], ) @@ -447,12 +447,12 @@ def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpre table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("total", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TextType()), - ColumnDefinition("updated_at", TextType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "total", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TextType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TextType(dialect=dialect)), ], ) @@ -467,14 +467,14 @@ def create_product_table(dialect, table_name: str = "product") -> CreateTableExp table=table_name, if_not_exists=True, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", TextType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("price", FloatType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("quantity", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", TextType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "price", FloatType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], ) diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py index 39960fb5..159ec651 100644 --- a/tests/providers/fixtures/events.py +++ b/tests/providers/fixtures/events.py @@ -35,19 +35,19 @@ def create_event_tests_table(dialect, table_name: str = "event_tests") -> Create table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("status", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="draft")]), - ColumnDefinition("revision", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), - ColumnDefinition("content", TextType()), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="draft")]), + ColumnDefinition(dialect, "revision", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], ) @@ -62,16 +62,16 @@ def create_event_tracking_models_table(dialect, table_name: str = "event_trackin table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("title", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("content", TextType()), - ColumnDefinition("view_count", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0)]), - ColumnDefinition("last_viewed_at", TimestampTzType(), - constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "view_count", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0)]), + ColumnDefinition(dialect, "last_viewed_at", TimestampTzType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NULL)]), ], ) diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py index b7c8f832..2c463bd3 100644 --- a/tests/providers/fixtures/mixins.py +++ b/tests/providers/fixtures/mixins.py @@ -37,21 +37,21 @@ def create_combined_articles_table(dialect, table_name: str = "combined_articles table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("title", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("content", TextType()), - ColumnDefinition("status", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="draft")]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), - ColumnDefinition("version", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), - ColumnDefinition("deleted_at", TimestampTzType(), - constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="draft")]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "version", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition(dialect, "deleted_at", TimestampTzType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NULL)]), ], ) @@ -66,15 +66,15 @@ def create_tasks_table(dialect, table_name: str = "tasks") -> CreateTableExpress table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("title", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("is_completed", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), - ColumnDefinition("deleted_at", TimestampTzType(), - constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "is_completed", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition(dialect, "deleted_at", TimestampTzType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NULL)]), ], ) @@ -89,13 +89,13 @@ def create_timestamped_posts_table(dialect, table_name: str = "timestamped_posts table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("title", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("content", TextType()), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], ) @@ -110,16 +110,16 @@ def create_versioned_products_table(dialect, table_name: str = "versioned_produc table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("price", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("version", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "price", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "version", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), ], ) diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py index 86e279fc..efeaaf77 100644 --- a/tests/providers/fixtures/query.py +++ b/tests/providers/fixtures/query.py @@ -42,21 +42,21 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpress table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("username", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("age", IntegerType()), - ColumnDefinition("balance", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("is_active", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "balance", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "is_active", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], ) @@ -71,21 +71,21 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("title", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("content", TextType()), - ColumnDefinition("status", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="published")]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="published")]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -101,23 +101,23 @@ def create_comments_table(dialect, table_name: str = "comments") -> CreateTableE table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("post_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("content", TextType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("is_hidden", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=False)]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "post_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "content", TextType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "is_hidden", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=False)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], on_delete=_CASCADE), - ForeignKeyConstraint(columns=["post_id"], foreign_key_table="posts", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["post_id"], foreign_key_table="posts", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -133,23 +133,23 @@ def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpre table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("order_number", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("total_amount", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("status", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="pending")]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "order_number", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "total_amount", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="pending")]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), ], ) @@ -164,25 +164,25 @@ def create_order_items_table(dialect, table_name: str = "order_items") -> Create table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("order_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("product_name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("quantity", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), - ColumnDefinition("unit_price", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("subtotal", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "order_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "product_name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition(dialect, "unit_price", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "subtotal", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["order_id"], foreign_key_table="orders", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["order_id"], foreign_key_table="orders", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -198,17 +198,17 @@ def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableE table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("bio", TextType()), - ColumnDefinition("avatar_url", VarCharType(512)), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "bio", TextType(dialect=dialect)), + ColumnDefinition(dialect, "avatar_url", VarCharType(512, dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), ], ) @@ -223,22 +223,22 @@ def create_json_users_table(dialect, table_name: str = "json_users") -> CreateTa table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("username", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("email", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("age", IntegerType()), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), - ColumnDefinition("settings", JsonBType()), - ColumnDefinition("tags", JsonBType()), - ColumnDefinition("profile", JsonBType()), - ColumnDefinition("roles", JsonBType()), - ColumnDefinition("scores", JsonBType()), - ColumnDefinition("subscription", JsonBType()), - ColumnDefinition("preferences", JsonBType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "settings", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "tags", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "profile", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "roles", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "scores", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "subscription", JsonBType(dialect=dialect)), + ColumnDefinition(dialect, "preferences", JsonBType(dialect=dialect)), ], ) @@ -253,20 +253,20 @@ def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpress table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("parent_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NULL)]), - ColumnDefinition("value", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "parent_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NULL)]), + ColumnDefinition(dialect, "value", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["parent_id"], foreign_key_table="nodes", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["parent_id"], foreign_key_table="nodes", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -282,12 +282,12 @@ def create_searchable_items_table(dialect, table_name: str = "searchable_items") table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(255)), - ColumnDefinition("tags", TextType()), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "tags", TextType(dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], ) @@ -302,34 +302,34 @@ def create_extended_orders_table(dialect, table_name: str = "extended_orders") - table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("order_number", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("total_amount", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition("status", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="pending")]), - ColumnDefinition("priority", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="medium")]), - ColumnDefinition("region", VarCharType(50), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value="default")]), - ColumnDefinition("category", VarCharType(255)), - ColumnDefinition("product", VarCharType(255)), - ColumnDefinition("department", VarCharType(255)), - ColumnDefinition("year", VarCharType(10)), - ColumnDefinition("quarter", VarCharType(10)), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "order_number", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "total_amount", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), + ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="pending")]), + ColumnDefinition(dialect, "priority", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="medium")]), + ColumnDefinition(dialect, "region", VarCharType(50, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="default")]), + ColumnDefinition(dialect, "category", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "product", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "department", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "year", VarCharType(10, dialect=dialect)), + ColumnDefinition(dialect, "quarter", VarCharType(10, dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -345,24 +345,24 @@ def create_extended_order_items_table(dialect, table_name: str = "extended_order table=table_name, if_not_exists=False, columns=[ - ColumnDefinition("id", PostgresSerialType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("order_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("product_name", VarCharType(255), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("quantity", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=1)]), - ColumnDefinition("price", DecimalType(precision=10, scale=2), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("category", VarCharType(255)), - ColumnDefinition("region", VarCharType(50)), - ColumnDefinition("created_at", TimestampTzType()), - ColumnDefinition("updated_at", TimestampTzType()), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "order_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "product_name", VarCharType(255, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), + ColumnDefinition(dialect, "price", DecimalType(precision=10, scale=2, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "category", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "region", VarCharType(50, dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), + ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(columns=["order_id"], foreign_key_table="extended_orders", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["order_id"], foreign_key_table="extended_orders", foreign_key_columns=["id"], on_delete=_CASCADE), ], ) diff --git a/tests/providers/partition.py b/tests/providers/partition.py index 64a73d68..4bc494e4 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -252,11 +252,11 @@ def _create_parent_sql(self, dialect): dialect=dialect, table=self.TABLE_NAME, columns=[ - ColumnDefinition("id", BigIntType(), dialect_options={"identity": "BY DEFAULT"}), - ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("tenant_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType()), - ColumnDefinition("amount", DecimalType(12, 2)), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), dialect_options={"identity": "BY DEFAULT"}), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "tenant_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect)), + ColumnDefinition(dialect, "amount", DecimalType(12, 2, dialect=dialect)), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_alter_table_if_exists.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_alter_table_if_exists.py index 5fee2aa0..38b0a734 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_alter_table_if_exists.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_alter_table_if_exists.py @@ -40,7 +40,7 @@ class TestPostgresAddColumnIfNotExists: def test_if_not_exists_renders_qualifier(self, dialect): action = AddColumn( dialect, - ColumnDefinition("content", TextType()), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), if_not_exists=True, ) sql, params = action.to_sql() @@ -48,7 +48,7 @@ def test_if_not_exists_renders_qualifier(self, dialect): assert params == () def test_none_renders_plain_form(self, dialect): - action = AddColumn(dialect, ColumnDefinition("content", TextType())) + action = AddColumn(dialect, ColumnDefinition(dialect, "content", TextType(dialect=dialect))) sql, params = action.to_sql() assert 'ADD COLUMN "content" TEXT' == sql assert "IF NOT EXISTS" not in sql @@ -57,7 +57,7 @@ def test_none_renders_plain_form(self, dialect): def test_inside_alter_table(self, dialect): action = AddColumn( dialect, - ColumnDefinition("content", TextType()), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), if_not_exists=True, ) expr = AlterTableExpression( @@ -224,7 +224,7 @@ def test_unlogged_renders_qualifier(self, dialect): expr = CreateTableExpression( dialect, table="audit", - columns=[ColumnDefinition("id", TextType())], + columns=[ColumnDefinition(dialect, "id", TextType(dialect=dialect))], dialect_options={"unlogged_table": True}, ) sql, params = expr.to_sql() @@ -239,7 +239,7 @@ def test_plain_omits_qualifier(self, dialect): expr = CreateTableExpression( dialect, table="audit", - columns=[ColumnDefinition("id", TextType())], + columns=[ColumnDefinition(dialect, "id", TextType(dialect=dialect))], ) sql, params = expr.to_sql() assert not sql.startswith("CREATE UNLOGGED") @@ -253,7 +253,7 @@ def test_temporary_wins_over_unlogged(self, dialect): expr = CreateTableExpression( dialect, table="audit", - columns=[ColumnDefinition("id", TextType())], + columns=[ColumnDefinition(dialect, "id", TextType(dialect=dialect))], temporary=True, dialect_options={"unlogged_table": True}, ) @@ -273,7 +273,7 @@ def test_version_gate_94(self): expr = CreateTableExpression( low, table="audit", - columns=[ColumnDefinition("id", TextType())], + columns=[ColumnDefinition(low, "id", TextType(dialect=low))], dialect_options={"unlogged_table": True}, ) with pytest.raises(UnsupportedFeatureError): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py index b0509aff..2badd88a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py @@ -61,14 +61,14 @@ def bloom_env(postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="email", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="email", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -165,13 +165,13 @@ def test_bloom_index_with_options(self, bloom_env): # Create temporary table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="code", data_type=TextType()), + ColumnDefinition(dialect, name="code", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -238,14 +238,14 @@ async def async_bloom_env(async_postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="email", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="email", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -342,13 +342,13 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="code", data_type=TextType()), + ColumnDefinition(dialect, name="code", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py index a6fd3461..e6adbf55 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py @@ -59,15 +59,15 @@ def btree_gin_env(postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="value", data_type=IntegerType()), - ColumnDefinition(name="tags", data_type=PostgresArrayType(TextType())), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="value", data_type=IntegerType(dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(TextType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -214,15 +214,15 @@ async def async_btree_gin_env(async_postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="value", data_type=IntegerType()), - ColumnDefinition(name="tags", data_type=PostgresArrayType(TextType())), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="value", data_type=IntegerType(dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(TextType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py index 8e6fc040..6b66c2d5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py @@ -61,15 +61,15 @@ def btree_gist_env(postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="created_at", data_type=TimestampType()), - ColumnDefinition(name="value", data_type=IntegerType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="created_at", data_type=TimestampType(dialect=dialect)), + ColumnDefinition(dialect, name="value", data_type=IntegerType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -225,15 +225,15 @@ async def async_btree_gist_env(async_postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="created_at", data_type=TimestampType()), - ColumnDefinition(name="value", data_type=IntegerType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="created_at", data_type=TimestampType(dialect=dialect)), + ColumnDefinition(dialect, name="value", data_type=IntegerType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py index 19277faf..ed508c4a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py @@ -54,13 +54,13 @@ def _setup_citext_table(backend, dialect, table): """Create and populate the basic citext test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=PostgresCitextType()), + ColumnDefinition(dialect, name="name", data_type=PostgresCitextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -88,17 +88,17 @@ def _setup_citext_unique_table(backend, dialect, table): """Create and populate the citext unique constraint test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( - name="email", - data_type=PostgresCitextType(), + dialect, name="email", + data_type=PostgresCitextType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.UNIQUE), + ColumnConstraint(dialect, ColumnConstraintType.UNIQUE), ], ), ] @@ -128,13 +128,13 @@ def _setup_citext_like_table(backend, dialect, table): """Create and populate the citext LIKE test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=PostgresCitextType()), + ColumnDefinition(dialect, name="name", data_type=PostgresCitextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -165,13 +165,13 @@ def _setup_citext_users_table(backend, dialect, table): """Create and populate the citext users test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type=PostgresCitextType()), + ColumnDefinition(dialect, name="username", data_type=PostgresCitextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -199,14 +199,14 @@ def _setup_citext_roles_table(backend, dialect, table): """Create and populate the citext roles test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type=PostgresCitextType()), - ColumnDefinition(name="role", data_type=TextType()), + ColumnDefinition(dialect, name="username", data_type=PostgresCitextType(dialect=dialect)), + ColumnDefinition(dialect, name="role", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -245,13 +245,13 @@ async def _async_setup_citext_table(backend, dialect, table): """Async: create and populate the basic citext test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=PostgresCitextType()), + ColumnDefinition(dialect, name="name", data_type=PostgresCitextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -279,17 +279,17 @@ async def _async_setup_citext_unique_table(backend, dialect, table): """Async: create and populate the citext unique constraint test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( - name="email", - data_type=PostgresCitextType(), + dialect, name="email", + data_type=PostgresCitextType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.UNIQUE), + ColumnConstraint(dialect, ColumnConstraintType.UNIQUE), ], ), ] @@ -319,13 +319,13 @@ async def _async_setup_citext_like_table(backend, dialect, table): """Async: create and populate the citext LIKE test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=PostgresCitextType()), + ColumnDefinition(dialect, name="name", data_type=PostgresCitextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -356,13 +356,13 @@ async def _async_setup_citext_users_table(backend, dialect, table): """Async: create and populate the citext users test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type=PostgresCitextType()), + ColumnDefinition(dialect, name="username", data_type=PostgresCitextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -390,14 +390,14 @@ async def _async_setup_citext_roles_table(backend, dialect, table): """Async: create and populate the citext roles test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type=PostgresCitextType()), - ColumnDefinition(name="role", data_type=TextType()), + ColumnDefinition(dialect, name="username", data_type=PostgresCitextType(dialect=dialect)), + ColumnDefinition(dialect, name="role", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py index d2966b0d..1998ebbb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py @@ -66,13 +66,13 @@ def cube_env(postgres_backend_single): table="test_cubes", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type=PostgresCubeType()), + ColumnDefinition(dialect, name="c", data_type=PostgresCubeType(dialect=dialect)), ], if_not_exists=True, ) @@ -101,13 +101,13 @@ def cube_env(postgres_backend_single): table="test_cube_dist", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type=PostgresCubeType()), + ColumnDefinition(dialect, name="c", data_type=PostgresCubeType(dialect=dialect)), ], if_not_exists=True, ) @@ -136,13 +136,13 @@ def cube_env(postgres_backend_single): table="test_cube_idx", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type=PostgresCubeType()), + ColumnDefinition(dialect, name="c", data_type=PostgresCubeType(dialect=dialect)), ], if_not_exists=True, ) @@ -352,13 +352,13 @@ async def async_cube_env(async_postgres_backend_single): table="test_cubes_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type=PostgresCubeType()), + ColumnDefinition(dialect, name="c", data_type=PostgresCubeType(dialect=dialect)), ], if_not_exists=True, ) @@ -387,13 +387,13 @@ async def async_cube_env(async_postgres_backend_single): table="test_cube_dist_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type=PostgresCubeType()), + ColumnDefinition(dialect, name="c", data_type=PostgresCubeType(dialect=dialect)), ], if_not_exists=True, ) @@ -422,13 +422,13 @@ async def async_cube_env(async_postgres_backend_single): table="test_cube_idx_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type=PostgresCubeType()), + ColumnDefinition(dialect, name="c", data_type=PostgresCubeType(dialect=dialect)), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py index 90298a2c..b6a0a2f4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py @@ -55,13 +55,13 @@ def earthdistance_env(postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="loc", data_type=PostgresPointType()), + ColumnDefinition(dialect, name="loc", data_type=PostgresPointType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -167,13 +167,13 @@ async def async_earthdistance_env(async_postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="loc", data_type=PostgresPointType()), + ColumnDefinition(dialect, name="loc", data_type=PostgresPointType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py index 64be7549..e8add2f1 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py @@ -54,13 +54,13 @@ def _setup_lev_table(backend, dialect, table): """Create and populate the levenshtein test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="word", data_type=TextType()), + ColumnDefinition(dialect, name="word", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -91,13 +91,13 @@ def _setup_diff_table(backend, dialect, table): """Create and populate the difference test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -138,13 +138,13 @@ async def _async_setup_lev_table(backend, dialect, table): """Async: create and populate the levenshtein test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="word", data_type=TextType()), + ColumnDefinition(dialect, name="word", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -175,13 +175,13 @@ async def _async_setup_diff_table(backend, dialect, table): """Async: create and populate the difference test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py index 1530b978..2f7bf8fa 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py @@ -47,17 +47,17 @@ # --- Helper functions --- -def _make_hstore_columns(): +def _make_hstore_columns(dialect): """Return standard column definitions for an hstore test table.""" return [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="data", data_type=PostgresHstoreType()), + ColumnDefinition(dialect, name="data", data_type=PostgresHstoreType(dialect=dialect)), ] @@ -66,7 +66,7 @@ def _setup_hstore_table(backend, dialect, table, hstore_value): create_expr = CreateTableExpression( dialect=dialect, table=table, - columns=_make_hstore_columns(), + columns=_make_hstore_columns(dialect), if_not_exists=True, ) sql, params = create_expr.to_sql() @@ -101,7 +101,7 @@ async def _async_setup_hstore_table(backend, dialect, table, hstore_value): create_expr = CreateTableExpression( dialect=dialect, table=table, - columns=_make_hstore_columns(), + columns=_make_hstore_columns(dialect), if_not_exists=True, ) sql, params = create_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py index 7cf7ab52..badb7f92 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py @@ -51,14 +51,14 @@ def hypopg_env(postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="email", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="email", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -267,14 +267,14 @@ async def async_hypopg_env(async_postgres_backend_single): # Setup: create test table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="email", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="email", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py index ccd5a613..c51a1033 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py @@ -55,13 +55,13 @@ def _setup_intarray_table(backend, dialect, table): """Create and populate the basic intarray test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -93,13 +93,13 @@ def _setup_intarray_overlap_table(backend, dialect, table): """Create and populate the intarray overlap test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -131,13 +131,13 @@ def _setup_intarray_contained_table(backend, dialect, table): """Create and populate the intarray contained-by test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -169,13 +169,13 @@ def _setup_intarray_idx_table(backend, dialect, table): """Create and populate the intarray idx() test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -207,13 +207,13 @@ def _setup_intarray_gin_table(backend, dialect, table, index_name): """ columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -259,13 +259,13 @@ async def _async_setup_intarray_table(backend, dialect, table): """Async: create and populate the basic intarray test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -297,13 +297,13 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table): """Async: create and populate the intarray overlap test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -335,13 +335,13 @@ async def _async_setup_intarray_contained_table(backend, dialect, table): """Async: create and populate the intarray contained-by test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -373,13 +373,13 @@ async def _async_setup_intarray_idx_table(backend, dialect, table): """Async: create and populate the intarray idx() test table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -411,13 +411,13 @@ async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): """ columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py index 6a968c93..0f6d4998 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py @@ -69,13 +69,13 @@ def ltree_env(postgres_backend_single): table="test_ltree", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -107,14 +107,14 @@ def ltree_env(postgres_backend_single): table="test_ltree_tree", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), - ColumnDefinition(name="label", data_type=TextType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), + ColumnDefinition(dialect, name="label", data_type=TextType(dialect=dialect)), ], if_not_exists=True, ) @@ -146,13 +146,13 @@ def ltree_env(postgres_backend_single): table="test_ltree_func", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -178,13 +178,13 @@ def ltree_env(postgres_backend_single): table="test_ltree_idx", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -226,13 +226,13 @@ def ltree_env(postgres_backend_single): table="test_ltree_txtq", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -575,13 +575,13 @@ async def async_ltree_env(async_postgres_backend_single): table="test_ltree_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -613,14 +613,14 @@ async def async_ltree_env(async_postgres_backend_single): table="test_ltree_tree_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), - ColumnDefinition(name="label", data_type=TextType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), + ColumnDefinition(dialect, name="label", data_type=TextType(dialect=dialect)), ], if_not_exists=True, ) @@ -652,13 +652,13 @@ async def async_ltree_env(async_postgres_backend_single): table="test_ltree_func_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -684,13 +684,13 @@ async def async_ltree_env(async_postgres_backend_single): table="test_ltree_idx_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) @@ -732,13 +732,13 @@ async def async_ltree_env(async_postgres_backend_single): table="test_ltree_txtq_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(dialect, name="path", data_type=PostgresLtreeType(dialect=dialect)), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py index eb0243b8..1d7476f5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py @@ -62,17 +62,17 @@ def orafce_backend(postgres_backend_single): # Create test table using expressions columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="dt", data_type=DateType()), - ColumnDefinition(name="ref_dt", data_type=DateType()), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="discount", data_type=DecimalType()), - ColumnDefinition(name="description", data_type=TextType()), + ColumnDefinition(dialect, name="dt", data_type=DateType(dialect=dialect)), + ColumnDefinition(dialect, name="ref_dt", data_type=DateType(dialect=dialect)), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="discount", data_type=DecimalType(dialect=dialect)), + ColumnDefinition(dialect, name="description", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -323,17 +323,17 @@ async def async_orafce_backend(async_postgres_backend_single): # Create test table using expressions columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="dt", data_type=DateType()), - ColumnDefinition(name="ref_dt", data_type=DateType()), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="discount", data_type=DecimalType()), - ColumnDefinition(name="description", data_type=TextType()), + ColumnDefinition(dialect, name="dt", data_type=DateType(dialect=dialect)), + ColumnDefinition(dialect, name="ref_dt", data_type=DateType(dialect=dialect)), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="discount", data_type=DecimalType(dialect=dialect)), + ColumnDefinition(dialect, name="description", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py index b022fb98..a1edff96 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py @@ -61,13 +61,13 @@ def _setup_trgm_table(backend, dialect, table): """Create and populate the test_trgm table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -98,13 +98,13 @@ def _setup_trgm_ops_table(backend, dialect, table): """Create and populate the test_trgm_ops table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -139,13 +139,13 @@ def _setup_trgm_idx_table(backend, dialect, table): """ columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -179,13 +179,13 @@ def _setup_trgm_word_table(backend, dialect, table): """Create and populate the test_trgm_word table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="description", data_type=TextType()), + ColumnDefinition(dialect, name="description", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -224,13 +224,13 @@ async def _async_setup_trgm_table(backend, dialect, table): """Async: create and populate the test_trgm table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -261,13 +261,13 @@ async def _async_setup_trgm_ops_table(backend, dialect, table): """Async: create and populate the test_trgm_ops table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -302,13 +302,13 @@ async def _async_setup_trgm_idx_table(backend, dialect, table): """ columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -342,13 +342,13 @@ async def _async_setup_trgm_word_table(backend, dialect, table): """Async: create and populate the test_trgm_word table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="description", data_type=TextType()), + ColumnDefinition(dialect, name="description", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py index 7005a7dd..c6867850 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py @@ -58,14 +58,14 @@ def _setup_users_table(backend, dialect, table): """Create and populate the test_pgcrypto_users table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type=TextType()), - ColumnDefinition(name="password_hash", data_type=TextType()), + ColumnDefinition(dialect, name="username", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="password_hash", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -99,13 +99,13 @@ def _setup_encrypt_table(backend, dialect, table): """Create and populate the test_pgcrypto_encrypt table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="secret_data", data_type=PostgresByteaType()), + ColumnDefinition(dialect, name="secret_data", data_type=PostgresByteaType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -150,14 +150,14 @@ async def _async_setup_users_table(backend, dialect, table): """Async: create and populate the test_pgcrypto_users table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type=TextType()), - ColumnDefinition(name="password_hash", data_type=TextType()), + ColumnDefinition(dialect, name="username", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="password_hash", data_type=TextType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -191,13 +191,13 @@ async def _async_setup_encrypt_table(backend, dialect, table): """Async: create and populate the test_pgcrypto_encrypt table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="secret_data", data_type=PostgresByteaType()), + ColumnDefinition(dialect, name="secret_data", data_type=PostgresByteaType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py index 622b86d7..a6564e7f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py @@ -49,20 +49,20 @@ def pgrouting_env(postgres_backend_single): # Setup: create test_route_edges table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="source", data_type=IntegerType()), - ColumnDefinition(name="target", data_type=IntegerType()), - ColumnDefinition(name="cost", data_type=FloatType()), - ColumnDefinition(name="reverse_cost", data_type=FloatType()), - ColumnDefinition(name="x1", data_type=FloatType()), - ColumnDefinition(name="y1", data_type=FloatType()), - ColumnDefinition(name="x2", data_type=FloatType()), - ColumnDefinition(name="y2", data_type=FloatType()), + ColumnDefinition(dialect, name="source", data_type=IntegerType(dialect=dialect)), + ColumnDefinition(dialect, name="target", data_type=IntegerType(dialect=dialect)), + ColumnDefinition(dialect, name="cost", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="reverse_cost", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="x1", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="y1", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="x2", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="y2", data_type=FloatType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -178,20 +178,20 @@ async def async_pgrouting_env(async_postgres_backend_single): # Setup: create test_route_edges_async table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="source", data_type=IntegerType()), - ColumnDefinition(name="target", data_type=IntegerType()), - ColumnDefinition(name="cost", data_type=FloatType()), - ColumnDefinition(name="reverse_cost", data_type=FloatType()), - ColumnDefinition(name="x1", data_type=FloatType()), - ColumnDefinition(name="y1", data_type=FloatType()), - ColumnDefinition(name="x2", data_type=FloatType()), - ColumnDefinition(name="y2", data_type=FloatType()), + ColumnDefinition(dialect, name="source", data_type=IntegerType(dialect=dialect)), + ColumnDefinition(dialect, name="target", data_type=IntegerType(dialect=dialect)), + ColumnDefinition(dialect, name="cost", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="reverse_cost", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="x1", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="y1", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="x2", data_type=FloatType(dialect=dialect)), + ColumnDefinition(dialect, name="y2", data_type=FloatType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py index 1541c41b..d724e920 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 @@ -48,16 +48,16 @@ def _id_column(): return ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ) def _embedding_column(dim=3): - return ColumnDefinition(name="embedding", data_type=PostgresVectorType(dim)) + return ColumnDefinition(dialect, name="embedding", data_type=PostgresVectorType(dim, dialect=dialect)) def _vector_literal(value, vector_type="vector"): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py index a0a89473..2464a103 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py @@ -47,13 +47,13 @@ def _setup_geo_points_table(backend, dialect, table): """Create and populate the test_geo_points table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -84,13 +84,13 @@ def _setup_geo_distance_table(backend, dialect, table): """Create and populate the test_geo_distance table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -121,13 +121,13 @@ def _setup_geo_contains_table(backend, dialect, table): """Create and populate the test_geo_contains table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -164,13 +164,13 @@ def _setup_geo_index_table(backend, dialect, table, index_name): """Create and populate the test_geo_index table with GiST index using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -211,13 +211,13 @@ def _setup_geog_table(backend, dialect, table): """Create and populate the test_geog table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geog", data_type=PostgresGeographyType()), + ColumnDefinition(dialect, name="geog", data_type=PostgresGeographyType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -258,13 +258,13 @@ async def _async_setup_geo_points_table(backend, dialect, table): """Async: create and populate the test_geo_points table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -295,13 +295,13 @@ async def _async_setup_geo_distance_table(backend, dialect, table): """Async: create and populate the test_geo_distance table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -332,13 +332,13 @@ async def _async_setup_geo_contains_table(backend, dialect, table): """Async: create and populate the test_geo_contains table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -375,13 +375,13 @@ async def _async_setup_geo_index_table(backend, dialect, table, index_name): """Async: create and populate the test_geo_index table with GiST index using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type=PostgresGeometryType()), + ColumnDefinition(dialect, name="geom", data_type=PostgresGeometryType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -422,13 +422,13 @@ async def _async_setup_geog_table(backend, dialect, table): """Async: create and populate the test_geog table using expressions.""" columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geog", data_type=PostgresGeographyType()), + ColumnDefinition(dialect, name="geog", data_type=PostgresGeographyType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py index e5b462e2..e012292f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py @@ -89,14 +89,14 @@ def raster_env(postgres_backend_single): # Setup: create test_raster_data table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="rast", data_type=PostgresRasterType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="rast", data_type=PostgresRasterType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -208,14 +208,14 @@ async def async_raster_env(async_postgres_backend_single): # Setup: create test_raster_data_async table using expression columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type=TextType()), - ColumnDefinition(name="rast", data_type=PostgresRasterType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="rast", data_type=PostgresRasterType(dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py index 7b1fa9df..1fc5b3ae 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py @@ -61,15 +61,15 @@ def tablefunc_env(postgres_backend_single): # Setup: create test_tf_sales table using expression sales_columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="month", data_type=TextType()), - ColumnDefinition(name="category", data_type=TextType()), - ColumnDefinition(name="amount", data_type=IntegerType()), + ColumnDefinition(dialect, name="month", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="category", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="amount", data_type=IntegerType(dialect=dialect)), ] create_sales = CreateTableExpression( dialect=dialect, @@ -101,14 +101,14 @@ def tablefunc_env(postgres_backend_single): # Setup: create test_tf_tree table using expression tree_columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="node_name", data_type=TextType()), - ColumnDefinition(name="parent_id", data_type=IntegerType()), + ColumnDefinition(dialect, name="node_name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="parent_id", data_type=IntegerType(dialect=dialect)), ] create_tree = CreateTableExpression( dialect=dialect, @@ -339,15 +339,15 @@ async def async_tablefunc_env(async_postgres_backend_single): # Setup: create test_tf_sales_async table using expression sales_columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="month", data_type=TextType()), - ColumnDefinition(name="category", data_type=TextType()), - ColumnDefinition(name="amount", data_type=IntegerType()), + ColumnDefinition(dialect, name="month", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="category", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="amount", data_type=IntegerType(dialect=dialect)), ] create_sales = CreateTableExpression( dialect=dialect, @@ -379,14 +379,14 @@ async def async_tablefunc_env(async_postgres_backend_single): # Setup: create test_tf_tree_async table using expression tree_columns = [ ColumnDefinition( - name="id", - data_type=PostgresSerialType(), + dialect, name="id", + data_type=PostgresSerialType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="node_name", data_type=TextType()), - ColumnDefinition(name="parent_id", data_type=IntegerType()), + ColumnDefinition(dialect, name="node_name", data_type=TextType(dialect=dialect)), + ColumnDefinition(dialect, name="parent_id", data_type=IntegerType(dialect=dialect)), ] create_tree = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py index 388bee98..69f2c7a6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py @@ -76,17 +76,17 @@ def uuid_ossp_env(postgres_backend_single): table="test_uuid_ossp", columns=[ ColumnDefinition( - name="id", - data_type=PostgresUUIDType(), + dialect, name="id", + data_type=PostgresUUIDType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ColumnConstraint( - ColumnConstraintType.DEFAULT, + dialect, ColumnConstraintType.DEFAULT, default_value=FunctionCall(dialect, "uuid_generate_v4"), ), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ], if_not_exists=True, ) @@ -112,13 +112,13 @@ def uuid_ossp_env(postgres_backend_single): table="test_uuid_explicit", columns=[ ColumnDefinition( - name="id", - data_type=PostgresUUIDType(), + dialect, name="id", + data_type=PostgresUUIDType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="label", data_type=TextType()), + ColumnDefinition(dialect, name="label", data_type=TextType(dialect=dialect)), ], if_not_exists=True, ) @@ -281,17 +281,17 @@ async def async_uuid_ossp_env(async_postgres_backend_single): table="test_uuid_ossp_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresUUIDType(), + dialect, name="id", + data_type=PostgresUUIDType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ColumnConstraint( - ColumnConstraintType.DEFAULT, + dialect, ColumnConstraintType.DEFAULT, default_value=FunctionCall(dialect, "uuid_generate_v4"), ), ], ), - ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ], if_not_exists=True, ) @@ -317,13 +317,13 @@ async def async_uuid_ossp_env(async_postgres_backend_single): table="test_uuid_explicit_async", columns=[ ColumnDefinition( - name="id", - data_type=PostgresUUIDType(), + dialect, name="id", + data_type=PostgresUUIDType(dialect=dialect), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="label", data_type=TextType()), + ColumnDefinition(dialect, name="label", data_type=TextType(dialect=dialect)), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index 7dd8ea9e..47ca6a5c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -27,69 +27,69 @@ class TestCoreTypeMappings: # The five critical mappings def test_boolean_to_boolean(self, dialect): from rhosocial.activerecord.backend.expression.types import BooleanType - assert BooleanType().to_sql(dialect) == ("BOOLEAN", ()) + assert BooleanType(dialect=dialect).to_sql() == ("BOOLEAN", ()) def test_tinyint_to_smallint(self, dialect): from rhosocial.activerecord.backend.expression.types import TinyIntType - assert TinyIntType().to_sql(dialect) == ("SMALLINT", ()) + assert TinyIntType(dialect=dialect).to_sql() == ("SMALLINT", ()) def test_blob_to_bytea(self, dialect): from rhosocial.activerecord.backend.expression.types import BlobType - assert BlobType().to_sql(dialect) == ("BYTEA", ()) + assert BlobType(dialect=dialect).to_sql() == ("BYTEA", ()) def test_datetime_to_timestamp(self, dialect): from rhosocial.activerecord.backend.expression.types import DateTimeType - assert DateTimeType().to_sql(dialect) == ("TIMESTAMP", ()) + assert DateTimeType(dialect=dialect).to_sql() == ("TIMESTAMP", ()) def test_int_to_integer(self, dialect): from rhosocial.activerecord.backend.expression.types import IntType - assert IntType().to_sql(dialect) == ("INTEGER", ()) + assert IntType(dialect=dialect).to_sql() == ("INTEGER", ()) # Standard integer family def test_smallint(self, dialect): from rhosocial.activerecord.backend.expression.types import SmallIntType - assert SmallIntType().to_sql(dialect) == ("SMALLINT", ()) + assert SmallIntType(dialect=dialect).to_sql() == ("SMALLINT", ()) def test_integer(self, dialect): from rhosocial.activerecord.backend.expression.types import IntegerType - assert IntegerType().to_sql(dialect) == ("INTEGER", ()) + assert IntegerType(dialect=dialect).to_sql() == ("INTEGER", ()) def test_bigint(self, dialect): from rhosocial.activerecord.backend.expression.types import BigIntType - assert BigIntType().to_sql(dialect) == ("BIGINT", ()) + assert BigIntType(dialect=dialect).to_sql() == ("BIGINT", ()) # Numeric family def test_real(self, dialect): from rhosocial.activerecord.backend.expression.types import RealType - assert RealType().to_sql(dialect) == ("REAL", ()) + assert RealType(dialect=dialect).to_sql() == ("REAL", ()) def test_double(self, dialect): from rhosocial.activerecord.backend.expression.types import DoubleType - assert DoubleType().to_sql(dialect) == ("DOUBLE PRECISION", ()) + assert DoubleType(dialect=dialect).to_sql() == ("DOUBLE PRECISION", ()) def test_numeric(self, dialect): from rhosocial.activerecord.backend.expression.types import DecimalType - assert DecimalType(10, 2).to_sql(dialect) == ("DECIMAL(10,2)", ()) - assert DecimalType().to_sql(dialect) == ("DECIMAL", ()) + assert DecimalType(10, 2, dialect=dialect).to_sql() == ("DECIMAL(10,2)", ()) + assert DecimalType(dialect=dialect).to_sql() == ("DECIMAL", ()) # String family def test_varchar(self, dialect): from rhosocial.activerecord.backend.expression.types import VarCharType - assert VarCharType(255).to_sql(dialect) == ("VARCHAR(255)", ()) - assert VarCharType().to_sql(dialect) == ("VARCHAR", ()) + assert VarCharType(255, dialect=dialect).to_sql() == ("VARCHAR(255)", ()) + assert VarCharType(dialect=dialect).to_sql() == ("VARCHAR", ()) def test_text(self, dialect): from rhosocial.activerecord.backend.expression.types import TextType - assert TextType().to_sql(dialect) == ("TEXT", ()) + assert TextType(dialect=dialect).to_sql() == ("TEXT", ()) # JSON family def test_json(self, dialect): from rhosocial.activerecord.backend.expression.types import JsonType - assert JsonType().to_sql(dialect) == ("JSON", ()) + assert JsonType(dialect=dialect).to_sql() == ("JSON", ()) def test_jsonb(self, dialect): from rhosocial.activerecord.backend.expression.types import JsonBType - assert JsonBType().to_sql(dialect) == ("JSONB", ()) + assert JsonBType(dialect=dialect).to_sql() == ("JSONB", ()) class TestArrayType: @@ -100,32 +100,32 @@ def test_array_1d_rendering(self, dialect): PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import IntegerType - arr = PostgresArrayType(IntegerType()) - assert arr.to_sql(dialect) == ("INTEGER[]", ()) + arr = PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect) + assert arr.to_sql() == ("INTEGER[]", ()) def test_array_2d_rendering(self, dialect): from rhosocial.activerecord.backend.impl.postgres.expression.types import ( PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import IntegerType - arr = PostgresArrayType(IntegerType(), dimensions=2) - assert arr.to_sql(dialect) == ("INTEGER[][]", ()) + arr = PostgresArrayType(IntegerType(dialect=dialect), dimensions=2, dialect=dialect) + assert arr.to_sql() == ("INTEGER[][]", ()) def test_array_varchar_rendering(self, dialect): from rhosocial.activerecord.backend.impl.postgres.expression.types import ( PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import VarCharType - arr = PostgresArrayType(VarCharType(255)) - assert arr.to_sql(dialect) == ("VARCHAR(255)[]", ()) + arr = PostgresArrayType(VarCharType(255, dialect=dialect), dialect=dialect) + assert arr.to_sql() == ("VARCHAR(255)[]", ()) def test_array_boolean_rendering(self, dialect): from rhosocial.activerecord.backend.impl.postgres.expression.types import ( PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import BooleanType - arr = PostgresArrayType(BooleanType()) - assert arr.to_sql(dialect) == ("BOOLEAN[]", ()) + arr = PostgresArrayType(BooleanType(dialect=dialect), dialect=dialect) + assert arr.to_sql() == ("BOOLEAN[]", ()) def test_array_equality(self, dialect): from rhosocial.activerecord.backend.impl.postgres.expression.types import ( @@ -134,14 +134,14 @@ def test_array_equality(self, dialect): from rhosocial.activerecord.backend.expression.types import ( IntegerType, VarCharType, ) - a1 = PostgresArrayType(IntegerType(), 2) - a2 = PostgresArrayType(IntegerType(), 2) - a3 = PostgresArrayType(VarCharType(255), 2) + a1 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) + a2 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) + a3 = PostgresArrayType(VarCharType(255, dialect=dialect), 2, dialect=dialect) assert a1 == a2 assert a1 != a3 assert hash(a1) == hash(a2) # PostgresArrayType.is_equivalent ignores dimensions - a4 = PostgresArrayType(IntegerType(), 1) + a4 = PostgresArrayType(IntegerType(dialect=dialect), 1, dialect=dialect) assert a1.is_equivalent(a4) def test_parse_array_bracket_suffix(self, dialect): @@ -198,14 +198,14 @@ def test_is_element_type_equivalent(self, dialect): from rhosocial.activerecord.backend.expression.types import ( IntegerType, SmallIntType, ) - a1 = PostgresArrayType(IntegerType(), 2) - a2 = PostgresArrayType(IntegerType(), 1) # different dimension - a3 = PostgresArrayType(SmallIntType()) # different element type + a1 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) + a2 = PostgresArrayType(IntegerType(dialect=dialect), 1, dialect=dialect) # different dimension + a3 = PostgresArrayType(SmallIntType(dialect=dialect), dialect=dialect) # different element type # Same element, different dimension → equivalent assert a1.is_element_type_equivalent(a2) # Different element → not equivalent assert not a1.is_element_type_equivalent(a3) # Plain IntegerType (not array) as other → matches element - assert a1.is_element_type_equivalent(IntegerType()) + assert a1.is_element_type_equivalent(IntegerType(dialect=dialect)) # Plain SmallIntType as other → doesn't match Integer element - assert not a1.is_element_type_equivalent(SmallIntType()) + assert not a1.is_element_type_equivalent(SmallIntType(dialect=dialect)) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_alter_config.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_alter_config.py index c44a8926..a84af276 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_alter_config.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_alter_config.py @@ -263,8 +263,9 @@ def test_capabilities(self, dialect): def test_add_column(self, dialect): column = ColumnDefinition( - "email", TextType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "email", TextType(dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ) sql, params = dialect.format_add_column_action( AddColumn(dialect, column) @@ -273,7 +274,7 @@ def test_add_column(self, dialect): assert params == () def test_add_column_if_not_exists(self, dialect): - column = ColumnDefinition("email", TextType()) + column = ColumnDefinition(dialect, "email", TextType(dialect)) sql, _ = dialect.format_add_column_action( AddColumn(dialect, column, if_not_exists=True) ) 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 22231b29..c729efa5 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 @@ -247,10 +247,11 @@ def test_create_range_partitioned_parent_table(self, dialect): dialect=dialect, table="events", columns=[ - ColumnDefinition("id", BigIntType()), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect)), ColumnDefinition( - "created_at", TimestampType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "created_at", TimestampType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ), ], partition=PartitionClause( @@ -271,8 +272,8 @@ def test_create_list_partitioned_parent_table(self, dialect): dialect=dialect, table="events", columns=[ - ColumnDefinition("id", BigIntType()), - ColumnDefinition("status", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect)), + ColumnDefinition(dialect, "status", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -293,8 +294,9 @@ def test_create_hash_partitioned_parent_table_pg10(self): table="events", columns=[ ColumnDefinition( - "tenant_id", BigIntType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "tenant_id", BigIntType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ) ], partition=PartitionClause( @@ -315,8 +317,9 @@ def test_create_hash_partitioned_parent_table_pg11(self): table="events", columns=[ ColumnDefinition( - "tenant_id", BigIntType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "tenant_id", BigIntType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ) ], partition=PartitionClause( @@ -338,8 +341,9 @@ def test_create_partitioned_parent_table_pg9(self): table="events", columns=[ ColumnDefinition( - "created_at", TimestampType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "created_at", TimestampType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ) ], partition=PartitionClause( @@ -368,14 +372,16 @@ def test_multi_column_range_partitioned_parent_table(self, dialect): table="tenanted_events", columns=[ ColumnDefinition( - "tenant_id", BigIntType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "tenant_id", BigIntType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ), ColumnDefinition( - "created_at", TimestampType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)], + dialect, + "created_at", TimestampType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)], ), - ColumnDefinition("payload", TextType()), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect)), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py index f8fa1597..d133a64d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py @@ -69,6 +69,7 @@ def test_malicious_data_type_rejected_at_construction(self, postgres_backend): # ColumnDefinition now rejects string data_type values at construction time with pytest.raises(TypeError, match="data_type must be a DataType instance"): ColumnDefinition( + postgres_backend.dialect, name="test_col", data_type="VARCHAR(255); DROP TABLE users--", ) 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 d289917c..5cc56cfa 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 @@ -17,6 +17,7 @@ from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateEnumTypeExpression, PostgresDropEnumTypeExpression, + PostgresAlterEnumAddValueExpression, PostgresAlterEnumTypeAddValueExpression, PostgresAlterEnumTypeRenameValueExpression, ) @@ -451,17 +452,27 @@ def test_format_enum_values(self): def test_format_create_enum_type(self): """Test format_create_enum_type method.""" dialect = PostgresDialect() - sql = dialect.format_create_enum_type('status', ['pending', 'ready']) + expr = PostgresCreateEnumTypeExpression( + dialect=dialect, name='status', values=['pending', 'ready'] + ) + sql, params = dialect.format_create_enum_type(expr) assert sql == "CREATE TYPE status AS ENUM ('pending', 'ready')" + assert params == () def test_format_drop_enum_type(self): """Test format_drop_enum_type method.""" dialect = PostgresDialect() - sql = dialect.format_drop_enum_type('status') + expr = PostgresDropEnumTypeExpression(dialect=dialect, name='status') + sql, params = dialect.format_drop_enum_type(expr) assert sql == "DROP TYPE status" + assert params == () def test_format_alter_enum_add_value(self): """Test format_alter_enum_add_value method.""" dialect = PostgresDialect() - sql = dialect.format_alter_enum_add_value('status', 'failed') + expr = PostgresAlterEnumAddValueExpression( + dialect=dialect, type_name='status', new_value='failed' + ) + sql, params = dialect.format_alter_enum_add_value(expr) assert sql == "ALTER TYPE status ADD VALUE 'failed'" + assert params == () diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py index 40fc7ee7..f79732e9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py @@ -789,7 +789,7 @@ class TestFormatAddDropIndexAction: def test_format_add_index_action_raises(self): d = PostgresDialect() - add = AddIndex(d, index=IndexDefinition(name="idx_test", columns=["a"])) + add = AddIndex(d, index=IndexDefinition(d, name="idx_test", columns=["a"])) with pytest.raises(UnsupportedFeatureError, match="ALTER TABLE ADD INDEX"): d.format_add_index_action(add) @@ -1013,9 +1013,8 @@ def test_supports_index_if_exists(self): def test_format_create_index_expression_column(self): from rhosocial.activerecord.backend.expression import Column, Literal # noqa: F401 - expr = CreateIndexExpression( - PostgresDialect((15, 0, 0)), "idx_e", "t", [Column(PostgresDialect((15, 0, 0)), "a")], - ) + d = PostgresDialect((15, 0, 0)) + expr = CreateIndexExpression(d, "idx_e", "t", [Column(d, "a")]) sql, _ = expr.to_sql() assert '"a"' in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py index c36e307f..1a45fc28 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py @@ -44,48 +44,48 @@ def social_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) people_cols = [ - ColumnDefinition("id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", TextType()), - ColumnDefinition("email", TextType()), - ColumnDefinition("city", TextType()), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", TextType(dialect=dialect)), + ColumnDefinition(dialect, "email", TextType(dialect=dialect)), + ColumnDefinition(dialect, "city", TextType(dialect=dialect)), ] backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) follows_cols = [ - ColumnDefinition("id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "follower_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("followed_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "followed_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("since", TextType()), + ColumnDefinition(dialect, "since", TextType(dialect=dialect)), ] backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) posts_cols = [ - ColumnDefinition("id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("author_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "author_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("content", TextType()), - ColumnDefinition("created_at", TextType()), + ColumnDefinition(dialect, "content", TextType(dialect=dialect)), + ColumnDefinition(dialect, "created_at", TextType(dialect=dialect)), ] backend.execute(*CreateTableExpression(dialect, "posts", posts_cols).to_sql()) likes_cols = [ - ColumnDefinition("id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("post_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "post_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("posts", ["id"]))]), - ColumnDefinition("created_at", TextType()), + ColumnDefinition(dialect, "created_at", TextType(dialect=dialect)), ] backend.execute(*CreateTableExpression(dialect, "likes", likes_cols).to_sql()) @@ -326,24 +326,24 @@ async def async_social_data(self, async_postgres_backend): await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) people_cols = [ - ColumnDefinition("id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", TextType()), - ColumnDefinition("email", TextType()), - ColumnDefinition("city", TextType()), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", TextType(dialect=dialect)), + ColumnDefinition(dialect, "email", TextType(dialect=dialect)), + ColumnDefinition(dialect, "city", TextType(dialect=dialect)), ] await backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) follows_cols = [ - ColumnDefinition("id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "follower_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("followed_id", IntegerType(), - constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, + ColumnDefinition(dialect, "followed_id", IntegerType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("since", TextType()), + ColumnDefinition(dialect, "since", TextType(dialect=dialect)), ] await backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index b05c6fb1..c1a211c6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -53,16 +53,16 @@ def social_network_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) backend.execute(*CreateTableExpression(dialect, "users", [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(100)), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(100, dialect=dialect)), ]).to_sql()) backend.execute(*CreateTableExpression(dialect, "follows", [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", IntegerType()), - ColumnDefinition("followed_id", IntegerType()), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "follower_id", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "followed_id", IntegerType(dialect=dialect)), ]).to_sql()) backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], @@ -98,18 +98,18 @@ def aml_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) backend.execute(*CreateTableExpression(dialect, "accounts", [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("account_holder", VarCharType(100)), - ColumnDefinition("account_type", VarCharType(20)), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "account_holder", VarCharType(100, dialect=dialect)), + ColumnDefinition(dialect, "account_type", VarCharType(20, dialect=dialect)), ]).to_sql()) backend.execute(*CreateTableExpression(dialect, "transactions", [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("source_account_id", IntegerType()), - ColumnDefinition("target_account_id", IntegerType()), - ColumnDefinition("amount", DecimalType(12, 2)), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "source_account_id", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "target_account_id", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "amount", DecimalType(12, 2, dialect=dialect)), ]).to_sql()) backend.execute(*InsertExpression(dialect, "accounts", columns=["id", "account_holder", "account_type"], @@ -465,16 +465,16 @@ async def async_social_network_data(self, async_postgres_backend): await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) await backend.execute(*CreateTableExpression(dialect, "users", [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(100)), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "name", VarCharType(100, dialect=dialect)), ]).to_sql()) await backend.execute(*CreateTableExpression(dialect, "follows", [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", IntegerType()), - ColumnDefinition("followed_id", IntegerType()), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition(dialect, "follower_id", IntegerType(dialect=dialect)), + ColumnDefinition(dialect, "followed_id", IntegerType(dialect=dialect)), ]).to_sql()) await backend.execute(*InsertExpression(dialect, "users", columns=["id", "name"], 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 50058992..84bef0e1 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 @@ -10,7 +10,7 @@ import pytest # noqa: F401 import warnings -from rhosocial.activerecord.backend.expression.core import Column, Literal +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 ( MONEY, NUMERIC, FLOAT8, INTEGER, VARCHAR, TEXT, @@ -123,34 +123,32 @@ def setup_method(self): def test_format_cast_basic(self): """Test basic type cast formatting.""" - sql, params = self.dialect.format_cast_expression( - "col", INTEGER, () - ) - assert sql == "col::integer" + expr = CastExpression(self.dialect, Column(self.dialect, "col"), INTEGER) + sql, params = expr.to_sql() + assert sql == '"col"::integer' assert params == () def test_format_cast_with_type_modifier(self): """Test type cast with type modifiers.""" - sql, params = self.dialect.format_cast_expression( - "name", "VARCHAR(100)", () - ) - assert sql == "name::VARCHAR(100)" + expr = CastExpression(self.dialect, Column(self.dialect, "name"), "VARCHAR(100)") + sql, params = expr.to_sql() + assert sql == '"name"::VARCHAR(100)' assert params == () def test_format_cast_with_alias(self): """Test type cast with alias.""" - sql, params = self.dialect.format_cast_expression( - "value", INTEGER, (), "int_val" + expr = CastExpression( + self.dialect, Column(self.dialect, "value"), INTEGER, alias="int_val" ) - assert sql == 'value::integer AS "int_val"' + sql, params = expr.to_sql() + assert sql == '"value"::integer AS "int_val"' assert params == () def test_format_cast_preserves_params(self): """Test that parameters are preserved.""" params_in = ("test_value",) - sql, params = self.dialect.format_cast_expression( - "%s", TEXT, params_in - ) + expr = Literal(self.dialect, "test_value").cast(TEXT) + sql, params = expr.to_sql() assert sql == "%s::text" assert params == params_in diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py index 840f5139..e93c2e14 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py @@ -26,22 +26,22 @@ def dialect(): return PostgresDialect() -def _pk(): - return [ColumnConstraint(ColumnConstraintType.PRIMARY_KEY, is_auto_increment=True)] +def _pk(dialect): + return [ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY, is_auto_increment=True)] def _build_table(dialect): return CreateTableExpression( dialect=dialect, table="test_tbl", columns=[ - ColumnDefinition("id", IntegerType(), constraints=_pk()), - ColumnDefinition("name", TextType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("flag", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), - ColumnDefinition("code", VarCharType(16)), - ColumnDefinition("created_at", TimestampType()), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=_pk(dialect)), + ColumnDefinition(dialect, "name", TextType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "flag", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition(dialect, "code", VarCharType(16, dialect=dialect)), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect)), ], ) @@ -62,10 +62,10 @@ def test_boolean_default_true(self, dialect): expr = CreateTableExpression( dialect=dialect, table="bool_test", columns=[ - ColumnDefinition("id", IntegerType(), constraints=_pk()), - ColumnDefinition("flag", BooleanType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL), - ColumnConstraint(ColumnConstraintType.DEFAULT, default_value=True)]), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=_pk(dialect)), + ColumnDefinition(dialect, "flag", BooleanType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), + ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), ], ) sql, _ = dialect.format_create_table_statement(expr) @@ -75,8 +75,8 @@ def test_timestamp_type(self, dialect): expr = CreateTableExpression( dialect=dialect, table="ts_test", columns=[ - ColumnDefinition("id", IntegerType(), constraints=_pk()), - ColumnDefinition("created_at", TimestampType()), + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=_pk(dialect)), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect)), ], ) sql, _ = dialect.format_create_table_statement(expr) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py index f8485637..5fe52d61 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py @@ -207,10 +207,10 @@ def test_like_ignores_columns(self): """Test that LIKE syntax ignores columns parameter.""" dialect = PostgresDialect() columns = [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY) + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition("name", VarCharType(255)) + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect)) ] create_expr = CreateTableExpression( dialect=dialect, @@ -261,11 +261,11 @@ def test_fallback_to_base_when_no_like(self): """Test that base implementation is used when LIKE is not specified.""" dialect = PostgresDialect() columns = [ - ColumnDefinition("id", IntegerType(), constraints=[ - ColumnConstraint(ColumnConstraintType.PRIMARY_KEY) + ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition("name", VarCharType(255), constraints=[ - ColumnConstraint(ColumnConstraintType.NOT_NULL) + ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), constraints=[ + ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL) ]) ] create_expr = CreateTableExpression( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py index 9234a7a3..15c816cf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py @@ -32,7 +32,7 @@ def for_update(d): from rhosocial.activerecord.backend.impl.postgres.expression.locking import ( PostgresForUpdateExpression, ) - return PostgresForUpdateExpression(d, columns=[Column(d, "id")]) + return PostgresForUpdateExpression(d, columns=[Column(dialect, d, "id")]) def advisory(d): from rhosocial.activerecord.backend.impl.postgres.expression.advisory.lock import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py index 95649d01..1feedbf9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py @@ -64,9 +64,9 @@ def _create_list_parent_expression(dialect): dialect=dialect, table=LIST_PARENT, columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("status", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "status", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -92,9 +92,9 @@ def _create_hash_parent_expression(dialect): dialect=dialect, table=HASH_PARENT, columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("bucket", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "bucket", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -120,9 +120,9 @@ def _create_range_parent_expression(dialect): dialect=dialect, table=RANGE_PARENT, columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -322,9 +322,9 @@ def test_default_attach_and_detach_workflow(self, postgres_backend, postgres_ran dialect=dialect, table=default_partition_name, columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], ) postgres_backend.execute(*create_default.to_sql()) @@ -382,8 +382,8 @@ def test_metadata_include_partitions_false_and_schema_filter(self, postgres_back dialect=dialect, table=TableExpression(dialect, SCHEMA_PARENT, schema_name=schema), columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, 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 e8a5bdbb..fc56195a 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 @@ -68,17 +68,17 @@ def _create_parent_table_expression(dialect): table=PARTITION_EXPLAIN_TABLE, if_not_exists=False, columns=[ - ColumnDefinition("id", BigIntType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampType(6), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("tenant_id", BigIntType(), - constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType()), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(6, dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "tenant_id", BigIntType(dialect=dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect)), ], table_constraints=[ TableConstraint( - TableConstraintType.PRIMARY_KEY, + dialect, TableConstraintType.PRIMARY_KEY, columns=["id", "created_at"], ), ], diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py index fed659ae..c7becee8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py @@ -82,9 +82,9 @@ def _create_partitioned_parent_sql(dialect, table_name: str): dialect=dialect, table=table_name, columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType()), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect)), ], partition=PartitionClause( dialect=dialect, @@ -206,14 +206,14 @@ def _create_production_parent_sql(dialect): dialect=dialect, table=PRODUCTION_PARTITION_TABLE, columns=[ - ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("created_at", TimestampType(6), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), - ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "tenant_id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], table_constraints=[ TableConstraint( - TableConstraintType.PRIMARY_KEY, + dialect, TableConstraintType.PRIMARY_KEY, columns=["id", "created_at"], ), ], diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py index bd13795b..1e52412a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py @@ -18,6 +18,11 @@ TableConstraintType, ) from rhosocial.activerecord.backend.expression.types import VarCharType +from rhosocial.activerecord.backend.expression.operators import ( + BinaryExpression, + RawSQLExpression, +) +from rhosocial.activerecord.backend.expression.core import CastExpression from typing import Tuple, Any # noqa: F401 @@ -44,8 +49,9 @@ def test_postgres_validate_data_type(dialect): def test_postgres_format_column_definition_data_type_validation(dialect): """Test column definition formats data_type correctly.""" col_def = ColumnDefinition( + dialect, name="test_col", - data_type=VarCharType(255), + data_type=VarCharType(255, dialect=dialect), ) sql, params = dialect.format_column_definition(col_def) @@ -56,6 +62,7 @@ def test_postgres_column_definition_rejects_string_data_type(dialect): """Test that ColumnDefinition rejects string data_type at construction.""" with pytest.raises(TypeError, match="data_type must be a DataType instance"): ColumnDefinition( + dialect, name="test_col", data_type="VARCHAR(255); DROP TABLE users--", ) @@ -64,6 +71,7 @@ def test_postgres_column_definition_rejects_string_data_type(dialect): def test_postgres_format_default_constraint_string_escaping(dialect): """Test DEFAULT constraint string is escaped.""" constraint = ColumnConstraint( + dialect, constraint_type=ColumnConstraintType.DEFAULT, default_value="test's value", ) @@ -82,14 +90,16 @@ def test_postgres_format_storage_options_string_escaping(dialect): def test_postgres_format_cast_expression_valid(dialect): - """Test that CAST expression validates target_type.""" - sql, params = dialect.format_cast_expression("column", "INTEGER", (), None) + """Test that CAST expression renders the target type (:: syntax).""" + expr = CastExpression(dialect, RawSQLExpression(dialect, "column"), "INTEGER") + sql, params = expr.to_sql() assert "INTEGER" in sql def test_exclude_constraint_valid_using_methods(dialect): """Test valid index access methods are accepted.""" constraint = TableConstraint( + dialect, constraint_type=TableConstraintType.EXCLUDE, name="test_exclude", dialect_options={ @@ -111,12 +121,13 @@ def test_exclude_constraint_valid_operators(dialect): ("box", "~="), ] - for expr, op in valid_ops: + for elem, op in valid_ops: constraint = TableConstraint( + dialect, constraint_type=TableConstraintType.EXCLUDE, name="test_exclude", dialect_options={ - "exclude_elements": [(expr, op)], + "exclude_elements": [(elem, op)], }, ) @@ -127,6 +138,7 @@ def test_exclude_constraint_valid_operators(dialect): def test_exclude_constraint_rejects_invalid_using(dialect): """Test that invalid index access method is rejected.""" constraint = TableConstraint( + dialect, constraint_type=TableConstraintType.EXCLUDE, name="test_exclude", dialect_options={ @@ -142,6 +154,7 @@ def test_exclude_constraint_rejects_invalid_using(dialect): def test_exclude_constraint_rejects_invalid_operator(dialect): """Test that invalid exclude operator is rejected.""" constraint = TableConstraint( + dialect, constraint_type=TableConstraintType.EXCLUDE, name="test_exclude", dialect_options={ @@ -156,6 +169,7 @@ def test_exclude_constraint_rejects_invalid_operator(dialect): def test_exclude_constraint_sql_injection_prevention(dialect): """Test that SQL injection attempts are blocked.""" constraint = TableConstraint( + dialect, constraint_type=TableConstraintType.EXCLUDE, name="test_exclude", dialect_options={ @@ -503,34 +517,43 @@ def test_format_identifier_empty_string(dialect): def test_format_binary_operator_percent_escaped(): """format_binary_operator escapes % to %% for psycopg compatibility.""" from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + from rhosocial.activerecord.backend.expression.core import Literal d = PostgresDialect((16, 0, 0)) + def binop(op, left_sql, right_sql, right_params=()): + left = RawSQLExpression(d, left_sql) + if right_params: + right = Literal(d, right_params[0]) + else: + right = RawSQLExpression(d, right_sql) + return BinaryExpression(d, op, left, right).to_sql() + # % operator → %% (psycopg requires %% for literal %) - sql, params = d.format_binary_operator("%", "a", "b", (), ()) + sql, params = binop("%", "a", "b") assert sql == "a %% b", f"Expected escaped %%, got: {sql}" assert params == () # %% is pre-escaped → should NOT be double-escaped to %%%% # (callers must pass raw operator, not pre-escaped) - sql, params = d.format_binary_operator("%%", "a", "b", (), ()) + sql, params = binop("%%", "a", "b") assert sql == "a %%%% b", f"raw %% should be escaped to %%%%, got: {sql}" # Operator without % → unchanged - sql, params = d.format_binary_operator("=", "a", "b", (), ()) + sql, params = binop("=", "a", "b") assert sql == "a = b" assert params == () # ? operator (hstore/jsonb) → preserved as-is, not treated as placeholder - sql, params = d.format_binary_operator("?", "data", "%s", (), ("key",)) + sql, params = binop("?", "data", "%s", ("key",)) assert sql == "data ? %s", f"? operator preserved: {sql}" assert params == ("key",) # ?| and ?& operators → preserved - sql, params = d.format_binary_operator("?|", "data", "%s", (), ("key",)) + sql, params = binop("?|", "data", "%s", ("key",)) assert sql == "data ?| %s", f"?| operator preserved: {sql}" # %# (pg_trgm) → %# (unchanged, no % to escape) - sql, params = d.format_binary_operator("%#", "a", "b", (), ()) + sql, params = binop("%#", "a", "b") assert sql == "a %%# b", f"%# operator: {sql}" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py index ef74389f..c8a067d3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py @@ -238,14 +238,14 @@ def test_format_array_type_with_postgres_element(dialect): assert sql == "UUID[]" -def test_format_array_type_unsupported_element_raises(dialect): - with pytest.raises(TypeError, match="is not supported"): - dialect.format_data_type(ArrayType(CustomType("X"), dimensions=1)) +def test_format_array_type_with_custom_element(dialect): + sql, _ = dialect.format_data_type(ArrayType(CustomType("X"), dimensions=1)) + assert sql == "X[]" -def test_format_unregistered_type_raises(dialect): - with pytest.raises(TypeError, match="does not support"): - dialect.format_data_type(CustomType("X")) +def test_format_custom_type_passthrough(dialect): + sql, _ = dialect.format_data_type(CustomType("X")) + assert sql == "X" # --------------------------------------------------------------------------- @@ -475,9 +475,9 @@ def test_vector_types_equality_and_hash(): def test_supports_data_types_registers_all_postgres_types(dialect): supported = dialect.supports_data_types() - assert isinstance(supported, list) + assert isinstance(supported, dict) assert len(supported) > 0 - classes = {cls for cls, _ in supported} + classes = set(supported.values()) for expected in ( PostgresSerialType, PostgresByteaType, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py index 540631b5..1c68d47b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field.py @@ -137,7 +137,7 @@ def _insert(self, Model, name, price, quantity): def test_derived_dict_with_lambda(self, product_class): self._insert(product_class, "D1", 100.0, 4) results = product_class.find_all( - derived={"my_discount": lambda d: Column(d, "price") * Literal(d, 0.8)} + derived={"my_discount": lambda d: Column(dialect, d, "price") * Literal(d, 0.8)} ) assert results[0].__dict__["my_discount"] == pytest.approx(80.0) @@ -185,7 +185,7 @@ def _insert(self, Model, name, price, quantity): def test_extra_derived_basic(self, product_class): self._insert(product_class, "F", 100.0, 10) results = product_class.find_all( - extra_derived={"triple_price": lambda d: Column(d, "price") * Literal(d, 3)} + extra_derived={"triple_price": lambda d: Column(dialect, d, "price") * Literal(d, 3)} ) assert len(results) == 1 assert results[0].__dict__["triple_price"] == pytest.approx(300.0) @@ -194,14 +194,14 @@ def test_extra_derived_conflict_raises(self, product_class): self._insert(product_class, "G", 50.0, 2) with pytest.raises(ValueError, match="conflicts with a declared derived field"): product_class.find_all( - extra_derived={"discounted_price": lambda d: Column(d, "price") * Literal(d, 0.5)} + extra_derived={"discounted_price": lambda d: Column(dialect, d, "price") * Literal(d, 0.5)} ) def test_derived_and_extra_derived_together(self, product_class): self._insert(product_class, "H", 40.0, 5) results = product_class.find_all( derived=True, - extra_derived={"double_qty": lambda d: Column(d, "quantity") * Literal(d, 2)} + extra_derived={"double_qty": lambda d: Column(dialect, d, "quantity") * Literal(d, 2)} ) assert results[0].discounted_price == pytest.approx(36.0) assert results[0].total_value == pytest.approx(200.0) @@ -315,7 +315,7 @@ class ConflictModel(ActiveRecord): id: Optional[int] = None discount_rate: Annotated[float, UseColumn("disc")] discounted: ClassVar[Annotated[float, DerivedField( - lambda d: Column(d, "price") * Literal(d, 0.9), + lambda d: Column(dialect, d, "price") * Literal(d, 0.9), ), UseColumn("disc")]] def test_use_column_no_conflict_different_names(self): @@ -325,7 +325,7 @@ class NoConflict(ActiveRecord): id: Optional[int] = None discount_rate: Annotated[float, UseColumn("rate")] discounted: ClassVar[Annotated[float, DerivedField( - lambda d: Column(d, "price") * Literal(d, 0.9), + lambda d: Column(dialect, d, "price") * Literal(d, 0.9), ), UseColumn("disc")]] assert NoConflict.__derived_fields__["discounted"].column_name == "disc" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py index 8720acd9..77485609 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/basic/fields/test_derived_field_async.py @@ -137,7 +137,7 @@ async def _insert(self, Model, name, price, quantity): async def test_derived_dict_with_lambda(self, async_product_class): await self._insert(async_product_class, "D1", 100.0, 4) results = await async_product_class.find_all( - derived={"my_discount": lambda d: Column(d, "price") * Literal(d, 0.8)} + derived={"my_discount": lambda d: Column(dialect, d, "price") * Literal(d, 0.8)} ) assert results[0].__dict__["my_discount"] == pytest.approx(80.0) @@ -185,7 +185,7 @@ async def _insert(self, Model, name, price, quantity): async def test_extra_derived_basic(self, async_product_class): await self._insert(async_product_class, "F", 100.0, 10) results = await async_product_class.find_all( - extra_derived={"triple_price": lambda d: Column(d, "price") * Literal(d, 3)} + extra_derived={"triple_price": lambda d: Column(dialect, d, "price") * Literal(d, 3)} ) assert len(results) == 1 assert results[0].__dict__["triple_price"] == pytest.approx(300.0) @@ -194,14 +194,14 @@ async def test_extra_derived_conflict_raises(self, async_product_class): await self._insert(async_product_class, "G", 50.0, 2) with pytest.raises(ValueError, match="conflicts with a declared derived field"): await async_product_class.find_all( - extra_derived={"discounted_price": lambda d: Column(d, "price") * Literal(d, 0.5)} + extra_derived={"discounted_price": lambda d: Column(dialect, d, "price") * Literal(d, 0.5)} ) async def test_derived_and_extra_derived_together(self, async_product_class): await self._insert(async_product_class, "H", 40.0, 5) results = await async_product_class.find_all( derived=True, - extra_derived={"double_qty": lambda d: Column(d, "quantity") * Literal(d, 2)} + extra_derived={"double_qty": lambda d: Column(dialect, d, "quantity") * Literal(d, 2)} ) assert results[0].discounted_price == pytest.approx(36.0) assert results[0].total_value == pytest.approx(200.0) @@ -315,7 +315,7 @@ class ConflictModel(ActiveRecord): id: Optional[int] = None discount_rate: Annotated[float, UseColumn("disc")] discounted: ClassVar[Annotated[float, DerivedField( - lambda d: Column(d, "price") * Literal(d, 0.9), + lambda d: Column(dialect, d, "price") * Literal(d, 0.9), ), UseColumn("disc")]] async def test_use_column_no_conflict_different_names(self): @@ -325,7 +325,7 @@ class NoConflict(ActiveRecord): id: Optional[int] = None discount_rate: Annotated[float, UseColumn("rate")] discounted: ClassVar[Annotated[float, DerivedField( - lambda d: Column(d, "price") * Literal(d, 0.9), + lambda d: Column(dialect, d, "price") * Literal(d, 0.9), ), UseColumn("disc")]] assert NoConflict.__derived_fields__["discounted"].column_name == "disc" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/relation/fixtures/models.py b/tests/rhosocial/activerecord_postgres_test/feature/relation/fixtures/models.py index 3e1f957f..7a5199d8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/relation/fixtures/models.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/relation/fixtures/models.py @@ -121,17 +121,17 @@ class User(ActiveRecord): # DerivedField: display name (coalesce email to name) display_name: ClassVar[Annotated[str, DerivedField( - lambda d: coalesce(d, Column(d, "email"), Column(d, "name")), + lambda d: coalesce(d, Column(dialect, d, "email"), Column(dialect, d, "name")), )]] # DerivedField (JSON): extract language preference language: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "settings"), "$.language"), + lambda d: json_extract_text(d, Column(dialect, d, "settings"), "$.language"), )]] # DerivedField (JSON): extract theme preference theme: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "settings"), "$.theme"), + lambda d: json_extract_text(d, Column(dialect, d, "settings"), "$.theme"), )]] # Relation @@ -159,17 +159,17 @@ class Post(ActiveRecord): # DerivedField: hotness score hotness: ClassVar[Annotated[int, DerivedField( - lambda d: Column(d, "view_count") + Literal(d, 1), + lambda d: Column(dialect, d, "view_count") + Literal(d, 1), )]] # DerivedField (JSON): first tag from metadata first_tag: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "metadata"), "$.tags[0]"), + lambda d: json_extract_text(d, Column(dialect, d, "metadata"), "$.tags[0]"), )]] # DerivedField (JSON): source from metadata source: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "metadata"), "$.source"), + lambda d: json_extract_text(d, Column(dialect, d, "metadata"), "$.source"), )]] # Relations @@ -199,7 +199,7 @@ class Comment(ActiveRecord): # DerivedField (JSON): platform from meta platform: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "meta"), "$.platform"), + lambda d: json_extract_text(d, Column(dialect, d, "meta"), "$.platform"), )]] # Relation @@ -221,15 +221,15 @@ class AsyncUser(AsyncActiveRecord): settings: Optional[str] = None display_name: ClassVar[Annotated[str, DerivedField( - lambda d: coalesce(d, Column(d, "email"), Column(d, "name")), + lambda d: coalesce(d, Column(dialect, d, "email"), Column(dialect, d, "name")), )]] language: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "settings"), "$.language"), + lambda d: json_extract_text(d, Column(dialect, d, "settings"), "$.language"), )]] theme: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "settings"), "$.theme"), + lambda d: json_extract_text(d, Column(dialect, d, "settings"), "$.theme"), )]] posts: ClassVar[AsyncHasMany["AsyncPost"]] = AsyncHasMany( @@ -254,15 +254,15 @@ class AsyncPost(AsyncActiveRecord): )]] hotness: ClassVar[Annotated[int, DerivedField( - lambda d: Column(d, "view_count") + Literal(d, 1), + lambda d: Column(dialect, d, "view_count") + Literal(d, 1), )]] first_tag: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "metadata"), "$.tags[0]"), + lambda d: json_extract_text(d, Column(dialect, d, "metadata"), "$.tags[0]"), )]] source: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "metadata"), "$.source"), + lambda d: json_extract_text(d, Column(dialect, d, "metadata"), "$.source"), )]] user: ClassVar[AsyncBelongsTo["AsyncUser"]] = AsyncBelongsTo( @@ -289,7 +289,7 @@ class AsyncComment(AsyncActiveRecord): )]] platform: ClassVar[Annotated[str, DerivedField( - lambda d: json_extract_text(d, Column(d, "meta"), "$.platform"), + lambda d: json_extract_text(d, Column(dialect, d, "meta"), "$.platform"), )]] post: ClassVar[AsyncBelongsTo["AsyncPost"]] = AsyncBelongsTo( From f0be87f48844bc0d4331c2390b3599029c63aaf1 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 11 Sep 2026 01:52:09 +0800 Subject: [PATCH 04/22] ci: test against Python 3.15.0-rc.2 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b5dc6c1..01932f94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -231,8 +231,8 @@ jobs: fail-fast: false matrix: include: - - { python-version: '3.15.0-rc.1', postgres-version: '18', freethreaded: false } - - { python-version: '3.15.0-rc.1', postgres-version: '18', freethreaded: true } + - { python-version: '3.15.0-rc.2', postgres-version: '18', freethreaded: false } + - { python-version: '3.15.0-rc.2', postgres-version: '18', freethreaded: true } services: postgres: From 95abef2399311a0c267c2c2354e23ff5aa57372b Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 11 Sep 2026 13:28:54 +0800 Subject: [PATCH 05/22] =?UTF-8?q?fix:=20Rename=20JoinExpression=E2=86=92Jo?= =?UTF-8?q?inClause=20and=20format=5Fgrouping=5Fexpression=E2=86=92format?= =?UTF-8?q?=5Fgrouping=5Fclause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated imports and usage in recursive_cte_graph_traversal.py (src + tests) - Updated import in test_property_graph_query_scenarios.py - Updated protocol conformance exclusion string in test_postgres_protocol_conformance.py --- .../examples/pgq/recursive_cte_graph_traversal.py | 10 +++++----- .../postgres/test_property_graph_query_scenarios.py | 4 ++-- .../postgres/test_recursive_cte_graph_traversal.py | 8 ++++---- .../backend/test_postgres_protocol_conformance.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) 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 2c7241df..14f4b8b9 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 @@ -57,7 +57,7 @@ from rhosocial.activerecord.backend.expression.query_parts import ( WhereClause, OrderByClause, - JoinExpression, + JoinClause, ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType @@ -164,7 +164,7 @@ ) # --- Recursive query: follow edges one hop, depth+1, bound depth<4 --- -recursive_join = JoinExpression( +recursive_join = JoinClause( dialect=dialect, left_table=TableExpression(dialect, 'traversal', alias='t'), right_table=TableExpression(dialect, 'follows', alias='f'), @@ -243,7 +243,7 @@ print("=" * 72) # --- Base query: direct sources sending to target account ACC-001 --- -aml_base_join = JoinExpression( +aml_base_join = JoinClause( dialect=dialect, left_table=TableExpression(dialect, 'transactions', alias='tx'), right_table=TableExpression(dialect, 'accounts', alias='a'), @@ -265,7 +265,7 @@ ) # --- Recursive query: trace upstream along the transaction chain --- -aml_recursive_join = JoinExpression( +aml_recursive_join = JoinClause( dialect=dialect, left_table=TableExpression(dialect, 'fund_trace', alias='tr'), right_table=TableExpression(dialect, 'transactions', alias='tx'), @@ -374,7 +374,7 @@ 1. SetOperationExpression(operation='UNION ALL') combines base + recursive queries 2. CTEExpression(name=..., query=..., columns=...) defines the CTE column signature 3. WithQueryExpression(recursive=True) generates WITH RECURSIVE -4. Recursive query joins the CTE reference with physical tables via chained JoinExpression +4. Recursive query joins the CTE reference with physical tables via chained JoinClause 5. depth field simulates the hop count of quantified path patterns 6. WHERE depth BETWEEN x AND y simulates {x,y} range constraint 7. Use cases: social recommendations, fund tracing, knowledge graph traversal diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py index 1a45fc28..b54de82d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py @@ -24,7 +24,7 @@ WhereClause, OrderByClause, GroupByHavingClause, LimitOffsetClause, ) from rhosocial.activerecord.backend.expression.core import Column -from rhosocial.activerecord.backend.expression.query_parts import JoinExpression +from rhosocial.activerecord.backend.expression.query_parts import JoinClause from rhosocial.activerecord.backend.expression.types import ( IntegerType, TextType, ) @@ -275,7 +275,7 @@ def test_graph_table_combined_with_regular_sql(self, postgres_backend, social_da GraphColumn("f", "since", "since")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") - join = JoinExpression(dialect, + join = JoinClause(dialect, left_table=gt, right_table=TableExpression(dialect, "people", alias="p"), join_type="INNER JOIN", diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index c1a211c6..291d03fb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -39,7 +39,7 @@ WhereClause, OrderByClause, GroupByHavingClause, - JoinExpression, + JoinClause, ) @@ -152,7 +152,7 @@ def _build_traversal_cte(self, dialect): where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), ) - recursive_join = JoinExpression( + recursive_join = JoinClause( dialect=dialect, left_table=TableExpression(dialect, "traversal", alias="t"), right_table=TableExpression(dialect, "follows", alias="f"), @@ -289,7 +289,7 @@ class TestAMLFundTracing: def _build_aml_cte(self, dialect): """Build recursive CTE expression for fund tracing.""" - base_join = JoinExpression( + base_join = JoinClause( dialect=dialect, left_table=TableExpression(dialect, "transactions", alias="tx"), right_table=TableExpression(dialect, "accounts", alias="a"), @@ -314,7 +314,7 @@ def _build_aml_cte(self, dialect): ), ) - recursive_join = JoinExpression( + recursive_join = JoinClause( dialect=dialect, left_table=TableExpression(dialect, "fund_trace", alias="tr"), right_table=TableExpression(dialect, "transactions", alias="tx"), diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py index 839cb1c0..e5b10c15 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py @@ -477,7 +477,7 @@ class TestProtocolMethodSignatureConformance: # ArraySupport: Mixin uses expr-based signatures ('ArraySupport', 'format_array_expression'), # AdvancedGroupingSupport: Mixin uses expr instead of named params - ('AdvancedGroupingSupport', 'format_grouping_expression'), + ('AdvancedGroupingSupport', 'format_grouping_clause'), # GraphSupport: Mixin uses expr instead of clause ('GraphSupport', 'format_match_clause'), # OrderedSetAggregationSupport: Mixin uses expr instead of aggregation From 2fae3ae75d54f395ae1c74af86b9632b11dc4c76 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 09:06:49 +0800 Subject: [PATCH 06/22] feat(dialect-protocol): formalize PostgreSQL type support and suggestion contracts - 5 type classes declare dialect_options explicitly; unified _type_params based equality/hash (hand-written eq/hash retired) - PostgresEnumType remains BaseExpression (DDL expression, not DataType); format_data_type_postgres_enum/supports pair added for future DataType subclasses - 70+ supports_data_type_* added (1:1 with format family); supports_data_types() updated to check supports() before including types - suggested_data_types() returns empty dict (PG handles all core types natively; no honest cross-backend suggestion warranted) - Precision validation: decimal 1-1000/0-precision, float 1-53, fractional-seconds 0-6 for time/timestamp types - parse_type() constructor calls converted to keyword arguments for dialect-first signatures; PostgresArrayType/CustomType calls fixed - New test_postgres_type_protocol.py (24 tests) --- .../backend/impl/postgres/expression/types.py | 72 +-- .../mixins/types/data_type_formatting.py | 380 +++++++++++++-- .../types/test_data_type_formatting.py | 12 +- .../types/test_postgres_type_protocol.py | 448 ++++++++++++++++++ 4 files changed, 827 insertions(+), 85 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index c2e7e1cc..c2b99f89 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import Optional, Set, TYPE_CHECKING +from typing import Any, Dict, Optional, Set, TYPE_CHECKING from rhosocial.activerecord.backend.expression.types import ( ArrayType, @@ -133,17 +133,13 @@ class PostgresBitType(DataType): name = "postgres_bit" n: Optional[int] = None - def __init__(self, n: Optional[int] = None, dialect: Optional["SQLDialectBase"] = None): - super().__init__(dialect) + def __init__(self, n: Optional[int] = None, dialect: Optional["SQLDialectBase"] = None, + dialect_options: Optional[Dict[str, Any]] = None): + super().__init__(dialect, dialect_options=dialect_options) self.n = n - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - return self.n == other.n - - def __hash__(self) -> int: - return hash((type(self), self.n)) + def _type_params(self) -> tuple: + return (self.n,) class PostgresVarBitType(DataType): @@ -152,17 +148,13 @@ class PostgresVarBitType(DataType): name = "postgres_varbit" n: Optional[int] = None - def __init__(self, n: Optional[int] = None, dialect: Optional["SQLDialectBase"] = None): - super().__init__(dialect) + def __init__(self, n: Optional[int] = None, dialect: Optional["SQLDialectBase"] = None, + dialect_options: Optional[Dict[str, Any]] = None): + super().__init__(dialect, dialect_options=dialect_options) self.n = n - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - return self.n == other.n - - def __hash__(self) -> int: - return hash((type(self), self.n)) + def _type_params(self) -> tuple: + return (self.n,) # --------------------------------------------------------------------------- @@ -366,17 +358,13 @@ class PostgresVectorType(DataType): name = "postgres_vector" dim: int - def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None): - super().__init__(dialect) + def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None, + dialect_options: Optional[Dict[str, Any]] = None): + super().__init__(dialect, dialect_options=dialect_options) self.dim = dim - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - return self.dim == other.dim - - def __hash__(self) -> int: - return hash((type(self), self.dim)) + def _type_params(self) -> tuple: + return (self.dim,) class PostgresHalfvecType(DataType): @@ -392,17 +380,13 @@ class PostgresHalfvecType(DataType): name = "postgres_halfvec" dim: int - def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None): - super().__init__(dialect) + def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None, + dialect_options: Optional[Dict[str, Any]] = None): + super().__init__(dialect, dialect_options=dialect_options) self.dim = dim - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - return self.dim == other.dim - - def __hash__(self) -> int: - return hash((type(self), self.dim)) + def _type_params(self) -> tuple: + return (self.dim,) class PostgresSparsevecType(DataType): @@ -418,17 +402,13 @@ class PostgresSparsevecType(DataType): name = "postgres_sparsevec" dim: int - def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None): - super().__init__(dialect) + def __init__(self, dim: int, dialect: Optional["SQLDialectBase"] = None, + dialect_options: Optional[Dict[str, Any]] = None): + super().__init__(dialect, dialect_options=dialect_options) self.dim = dim - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - return self.dim == other.dim - - def __hash__(self) -> int: - return hash((type(self), self.dim)) + def _type_params(self) -> tuple: + return (self.dim,) # --------------------------------------------------------------------------- diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 01da5916..e6e51d76 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -111,6 +111,14 @@ class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): # --- PostgreSQL-specific formatters --- + def format_data_type_postgres_enum(self, data_type) -> Tuple[str, tuple]: + """Render a PostgresEnumType reference for DDL column definitions. + + PostgreSQL ENUM types are referenced by name in column definitions; + the CREATE TYPE ... AS ENUM is a separate DDL operation. + """ + return data_type.name, () + def format_data_type_postgres_bytea(self, data_type) -> Tuple[str, tuple]: return "BYTEA", () @@ -302,6 +310,11 @@ def format_data_type_bigint(self, data_type: BigIntType) -> Tuple[str, tuple]: def format_data_type_float(self, data_type: FloatType) -> Tuple[str, tuple]: if data_type.precision is not None: + if not (1 <= data_type.precision <= 53): + raise ValueError( + f"FLOAT precision must be between 1 and 53 " + f"(PostgreSQL limit), got {data_type.precision}" + ) return f"FLOAT({data_type.precision})", () return "REAL", () @@ -312,6 +325,18 @@ def format_data_type_double(self, data_type: DoubleType) -> Tuple[str, tuple]: return "DOUBLE PRECISION", () def format_data_type_decimal(self, data_type: DecimalType) -> Tuple[str, tuple]: + if data_type.precision is not None: + if not (1 <= data_type.precision <= 1000): + raise ValueError( + f"DECIMAL precision must be between 1 and 1000 " + f"(PostgreSQL limit), got {data_type.precision}" + ) + if data_type.scale is not None: + if not (0 <= data_type.scale <= data_type.precision if data_type.precision else True): + raise ValueError( + f"DECIMAL scale must be between 0 and precision " + f"({data_type.precision}), got {data_type.scale}" + ) if data_type.precision is not None and data_type.scale is not None: return f"DECIMAL({data_type.precision},{data_type.scale})", () if data_type.precision is not None: @@ -342,11 +367,21 @@ def format_data_type_date(self, data_type: DateType) -> Tuple[str, tuple]: def format_data_type_time(self, data_type: TimeType) -> Tuple[str, tuple]: if data_type.precision is not None: + if not (0 <= data_type.precision <= 6): + raise ValueError( + f"TIME precision must be between 0 and 6 " + f"(PostgreSQL limit), got {data_type.precision}" + ) return f"TIME({data_type.precision})", () return "TIME", () def format_data_type_timetz(self, data_type: TimeTzType) -> Tuple[str, tuple]: if data_type.precision is not None: + if not (0 <= data_type.precision <= 6): + raise ValueError( + f"TIME precision must be between 0 and 6 " + f"(PostgreSQL limit), got {data_type.precision}" + ) return f"TIME({data_type.precision}) WITH TIME ZONE", () return "TIME WITH TIME ZONE", () @@ -355,11 +390,21 @@ def format_data_type_datetime(self, data_type: DateTimeType) -> Tuple[str, tuple def format_data_type_timestamp(self, data_type: TimestampType) -> Tuple[str, tuple]: if data_type.precision is not None: + if not (0 <= data_type.precision <= 6): + raise ValueError( + f"TIMESTAMP precision must be between 0 and 6 " + f"(PostgreSQL limit), got {data_type.precision}" + ) return f"TIMESTAMP({data_type.precision})", () return "TIMESTAMP", () def format_data_type_timestamptz(self, data_type: TimestampTzType) -> Tuple[str, tuple]: if data_type.precision is not None: + if not (0 <= data_type.precision <= 6): + raise ValueError( + f"TIMESTAMP precision must be between 0 and 6 " + f"(PostgreSQL limit), got {data_type.precision}" + ) return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE", () return "TIMESTAMP WITH TIME ZONE", () @@ -374,6 +419,258 @@ def format_data_type_json(self, data_type: JsonType) -> Tuple[str, tuple]: def format_data_type_jsonb(self, data_type: JsonBType) -> Tuple[str, tuple]: return "JSONB", () + # ------------------------------------------------------------------ + # supports_data_type_ — per-type capability queries + # ------------------------------------------------------------------ + + def supports_data_type_postgres_enum(self) -> bool: + return True + + def supports_data_type_postgres_bytea(self) -> bool: + return True + + def supports_data_type_postgres_smallserial(self) -> bool: + return True + + def supports_data_type_postgres_serial(self) -> bool: + return True + + def supports_data_type_postgres_bigserial(self) -> bool: + return True + + def supports_data_type_postgres_uuid(self) -> bool: + return True + + def supports_data_type_postgres_xml(self) -> bool: + return True + + def supports_data_type_postgres_character_varying(self) -> bool: + return True + + def supports_data_type_postgres_tsvector(self) -> bool: + return True + + def supports_data_type_postgres_tsquery(self) -> bool: + return True + + def supports_data_type_postgres_jsonpath(self) -> bool: + return True + + def supports_data_type_postgres_bit(self) -> bool: + return True + + def supports_data_type_postgres_varbit(self) -> bool: + return True + + def supports_data_type_postgres_inet(self) -> bool: + return True + + def supports_data_type_postgres_cidr(self) -> bool: + return True + + def supports_data_type_postgres_macaddr(self) -> bool: + return True + + def supports_data_type_postgres_macaddr8(self) -> bool: + return True + + def supports_data_type_postgres_point(self) -> bool: + return True + + def supports_data_type_postgres_line(self) -> bool: + return True + + def supports_data_type_postgres_line_segment(self) -> bool: + return True + + def supports_data_type_postgres_box(self) -> bool: + return True + + def supports_data_type_postgres_path(self) -> bool: + return True + + def supports_data_type_postgres_polygon(self) -> bool: + return True + + def supports_data_type_postgres_circle(self) -> bool: + return True + + def supports_data_type_postgres_money(self) -> bool: + return True + + def supports_data_type_postgres_int4range(self) -> bool: + return True + + def supports_data_type_postgres_int8range(self) -> bool: + return True + + def supports_data_type_postgres_numrange(self) -> bool: + return True + + def supports_data_type_postgres_tsrange(self) -> bool: + return True + + def supports_data_type_postgres_tstzrange(self) -> bool: + return True + + def supports_data_type_postgres_daterange(self) -> bool: + return True + + def supports_data_type_postgres_int4multirange(self) -> bool: + return True + + def supports_data_type_postgres_int8multirange(self) -> bool: + return True + + def supports_data_type_postgres_nummultirange(self) -> bool: + return True + + def supports_data_type_postgres_tsmultirange(self) -> bool: + return True + + def supports_data_type_postgres_tstzmultirange(self) -> bool: + return True + + def supports_data_type_postgres_datemultirange(self) -> bool: + return True + + def supports_data_type_postgres_oid(self) -> bool: + return True + + def supports_data_type_postgres_regclass(self) -> bool: + return True + + def supports_data_type_postgres_regtype(self) -> bool: + return True + + def supports_data_type_postgres_xid(self) -> bool: + return True + + def supports_data_type_postgres_xid8(self) -> bool: + return True + + def supports_data_type_postgres_cid(self) -> bool: + return True + + def supports_data_type_postgres_tid(self) -> bool: + return True + + def supports_data_type_postgres_pg_lsn(self) -> bool: + return True + + def supports_data_type_postgres_hstore(self) -> bool: + return True + + def supports_data_type_postgres_geometry(self) -> bool: + return True + + def supports_data_type_postgres_geography(self) -> bool: + return True + + def supports_data_type_postgres_vector(self) -> bool: + return True + + def supports_data_type_postgres_halfvec(self) -> bool: + return True + + def supports_data_type_postgres_sparsevec(self) -> bool: + return True + + def supports_data_type_postgres_citext(self) -> bool: + return True + + def supports_data_type_postgres_cube(self) -> bool: + return True + + def supports_data_type_postgres_ltree(self) -> bool: + return True + + def supports_data_type_postgres_raster(self) -> bool: + return True + + def supports_data_type_postgres_array(self) -> bool: + return True + + # --- Core type support --- + + def supports_data_type_tinyint(self) -> bool: + return True + + def supports_data_type_smallint(self) -> bool: + return True + + def supports_data_type_int(self) -> bool: + return True + + def supports_data_type_integer(self) -> bool: + return True + + def supports_data_type_bigint(self) -> bool: + return True + + def supports_data_type_float(self) -> bool: + return True + + def supports_data_type_real(self) -> bool: + return True + + def supports_data_type_double(self) -> bool: + return True + + def supports_data_type_decimal(self) -> bool: + return True + + def supports_data_type_char(self) -> bool: + return True + + def supports_data_type_varchar(self) -> bool: + return True + + def supports_data_type_text(self) -> bool: + return True + + def supports_data_type_boolean(self) -> bool: + return True + + def supports_data_type_blob(self) -> bool: + return True + + def supports_data_type_date(self) -> bool: + return True + + def supports_data_type_time(self) -> bool: + return True + + def supports_data_type_timetz(self) -> bool: + return True + + def supports_data_type_datetime(self) -> bool: + return True + + def supports_data_type_timestamp(self) -> bool: + return True + + def supports_data_type_timestamptz(self) -> bool: + return True + + def supports_data_type_interval(self) -> bool: + return True + + def supports_data_type_json(self) -> bool: + return True + + def supports_data_type_jsonb(self) -> bool: + return True + + def supports_data_type_uuid(self) -> bool: + return True + + def supports_data_type_custom(self) -> bool: + return True + + def supports_data_type_array(self) -> bool: + return True + # ------------------------------------------------------------------ # Supported types advertisement # ------------------------------------------------------------------ @@ -384,7 +681,8 @@ def supports_data_types(self) -> dict: The formatters a dialect defines **are** the set of types it supports (naming-convention dispatch has no registry), so this - derives the mapping from the ``format_data_type_`` methods. + derives the mapping from the ``format_data_type_`` methods + and checks ``supports_data_type_()`` for each. """ result = {} for member_name in dir(type(self)): @@ -392,11 +690,27 @@ def supports_data_types(self) -> dict: if not match: continue name = match.group(1) - cls = self._type_class_for(name) - if cls is not None: - result[name] = cls + supports = getattr(self, f"supports_data_type_{name}", None) + if supports is not None and supports(): + cls = self._type_class_for(name) + if cls is not None: + result[name] = cls return result + def suggested_data_types(self) -> dict: + """Cross-backend type-consistency suggestion map. + + PostgreSQL has native BYTEA for binary data (mapped via ``blob``), + and all core types it supports are rendered through their own + ``format_data_type_`` formatters. There are no core types + that PostgreSQL cannot handle natively and honestly suggest an + alternative for. + + Returns an empty dict — PostgreSQL covers every core type it + renders; suggestions are not needed. + """ + return {} + def format_data_type_uuid(self, data_type) -> Tuple[str, tuple]: return "UUID", () @@ -495,7 +809,7 @@ def parse_type(self, raw: str) -> DataType: base_raw = stripped[:kw_match.start()] element_type = self.parse_type(base_raw) # PG normalises all array declarations to 1-D internally - return PostgresArrayType(element_type, dimensions=1, dialect=self) + return PostgresArrayType(element_type=element_type, dimensions=1, dialect=self) # Check for array bracket suffix: e.g. "INTEGER[]", "INTEGER[][]" remaining = stripped @@ -511,7 +825,7 @@ def parse_type(self, raw: str) -> DataType: if remaining != stripped: element_type = self.parse_type(remaining) # PG normalises all array declarations to 1-D internally - return PostgresArrayType(element_type, dimensions=1, dialect=self) + return PostgresArrayType(element_type=element_type, dimensions=1, dialect=self) # ---- Standard type dispatch ---- @@ -539,31 +853,31 @@ def parse_type(self, raw: str) -> DataType: # Float family if self._PG_FLOAT_TYPES.match(upper): if upper.startswith("DOUBLE"): - return DoubleType(self) + return DoubleType(dialect=self) if upper.startswith("REAL") or upper.startswith("FLOAT4"): - return RealType(self) + return RealType(dialect=self) nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None - return FloatType(precision, self) + return FloatType(dialect=self, precision=precision) # Decimal family if self._PG_DECIMAL_TYPES.match(upper): nums = re.findall(r"\d+", stripped) if len(nums) >= 2: - return DecimalType(int(nums[0]), int(nums[1]), self) + return DecimalType(dialect=self, precision=int(nums[0]), scale=int(nums[1])) if len(nums) == 1: - return DecimalType(int(nums[0]), self) - return DecimalType(self) + return DecimalType(dialect=self, precision=int(nums[0])) + return DecimalType(dialect=self) # String family if self._PG_STRING_TYPES.match(upper): length_match = re.search(r"\((\d+)\)", stripped) length = int(length_match.group(1)) if length_match else None if upper.startswith("VARCHAR") or upper.startswith("CHARACTER VARYING"): - return VarCharType(length, self) + return VarCharType(dialect=self, length=length) if upper.startswith("CHAR") or upper.startswith("CHARACTER"): - return CharType(length, self) - return TextType(self) + return CharType(dialect=self, length=length) + return TextType(dialect=self) # Binary if self._PG_BINARY_TYPES.match(upper): @@ -573,43 +887,43 @@ def parse_type(self, raw: str) -> DataType: # Date/time if self._PG_DATE_TYPES.match(upper): if upper.startswith("DATETIME"): - return DateTimeType(self) + return DateTimeType(dialect=self) if upper.startswith("DATE"): - return DateType(self) + return DateType(dialect=self) if upper.startswith("TIMESTAMP"): nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None if "WITH TIME ZONE" in upper or upper.startswith("TIMESTAMPTZ"): - return TimestampTzType(precision, self) - return TimestampType(precision, self) + return TimestampTzType(dialect=self, precision=precision) + return TimestampType(dialect=self, precision=precision) if upper.startswith("TIME"): nums = re.findall(r"\d+", stripped) precision = int(nums[0]) if nums else None if "WITH TIME ZONE" in upper or upper.startswith("TIMETZ"): - return TimeTzType(precision, self) - return TimeType(precision, self) + return TimeTzType(dialect=self, precision=precision) + return TimeType(dialect=self, precision=precision) if upper.startswith("INTERVAL"): fields_match = re.search(r"INTERVAL\s+(.*)", upper) fields = fields_match.group(1).strip() if fields_match else None - return IntervalType(fields, self) + return IntervalType(dialect=self, fields=fields) # JSON if self._PG_JSON_TYPES.match(upper): if upper.startswith("JSONB"): - return JsonBType(self) + return JsonBType(dialect=self) if upper.startswith("JSONPATH"): from ...expression.types import PostgresJsonPathType - return PostgresJsonPathType(self) - return JsonType(self) + return PostgresJsonPathType(dialect=self) + return JsonType(dialect=self) # UUID if self._PG_UUID_TYPES.match(upper): from ...expression.types import PostgresUUIDType - return PostgresUUIDType(self) + return PostgresUUIDType(dialect=self) # Boolean if upper.startswith("BOOLEAN") or upper.startswith("BOOL"): - return BooleanType(self) + return BooleanType(dialect=self) # Bit string if self._PG_BIT_TYPES.match(upper): @@ -617,9 +931,9 @@ def parse_type(self, raw: str) -> DataType: n = int(nums[0]) if nums else None if upper.startswith("BIT"): from ...expression.types import PostgresBitType - return PostgresBitType(n, self) + return PostgresBitType(n=n, dialect=self) from ...expression.types import PostgresVarBitType - return PostgresVarBitType(n, self) + return PostgresVarBitType(n=n, dialect=self) # Network address if self._PG_NET_TYPES.match(upper): @@ -783,13 +1097,13 @@ def parse_type(self, raw: str) -> DataType: vector_kind = vector_match.group(1) if vector_kind == "HALFVEC": from ...expression.types import PostgresHalfvecType - return PostgresHalfvecType(dim, self) + return PostgresHalfvecType(dim=dim, dialect=self) if vector_kind == "SPARSEVEC": from ...expression.types import PostgresSparsevecType - return PostgresSparsevecType(dim, self) + return PostgresSparsevecType(dim=dim, dialect=self) from ...expression.types import PostgresVectorType - return PostgresVectorType(dim, self) + return PostgresVectorType(dim=dim, dialect=self) # Fallback from rhosocial.activerecord.backend.expression.types import CustomType - return CustomType(stripped, self) \ No newline at end of file + return CustomType(dialect=self, raw=stripped) \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py index c8a067d3..82bbec8a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_data_type_formatting.py @@ -221,30 +221,30 @@ def test_format_core_types(dialect, data_type, expected): def test_format_array_type(dialect): - sql, _ = dialect.format_data_type(ArrayType(IntegerType(), dimensions=1)) + sql, _ = dialect.format_data_type(ArrayType(element_type=IntegerType(), dimensions=1)) assert sql == "INTEGER[]" - sql, _ = dialect.format_data_type(ArrayType(VarCharType(10), dimensions=3)) + sql, _ = dialect.format_data_type(ArrayType(element_type=VarCharType(length=10), dimensions=3)) assert sql == "VARCHAR(10)[][][]" def test_format_array_type_multidimensional(dialect): - sql, _ = dialect.format_data_type(ArrayType(IntegerType(), dimensions=2)) + sql, _ = dialect.format_data_type(ArrayType(element_type=IntegerType(), dimensions=2)) assert sql == "INTEGER[][]" def test_format_array_type_with_postgres_element(dialect): - sql, _ = dialect.format_data_type(ArrayType(PostgresUUIDType(), dimensions=1)) + sql, _ = dialect.format_data_type(ArrayType(element_type=PostgresUUIDType(), dimensions=1)) assert sql == "UUID[]" def test_format_array_type_with_custom_element(dialect): - sql, _ = dialect.format_data_type(ArrayType(CustomType("X"), dimensions=1)) + sql, _ = dialect.format_data_type(ArrayType(element_type=CustomType(raw="X"), dimensions=1)) assert sql == "X[]" def test_format_custom_type_passthrough(dialect): - sql, _ = dialect.format_data_type(CustomType("X")) + sql, _ = dialect.format_data_type(CustomType(raw="X")) assert sql == "X" 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 new file mode 100644 index 00000000..4ecf2bd8 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py @@ -0,0 +1,448 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py +"""Tests for the formalised core DataType protocol conformance. + +Verifies: +- format family == supports family (1:1 correspondence) +- supports_data_types() includes postgres_* + core entries +- suggested_data_types(): values are DataType classes; keys disjoint from supported +- dialect_options forwarding and equality +""" + +import pytest + +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, + BlobType, + BooleanType, + CharType, + DataType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + IntType, + JsonBType, + JsonType, + RealType, + SmallIntType, + TextType, + TimeType, + TimestampType, + TinyIntType, + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCharacterVaryingType, + PostgresCidrType, + PostgresCircleType, + PostgresCitextType, + PostgresCubeType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHalfvecType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresLtreeType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRasterType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresSparsevecType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, +) + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(16, 0, 0)) + + +# --------------------------------------------------------------------------- +# W2: format_data_type_postgres_enum / supports_data_type_postgres_enum pair +# --------------------------------------------------------------------------- + + +def test_format_data_type_postgres_enum_exists(dialect): + """format_data_type_postgres_enum method exists on the dialect.""" + assert hasattr(dialect, "format_data_type_postgres_enum") + assert callable(getattr(dialect, "format_data_type_postgres_enum")) + + +def test_supports_data_type_postgres_enum_exists(dialect): + """supports_data_type_postgres_enum method exists on the dialect.""" + assert hasattr(dialect, "supports_data_type_postgres_enum") + assert callable(getattr(dialect, "supports_data_type_postgres_enum")) + assert dialect.supports_data_type_postgres_enum() is True + + +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 + + enum_ref = PostgresEnumType( + dialect=dialect, + name="video_status", + values=["pending", "processing", "ready"], + ) + sql, params = dialect.format_enum_type_expression(enum_ref) + assert sql == "video_status" + assert params == () + + +def test_postgres_enum_expression_with_schema_renders_via_dialect(dialect): + from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + + enum_ref = PostgresEnumType( + dialect=dialect, + name="video_status", + values=["pending", "processing"], + schema="app", + ) + sql, params = dialect.format_enum_type_expression(enum_ref) + assert sql == "app.video_status" + assert params == () + + +# --------------------------------------------------------------------------- +# W3: format family == supports family (1:1) +# --------------------------------------------------------------------------- + +# Collect all format_data_type_* and supports_data_type_* names from the dialect +# and verify they match exactly. + + +def _get_method_suffixes(dialect, prefix): + """Extract method name suffixes matching ``prefix_``.""" + import re + pattern = re.compile(rf"^{prefix}([a-z][a-z0-9_]*)$") + suffixes = set() + for member_name in dir(type(dialect)): + match = pattern.match(member_name) + if match: + suffixes.add(match.group(1)) + return suffixes + + +def test_format_supports_family_1to1(dialect): + """Every format_data_type_ must have supports_data_type_ and vice versa.""" + format_names = _get_method_suffixes(dialect, "format_data_type_") + supports_names = _get_method_suffixes(dialect, "supports_data_type_") + + # Remove non-dispatch names (these are helpers, not type names) + format_names.discard("custom") + supports_names.discard("custom") + # supports_data_types and format_data_type are the entry points + format_names.discard("") + supports_names.discard("") + + missing_supports = format_names - supports_names + missing_format = supports_names - format_names + + assert not missing_supports, ( + f"format_data_type_ methods without corresponding supports_data_type_: " + f"{sorted(missing_supports)}" + ) + assert not missing_format, ( + f"supports_data_type_ methods without corresponding format_data_type_: " + f"{sorted(missing_format)}" + ) + + +# --------------------------------------------------------------------------- +# W3: supports_data_types() includes postgres_* + core entries +# --------------------------------------------------------------------------- + + +def test_supports_data_types_includes_postgres_types(dialect): + supported = dialect.supports_data_types() + assert isinstance(supported, dict) + assert len(supported) > 0 + + # Verify a representative sample of postgres-specific types + postgres_expected = { + "postgres_bytea": PostgresByteaType, + "postgres_serial": PostgresSerialType, + "postgres_uuid": PostgresUUIDType, + "postgres_vector": PostgresVectorType, + "postgres_tsrange": PostgresTsRangeType, + "postgres_inet": PostgresInetType, + "postgres_point": PostgresPointType, + "postgres_jsonpath": PostgresJsonPathType, + } + for name, cls in postgres_expected.items(): + assert name in supported, f"Missing postgres type: {name}" + assert supported[name] is cls, ( + f"Type {name}: expected {cls.__name__}, got {supported[name].__name__}" + ) + + +def test_supports_data_types_includes_core_types(dialect): + supported = dialect.supports_data_types() + + core_expected = { + "integer": IntegerType, + "bigint": BigIntType, + "smallint": SmallIntType, + "decimal": DecimalType, + "varchar": VarCharType, + "text": TextType, + "boolean": BooleanType, + "blob": BlobType, + "date": None, # DateType + "timestamp": None, # TimestampType + "json": JsonType, + "jsonb": JsonBType, + } + for name in core_expected: + assert name in supported, f"Missing core type: {name}" + + +def test_supports_data_types_values_are_data_type_classes(dialect): + supported = dialect.supports_data_types() + for name, cls in supported.items(): + assert isinstance(cls, type), ( + f"supports_data_types()[{name!r}] is not a class: {cls!r}" + ) + assert issubclass(cls, DataType), ( + f"supports_data_types()[{name!r}] = {cls.__name__} is not a DataType subclass" + ) + + +# --------------------------------------------------------------------------- +# W4: suggested_data_types() +# --------------------------------------------------------------------------- + + +def test_suggested_data_types_returns_dict(dialect): + result = dialect.suggested_data_types() + assert isinstance(result, dict) + + +def test_suggested_data_types_values_are_classes(dialect): + result = dialect.suggested_data_types() + for name, cls in result.items(): + assert isinstance(cls, type), ( + f"suggested_data_types()[{name!r}] is not a class: {cls!r}" + ) + assert issubclass(cls, DataType), ( + f"suggested_data_types()[{name!r}] = {cls.__name__} is not a DataType subclass" + ) + + +def test_suggested_data_types_keys_disjoint_from_supported(dialect): + supported = set(dialect.supports_data_types().keys()) + suggested = set(dialect.suggested_data_types().keys()) + overlap = supported & suggested + assert not overlap, ( + f"suggested_data_types() keys overlap with supports_data_types(): {overlap}" + ) + + +# --------------------------------------------------------------------------- +# W1: dialect_options forwarding and equality +# --------------------------------------------------------------------------- + + +def test_bit_type_dialect_options_forwarded(): + t1 = PostgresBitType(n=8, dialect_options={"flag": True}) + t2 = PostgresBitType(n=8, dialect_options={"flag": True}) + t3 = PostgresBitType(n=8, dialect_options={"flag": False}) + t4 = PostgresBitType(n=8) + + # Same options → equal + assert t1 == t2 + assert hash(t1) == hash(t2) + + # Different options → not equal + assert t1 != t3 + assert t1 != t4 + + +def test_vector_type_dialect_options_forwarded(): + t1 = PostgresVectorType(dim=384, dialect_options={"metric": "cosine"}) + t2 = PostgresVectorType(dim=384, dialect_options={"metric": "cosine"}) + t3 = PostgresVectorType(dim=384, dialect_options={"metric": "l2"}) + + assert t1 == t2 + assert hash(t1) == hash(t2) + assert t1 != t3 + + +def test_halfvec_type_dialect_options_forwarded(): + t1 = PostgresHalfvecType(dim=128, dialect_options={"quantize": True}) + t2 = PostgresHalfvecType(dim=128, dialect_options={"quantize": True}) + t3 = PostgresHalfvecType(dim=128) + + assert t1 == t2 + assert hash(t1) == hash(t2) + assert t1 != t3 + + +def test_sparsevec_type_dialect_options_forwarded(): + t1 = PostgresSparsevecType(dim=16, dialect_options={"sparse": True}) + t2 = PostgresSparsevecType(dim=16, dialect_options={"sparse": True}) + t3 = PostgresSparsevecType(dim=16) + + assert t1 == t2 + assert hash(t1) == hash(t2) + assert t1 != t3 + + +def test_varbit_type_dialect_options_forwarded(): + t1 = PostgresVarBitType(n=16, dialect_options={"strict": True}) + t2 = PostgresVarBitType(n=16, dialect_options={"strict": True}) + t3 = PostgresVarBitType(n=16) + + assert t1 == t2 + assert hash(t1) == hash(t2) + assert t1 != t3 + + +# --------------------------------------------------------------------------- +# W1: _type_params() based equality replaces hand-written __eq__/__hash__ +# --------------------------------------------------------------------------- + + +def test_bit_type_type_params(): + t1 = PostgresBitType(n=8) + t2 = PostgresBitType(n=8) + t3 = PostgresBitType(n=16) + t4 = PostgresBitType() + + assert t1 == t2 + assert t1 != t3 + assert t1 != t4 + assert hash(t1) == hash(t2) + assert hash(t1) != hash(t3) + + +def test_vector_type_type_params(): + t1 = PostgresVectorType(dim=384) + t2 = PostgresVectorType(dim=384) + t3 = PostgresVectorType(dim=768) + + assert t1 == t2 + assert t1 != t3 + assert hash(t1) == hash(t2) + assert hash(t1) != hash(t3) + + +def test_vector_types_cross_class_inequality(): + """Different vector types with same dim are not equal.""" + assert PostgresVectorType(3) != PostgresHalfvecType(3) + assert PostgresVectorType(3) != PostgresSparsevecType(3) + assert PostgresHalfvecType(3) != PostgresSparsevecType(3) + + +# --------------------------------------------------------------------------- +# W5: Precision validation +# --------------------------------------------------------------------------- + + +def test_decimal_precision_validation(dialect): + """PostgreSQL allows DECIMAL precision up to 1000.""" + # Valid + sql, _ = dialect.format_data_type(DecimalType(precision=1000, scale=0)) + assert sql == "DECIMAL(1000,0)" + + # Invalid: precision 0 + with pytest.raises(ValueError, match="precision must be between 1 and 1000"): + dialect.format_data_type(DecimalType(precision=0)) + + # Invalid: precision > 1000 + with pytest.raises(ValueError, match="precision must be between 1 and 1000"): + dialect.format_data_type(DecimalType(precision=1001)) + + +def test_float_precision_validation(dialect): + """PostgreSQL allows FLOAT precision up to 53.""" + # Valid + sql, _ = dialect.format_data_type(FloatType(precision=53)) + assert sql == "FLOAT(53)" + + # Invalid: precision 0 + with pytest.raises(ValueError, match="precision must be between 1 and 53"): + dialect.format_data_type(FloatType(precision=0)) + + # Invalid: precision > 53 + with pytest.raises(ValueError, match="precision must be between 1 and 53"): + dialect.format_data_type(FloatType(precision=54)) + + +def test_timestamp_precision_validation(dialect): + """PostgreSQL allows TIMESTAMP precision 0-6.""" + # Valid + sql, _ = dialect.format_data_type(TimestampType(precision=6)) + assert sql == "TIMESTAMP(6)" + + # Invalid: precision -1 + with pytest.raises(ValueError, match="precision must be between 0 and 6"): + dialect.format_data_type(TimestampType(precision=-1)) + + # Invalid: precision 7 + with pytest.raises(ValueError, match="precision must be between 0 and 6"): + dialect.format_data_type(TimestampType(precision=7)) + + +def test_time_precision_validation(dialect): + """PostgreSQL allows TIME precision 0-6.""" + # Valid + sql, _ = dialect.format_data_type(TimeType(precision=6)) + assert sql == "TIME(6)" + + # Invalid: precision 7 + with pytest.raises(ValueError, match="precision must be between 0 and 6"): + dialect.format_data_type(TimeType(precision=7)) + + +# --------------------------------------------------------------------------- +# PostgresEnumType not a DataType — protocol boundary +# --------------------------------------------------------------------------- + + +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 + + assert not issubclass(PostgresEnumType, DataType) + assert issubclass(PostgresEnumType, BaseExpression) From 9df24dea461eeeb6307253e4c115252a5834f871 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 10:29:07 +0800 Subject: [PATCH 07/22] fix(test): VarCharType dialect-first call sites in fixtures and examples --- .../schema_diff/column_order_irrelevant.py | 4 +- .../examples/schema_diff/index_type_diff.py | 2 +- .../schema_diff/postgres10_identity.py | 4 +- .../schema_diff/serialization_roundtrip.py | 6 +- .../examples/schema_diff/table_changes.py | 2 +- tests/providers/fixtures/basic.py | 56 +++++++++---------- tests/providers/fixtures/events.py | 6 +- tests/providers/fixtures/mixins.py | 10 ++-- tests/providers/fixtures/query.py | 48 ++++++++-------- .../postgres/test_core_type_mappings.py | 6 +- .../test_recursive_cte_graph_traversal.py | 8 +-- .../backend/test_auto_increment_ddl.py | 2 +- .../feature/backend/test_create_table_like.py | 4 +- .../backend/test_postgres_dialect_security.py | 2 +- 14 files changed, 80 insertions(+), 80 deletions(-) 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 1015e988..d321390f 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 @@ -48,8 +48,8 @@ dialect=dialect, table="demo", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(100)), - ColumnDefinition("email", VarCharType(200)), + ColumnDefinition("name", VarCharType(length=100)), + ColumnDefinition("email", VarCharType(length=200)), ] ) sql, params = demo_table.to_sql() 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 eb2cc5d2..8baaba48 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 @@ -48,7 +48,7 @@ dialect=dialect, table="demo", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(200)), + ColumnDefinition("name", VarCharType(length=200)), ColumnDefinition("tags", ArrayType(TextType(), dimensions=1)), ] ) 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 7075b33e..72adc5c5 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 @@ -54,7 +54,7 @@ dialect=dialect, table="pg9_style", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(100)), + ColumnDefinition("name", VarCharType(length=100)), ] ) sql, params = pg9_table.to_sql() @@ -69,7 +69,7 @@ ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)], dialect_options={"identity": "ALWAYS"}), - ColumnDefinition("name", VarCharType(100)), + ColumnDefinition("name", VarCharType(length=100)), ] ) sql, params = pg10_table.to_sql() 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 fb41ddd0..8c5ca1bd 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 @@ -48,8 +48,8 @@ dialect=dialect, table="users", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(100)), - ColumnDefinition("email", VarCharType(200)), + ColumnDefinition("name", VarCharType(length=100)), + ColumnDefinition("email", VarCharType(length=200)), ] ) sql, params = users_table.to_sql() @@ -75,7 +75,7 @@ # Modify the database add_phone = AlterTableExpression(dialect, "users", [ - AddColumn(dialect, ColumnDefinition("phone", VarCharType(20))) + AddColumn(dialect, ColumnDefinition("phone", VarCharType(length=20))) ]) sql, params = add_phone.to_sql() backend.execute(sql, params) 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 e9665e0b..c3626923 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 @@ -60,7 +60,7 @@ dialect=dialect, table="users", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(100), + ColumnDefinition("name", VarCharType(length=100), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.NOT_NULL)]), ] ) diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py index 36e3b436..5613b7d1 100644 --- a/tests/providers/fixtures/basic.py +++ b/tests/providers/fixtures/basic.py @@ -59,9 +59,9 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpress columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "username", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "balance", DecimalType(precision=10, scale=2, dialect=dialect), @@ -88,9 +88,9 @@ def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTa columns=[ ColumnDefinition(dialect, "id", PostgresUUIDType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "username", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tiny_int", SmallIntType(dialect=dialect)), ColumnDefinition(dialect, "small_int", SmallIntType(dialect=dialect)), @@ -98,8 +98,8 @@ def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTa ColumnDefinition(dialect, "float_val", FloatType(dialect=dialect)), ColumnDefinition(dialect, "double_val", DoubleType(dialect=dialect)), ColumnDefinition(dialect, "decimal_val", DecimalType(precision=10, scale=4, dialect=dialect)), - ColumnDefinition(dialect, "char_val", VarCharType(255, dialect=dialect)), - ColumnDefinition(dialect, "varchar_val", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "char_val", VarCharType(length=255, dialect=dialect)), + ColumnDefinition(dialect, "varchar_val", VarCharType(length=255, dialect=dialect)), ColumnDefinition(dialect, "text_val", TextType(dialect=dialect)), ColumnDefinition(dialect, "date_val", DateType(dialect=dialect)), ColumnDefinition(dialect, "time_val", TimeTzType(dialect=dialect)), @@ -130,7 +130,7 @@ def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTa columns=[ ColumnDefinition(dialect, "id", PostgresUUIDType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "string_field", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "string_field", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "int_field", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), @@ -143,7 +143,7 @@ def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTa ColumnDefinition(dialect, "datetime_field", TimestampTzType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "json_field", JsonBType(dialect=dialect)), - ColumnDefinition(dialect, "nullable_field", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "nullable_field", VarCharType(length=255, dialect=dialect)), ], table_constraints=[ TableConstraint(dialect, constraint_type=TableConstraintType.PRIMARY_KEY, @@ -164,9 +164,9 @@ def create_validated_field_users_table(dialect, table_name: str = "validated_fie columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "username", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "balance", DecimalType(precision=10, scale=2, dialect=dialect), @@ -174,7 +174,7 @@ def create_validated_field_users_table(dialect, table_name: str = "validated_fie ColumnDefinition(dialect, "credit_score", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=300)]), - ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "status", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="active")]), ColumnDefinition(dialect, "is_active", BooleanType(dialect=dialect), @@ -196,9 +196,9 @@ def create_validated_users_table(dialect, table_name: str = "validated_users") - columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "username", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "username", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), ], @@ -217,15 +217,15 @@ def create_pydantic_validated_models_table(dialect, table_name: str = "pydantic_ columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "code", VarCharType(32, dialect=dialect)), + ColumnDefinition(dialect, "code", VarCharType(length=32, dialect=dialect)), ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "step_count", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "price", DecimalType(precision=10, scale=2, dialect=dialect)), ColumnDefinition(dialect, "start_at", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "end_at", TimestampTzType(dialect=dialect)), - ColumnDefinition(dialect, "status", VarCharType(32, dialect=dialect)), - ColumnDefinition(dialect, "normalized_name", VarCharType(50, dialect=dialect)), - ColumnDefinition(dialect, "created_token", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "status", VarCharType(length=32, dialect=dialect)), + ColumnDefinition(dialect, "normalized_name", VarCharType(length=50, dialect=dialect)), + ColumnDefinition(dialect, "created_token", VarCharType(length=255, dialect=dialect)), ], ) @@ -242,11 +242,11 @@ def create_bulk_users_table(dialect, table_name: str = "bulk_users") -> CreateTa columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "age", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="")]), ], ) @@ -266,7 +266,7 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "author", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "title", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "content", TextType(dialect=dialect)), ColumnDefinition(dialect, "published_at", TimestampTzType(dialect=dialect)), @@ -327,7 +327,7 @@ def create_column_mapping_items_table(dialect, table_name: str = "column_mapping columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "item_total", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), @@ -348,7 +348,7 @@ def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annota columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tags", ArrayType(TextType(dialect=dialect), dialect=dialect)), ColumnDefinition(dialect, "meta", TextType(dialect=dialect)), @@ -370,15 +370,15 @@ def create_type_adapter_tests_table(dialect, table_name: str = "type_adapter_tes columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "optional_name", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "optional_name", VarCharType(length=255, dialect=dialect)), ColumnDefinition(dialect, "optional_age", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "last_login", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "is_premium", BooleanType(dialect=dialect)), - ColumnDefinition(dialect, "unsupported_union", VarCharType(255, dialect=dialect)), - ColumnDefinition(dialect, "custom_bool", VarCharType(10, dialect=dialect)), - ColumnDefinition(dialect, "optional_custom_bool", VarCharType(10, dialect=dialect)), + ColumnDefinition(dialect, "unsupported_union", VarCharType(length=255, dialect=dialect)), + ColumnDefinition(dialect, "custom_bool", VarCharType(length=10, dialect=dialect)), + ColumnDefinition(dialect, "optional_custom_bool", VarCharType(length=10, dialect=dialect)), ], ) @@ -424,7 +424,7 @@ def create_store_inventory_table(dialect, table_name: str = "store_inventory") - constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "product_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "batch_id", VarCharType(64, dialect=dialect), + ColumnDefinition(dialect, "batch_id", VarCharType(length=64, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "stock", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py index 159ec651..3e942487 100644 --- a/tests/providers/fixtures/events.py +++ b/tests/providers/fixtures/events.py @@ -37,9 +37,9 @@ def create_event_tests_table(dialect, table_name: str = "event_tests") -> Create columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "status", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="draft")]), ColumnDefinition(dialect, "revision", IntegerType(dialect=dialect), @@ -64,7 +64,7 @@ def create_event_tracking_models_table(dialect, table_name: str = "event_trackin columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "title", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "content", TextType(dialect=dialect)), ColumnDefinition(dialect, "view_count", IntegerType(dialect=dialect), diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py index 2c463bd3..9b706da8 100644 --- a/tests/providers/fixtures/mixins.py +++ b/tests/providers/fixtures/mixins.py @@ -39,10 +39,10 @@ def create_combined_articles_table(dialect, table_name: str = "combined_articles columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "title", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "content", TextType(dialect=dialect)), - ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "status", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="draft")]), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), @@ -68,7 +68,7 @@ def create_tasks_table(dialect, table_name: str = "tasks") -> CreateTableExpress columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "title", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "is_completed", BooleanType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), @@ -91,7 +91,7 @@ def create_timestamped_posts_table(dialect, table_name: str = "timestamped_posts columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "title", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "content", TextType(dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), @@ -112,7 +112,7 @@ def create_versioned_products_table(dialect, table_name: str = "versioned_produc columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "price", DecimalType(precision=10, scale=2, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py index efeaaf77..2f0fbdec 100644 --- a/tests/providers/fixtures/query.py +++ b/tests/providers/fixtures/query.py @@ -44,9 +44,9 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpress columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "username", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "balance", DecimalType(precision=10, scale=2, dialect=dialect), @@ -75,10 +75,10 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "title", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "title", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "content", TextType(dialect=dialect)), - ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "status", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="published")]), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), @@ -137,12 +137,12 @@ def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpre constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "order_number", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "order_number", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "total_amount", DecimalType(precision=10, scale=2, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "status", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="pending")]), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), @@ -168,7 +168,7 @@ def create_order_items_table(dialect, table_name: str = "order_items") -> Create constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "order_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "product_name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "product_name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), @@ -203,7 +203,7 @@ def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableE ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "bio", TextType(dialect=dialect)), - ColumnDefinition(dialect, "avatar_url", VarCharType(512, dialect=dialect)), + ColumnDefinition(dialect, "avatar_url", VarCharType(length=512, dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], @@ -225,9 +225,9 @@ def create_json_users_table(dialect, table_name: str = "json_users") -> CreateTa columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "username", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "username", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "email", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "email", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "age", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), @@ -255,7 +255,7 @@ def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpress columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "parent_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NULL)]), @@ -284,7 +284,7 @@ def create_searchable_items_table(dialect, table_name: str = "searchable_items") columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect)), + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect)), ColumnDefinition(dialect, "tags", TextType(dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), @@ -306,25 +306,25 @@ def create_extended_orders_table(dialect, table_name: str = "extended_orders") - constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "user_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "order_number", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "order_number", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "total_amount", DecimalType(precision=10, scale=2, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=0.00)]), - ColumnDefinition(dialect, "status", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "status", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="pending")]), - ColumnDefinition(dialect, "priority", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "priority", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="medium")]), - ColumnDefinition(dialect, "region", VarCharType(50, dialect=dialect), + ColumnDefinition(dialect, "region", VarCharType(length=50, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value="default")]), - ColumnDefinition(dialect, "category", VarCharType(255, dialect=dialect)), - ColumnDefinition(dialect, "product", VarCharType(255, dialect=dialect)), - ColumnDefinition(dialect, "department", VarCharType(255, dialect=dialect)), - ColumnDefinition(dialect, "year", VarCharType(10, dialect=dialect)), - ColumnDefinition(dialect, "quarter", VarCharType(10, dialect=dialect)), + ColumnDefinition(dialect, "category", VarCharType(length=255, dialect=dialect)), + ColumnDefinition(dialect, "product", VarCharType(length=255, dialect=dialect)), + ColumnDefinition(dialect, "department", VarCharType(length=255, dialect=dialect)), + ColumnDefinition(dialect, "year", VarCharType(length=10, dialect=dialect)), + ColumnDefinition(dialect, "quarter", VarCharType(length=10, dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], @@ -349,15 +349,15 @@ def create_extended_order_items_table(dialect, table_name: str = "extended_order constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "order_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "product_name", VarCharType(255, dialect=dialect), + ColumnDefinition(dialect, "product_name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "quantity", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=1)]), ColumnDefinition(dialect, "price", DecimalType(precision=10, scale=2, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "category", VarCharType(255, dialect=dialect)), - ColumnDefinition(dialect, "region", VarCharType(50, dialect=dialect)), + ColumnDefinition(dialect, "category", VarCharType(length=255, dialect=dialect)), + ColumnDefinition(dialect, "region", VarCharType(length=50, dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index 47ca6a5c..1d123d9e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -75,7 +75,7 @@ def test_numeric(self, dialect): # String family def test_varchar(self, dialect): from rhosocial.activerecord.backend.expression.types import VarCharType - assert VarCharType(255, dialect=dialect).to_sql() == ("VARCHAR(255)", ()) + assert VarCharType(length=255, dialect=dialect).to_sql() == ("VARCHAR(255)", ()) assert VarCharType(dialect=dialect).to_sql() == ("VARCHAR", ()) def test_text(self, dialect): @@ -116,7 +116,7 @@ def test_array_varchar_rendering(self, dialect): PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import VarCharType - arr = PostgresArrayType(VarCharType(255, dialect=dialect), dialect=dialect) + arr = PostgresArrayType(VarCharType(length=255, dialect=dialect), dialect=dialect) assert arr.to_sql() == ("VARCHAR(255)[]", ()) def test_array_boolean_rendering(self, dialect): @@ -136,7 +136,7 @@ def test_array_equality(self, dialect): ) a1 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) a2 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) - a3 = PostgresArrayType(VarCharType(255, dialect=dialect), 2, dialect=dialect) + a3 = PostgresArrayType(VarCharType(length=255, dialect=dialect), 2, dialect=dialect) assert a1 == a2 assert a1 != a3 assert hash(a1) == hash(a2) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index 291d03fb..84a19f93 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -55,7 +55,7 @@ def social_network_data(postgres_backend): backend.execute(*CreateTableExpression(dialect, "users", [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(100, dialect=dialect)), + ColumnDefinition(dialect, "name", VarCharType(length=100, dialect=dialect)), ]).to_sql()) backend.execute(*CreateTableExpression(dialect, "follows", [ @@ -100,8 +100,8 @@ def aml_data(postgres_backend): backend.execute(*CreateTableExpression(dialect, "accounts", [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "account_holder", VarCharType(100, dialect=dialect)), - ColumnDefinition(dialect, "account_type", VarCharType(20, dialect=dialect)), + ColumnDefinition(dialect, "account_holder", VarCharType(length=100, dialect=dialect)), + ColumnDefinition(dialect, "account_type", VarCharType(length=20, dialect=dialect)), ]).to_sql()) backend.execute(*CreateTableExpression(dialect, "transactions", [ @@ -467,7 +467,7 @@ async def async_social_network_data(self, async_postgres_backend): await backend.execute(*CreateTableExpression(dialect, "users", [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition(dialect, "name", VarCharType(100, dialect=dialect)), + ColumnDefinition(dialect, "name", VarCharType(length=100, dialect=dialect)), ]).to_sql()) await backend.execute(*CreateTableExpression(dialect, "follows", [ diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py index e93c2e14..6fb0a85b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_auto_increment_ddl.py @@ -40,7 +40,7 @@ def _build_table(dialect): ColumnDefinition(dialect, "flag", BooleanType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), - ColumnDefinition(dialect, "code", VarCharType(16, dialect=dialect)), + ColumnDefinition(dialect, "code", VarCharType(length=16, dialect=dialect)), ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect)), ], ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py index 5fe52d61..09be55ce 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py @@ -210,7 +210,7 @@ def test_like_ignores_columns(self): ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect)) + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect)) ] create_expr = CreateTableExpression( dialect=dialect, @@ -264,7 +264,7 @@ def test_fallback_to_base_when_no_like(self): ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition(dialect, "name", VarCharType(255, dialect=dialect), constraints=[ + ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL) ]) ] diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py index 1e52412a..e972f799 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py @@ -51,7 +51,7 @@ def test_postgres_format_column_definition_data_type_validation(dialect): col_def = ColumnDefinition( dialect, name="test_col", - data_type=VarCharType(255, dialect=dialect), + data_type=VarCharType(length=255, dialect=dialect), ) sql, params = dialect.format_column_definition(col_def) From 15dfa9982555c61f4ac1b43eea00c5caec014519 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 11:19:28 +0800 Subject: [PATCH 08/22] fix(test): ArrayType/DecimalType/TimestampType dialect-first call sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ArrayType(IntegerType(...), dialect=...) → ArrayType(element_type=..., dialect=...) - DecimalType(10, 2, dialect=...) → DecimalType(precision=10, scale=2, dialect=...) - TimestampType(6, dialect=...) → TimestampType(fsp=6, dialect=...) - Hash tests updated: hash(X) == hash(X._type_params()) --- .../examples/schema_diff/array_type_diff.py | 8 +++---- .../examples/schema_diff/index_type_diff.py | 2 +- tests/providers/fixtures/basic.py | 4 ++-- tests/providers/partition.py | 2 +- .../extensions/test_btree_gin_integration.py | 4 ++-- .../extensions/test_intarray_integration.py | 20 ++++++++-------- .../postgres/test_core_type_mappings.py | 24 +++++++++---------- .../test_recursive_cte_graph_traversal.py | 2 +- .../feature/backend/test_partition_explain.py | 2 +- .../backend/test_partition_operations.py | 2 +- .../backend/types/test_expression_types.py | 16 ++++++------- 11 files changed, 43 insertions(+), 43 deletions(-) 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 5b0583c7..25d30404 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 @@ -49,7 +49,7 @@ dialect=dialect, table="scores_v1", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("values", ArrayType(IntegerType(), dimensions=1)), + ColumnDefinition("values", ArrayType(element_type=IntegerType(), dimensions=1)), ] ) sql, params = scores_v1_table.to_sql() @@ -60,7 +60,7 @@ dialect=dialect, table="scores_v2", columns=[ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("values", ArrayType(IntegerType(), dimensions=2)), + ColumnDefinition("values", ArrayType(element_type=IntegerType(), dimensions=2)), ] ) sql, params = scores_v2_table.to_sql() @@ -102,7 +102,7 @@ # V1: 1-d array col1d = ColumnInfo( name="values", table_name="t", ordinal_position=1, - data_type="INTEGER[]", parsed_data_type=ArrayType(IntegerType(), dimensions=1), + data_type="INTEGER[]", parsed_data_type=ArrayType(element_type=IntegerType(), dimensions=1), nullable=ColumnNullable.NULLABLE, ) snap1d = snapshot.__class__( @@ -115,7 +115,7 @@ # V2: 2-d array col2d = ColumnInfo( name="values", table_name="t", ordinal_position=1, - data_type="INTEGER[][]", parsed_data_type=ArrayType(IntegerType(), dimensions=2), + data_type="INTEGER[][]", parsed_data_type=ArrayType(element_type=IntegerType(), dimensions=2), nullable=ColumnNullable.NULLABLE, ) snap2d = snapshot.__class__( 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 8baaba48..2689021a 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 @@ -49,7 +49,7 @@ ColumnDefinition("id", PostgresSerialType(), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition("name", VarCharType(length=200)), - ColumnDefinition("tags", ArrayType(TextType(), dimensions=1)), + ColumnDefinition("tags", ArrayType(element_type=TextType(), dimensions=1)), ] ) sql, params = demo_table.to_sql() diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py index 5613b7d1..fec5d58f 100644 --- a/tests/providers/fixtures/basic.py +++ b/tests/providers/fixtures/basic.py @@ -106,7 +106,7 @@ def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTa ColumnDefinition(dialect, "timestamp_val", TimestampTzType(dialect=dialect)), ColumnDefinition(dialect, "blob_val", PostgresByteaType(dialect=dialect)), ColumnDefinition(dialect, "json_val", JsonBType(dialect=dialect)), - ColumnDefinition(dialect, "array_val", ArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, "array_val", ArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ColumnDefinition(dialect, "is_active", BooleanType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL), ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=True)]), @@ -350,7 +350,7 @@ def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annota constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(length=255, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "tags", ArrayType(TextType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, "tags", ArrayType(element_type=TextType(dialect=dialect), dialect=dialect)), ColumnDefinition(dialect, "meta", TextType(dialect=dialect)), ColumnDefinition(dialect, "description", TextType(dialect=dialect)), ColumnDefinition(dialect, "status", TextType(dialect=dialect)), diff --git a/tests/providers/partition.py b/tests/providers/partition.py index 4bc494e4..dd6bc575 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -256,7 +256,7 @@ def _create_parent_sql(self, dialect): ColumnDefinition(dialect, "created_at", TimestampType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tenant_id", IntegerType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "payload", TextType(dialect=dialect)), - ColumnDefinition(dialect, "amount", DecimalType(12, 2, dialect=dialect)), + ColumnDefinition(dialect, "amount", DecimalType(precision=12, scale=2, dialect=dialect)), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py index e6adbf55..aef65b2c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py @@ -67,7 +67,7 @@ def btree_gin_env(postgres_backend_single): ), ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ColumnDefinition(dialect, name="value", data_type=IntegerType(dialect=dialect)), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(TextType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=TextType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -222,7 +222,7 @@ async def async_btree_gin_env(async_postgres_backend_single): ), ColumnDefinition(dialect, name="name", data_type=TextType(dialect=dialect)), ColumnDefinition(dialect, name="value", data_type=IntegerType(dialect=dialect)), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(TextType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=TextType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py index c51a1033..fef4b978 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py @@ -61,7 +61,7 @@ def _setup_intarray_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -99,7 +99,7 @@ def _setup_intarray_overlap_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -137,7 +137,7 @@ def _setup_intarray_contained_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -175,7 +175,7 @@ def _setup_intarray_idx_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -213,7 +213,7 @@ def _setup_intarray_gin_table(backend, dialect, table, index_name): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -265,7 +265,7 @@ async def _async_setup_intarray_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -303,7 +303,7 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -341,7 +341,7 @@ async def _async_setup_intarray_contained_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -379,7 +379,7 @@ async def _async_setup_intarray_idx_table(backend, dialect, table): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, @@ -417,7 +417,7 @@ async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect)), + ColumnDefinition(dialect, name="tags", data_type=PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect)), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index 1d123d9e..cc8631ef 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -69,7 +69,7 @@ def test_double(self, dialect): def test_numeric(self, dialect): from rhosocial.activerecord.backend.expression.types import DecimalType - assert DecimalType(10, 2, dialect=dialect).to_sql() == ("DECIMAL(10,2)", ()) + assert DecimalType(precision=10, scale=2, dialect=dialect).to_sql() == ("DECIMAL(10,2)", ()) assert DecimalType(dialect=dialect).to_sql() == ("DECIMAL", ()) # String family @@ -100,7 +100,7 @@ def test_array_1d_rendering(self, dialect): PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import IntegerType - arr = PostgresArrayType(IntegerType(dialect=dialect), dialect=dialect) + arr = PostgresArrayType(element_type=IntegerType(dialect=dialect), dialect=dialect) assert arr.to_sql() == ("INTEGER[]", ()) def test_array_2d_rendering(self, dialect): @@ -108,7 +108,7 @@ def test_array_2d_rendering(self, dialect): PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import IntegerType - arr = PostgresArrayType(IntegerType(dialect=dialect), dimensions=2, dialect=dialect) + arr = PostgresArrayType(element_type=IntegerType(dialect=dialect), dimensions=2, dialect=dialect) assert arr.to_sql() == ("INTEGER[][]", ()) def test_array_varchar_rendering(self, dialect): @@ -116,7 +116,7 @@ def test_array_varchar_rendering(self, dialect): PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import VarCharType - arr = PostgresArrayType(VarCharType(length=255, dialect=dialect), dialect=dialect) + arr = PostgresArrayType(element_type=VarCharType(length=255, dialect=dialect), dialect=dialect) assert arr.to_sql() == ("VARCHAR(255)[]", ()) def test_array_boolean_rendering(self, dialect): @@ -124,7 +124,7 @@ def test_array_boolean_rendering(self, dialect): PostgresArrayType, ) from rhosocial.activerecord.backend.expression.types import BooleanType - arr = PostgresArrayType(BooleanType(dialect=dialect), dialect=dialect) + arr = PostgresArrayType(element_type=BooleanType(dialect=dialect), dialect=dialect) assert arr.to_sql() == ("BOOLEAN[]", ()) def test_array_equality(self, dialect): @@ -134,14 +134,14 @@ def test_array_equality(self, dialect): from rhosocial.activerecord.backend.expression.types import ( IntegerType, VarCharType, ) - a1 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) - a2 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) - a3 = PostgresArrayType(VarCharType(length=255, dialect=dialect), 2, dialect=dialect) + a1 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 2, dialect=dialect) + a2 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 2, dialect=dialect) + a3 = PostgresArrayType(element_type=VarCharType(length=255, dialect=dialect), 2, dialect=dialect) assert a1 == a2 assert a1 != a3 assert hash(a1) == hash(a2) # PostgresArrayType.is_equivalent ignores dimensions - a4 = PostgresArrayType(IntegerType(dialect=dialect), 1, dialect=dialect) + a4 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 1, dialect=dialect) assert a1.is_equivalent(a4) def test_parse_array_bracket_suffix(self, dialect): @@ -198,9 +198,9 @@ def test_is_element_type_equivalent(self, dialect): from rhosocial.activerecord.backend.expression.types import ( IntegerType, SmallIntType, ) - a1 = PostgresArrayType(IntegerType(dialect=dialect), 2, dialect=dialect) - a2 = PostgresArrayType(IntegerType(dialect=dialect), 1, dialect=dialect) # different dimension - a3 = PostgresArrayType(SmallIntType(dialect=dialect), dialect=dialect) # different element type + a1 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 2, dialect=dialect) + a2 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 1, dialect=dialect) # different dimension + a3 = PostgresArrayType(element_type=SmallIntType(dialect=dialect), dialect=dialect) # different element type # Same element, different dimension → equivalent assert a1.is_element_type_equivalent(a2) # Different element → not equivalent diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py index 84a19f93..6ce11a75 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_recursive_cte_graph_traversal.py @@ -109,7 +109,7 @@ def aml_data(postgres_backend): ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "source_account_id", IntegerType(dialect=dialect)), ColumnDefinition(dialect, "target_account_id", IntegerType(dialect=dialect)), - ColumnDefinition(dialect, "amount", DecimalType(12, 2, dialect=dialect)), + ColumnDefinition(dialect, "amount", DecimalType(precision=12, scale=2, dialect=dialect)), ]).to_sql()) backend.execute(*InsertExpression(dialect, "accounts", columns=["id", "account_holder", "account_type"], 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 fc56195a..f3f18d2b 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 @@ -70,7 +70,7 @@ def _create_parent_table_expression(dialect): columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "created_at", TimestampType(6, dialect=dialect), + ColumnDefinition(dialect, "created_at", TimestampType(fsp=6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tenant_id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py index c7becee8..0da10b98 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py @@ -207,7 +207,7 @@ def _create_production_parent_sql(dialect): table=PRODUCTION_PARTITION_TABLE, columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "created_at", TimestampType(6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(fsp=6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tenant_id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py index e8a96295..4835f0a2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py @@ -54,7 +54,7 @@ def test_none_vs_value(self): assert PostgresBitType(None) != PostgresBitType(8) def test_hash(self): - assert hash(PostgresBitType(8)) == hash((type(PostgresBitType(8)), 8)) + assert hash(PostgresBitType(8)) == hash(PostgresBitType(8)._type_params()) class TestPostgresVarBitTypeEquality: @@ -69,7 +69,7 @@ def test_not_equal_types(self): assert PostgresVarBitType(16) != object() def test_hash(self): - assert hash(PostgresVarBitType(16)) == hash((type(PostgresVarBitType(16)), 16)) + assert hash(PostgresVarBitType(16)) == hash(PostgresVarBitType(16)._type_params()) class TestPostgresVectorTypeEquality: @@ -84,21 +84,21 @@ def test_not_equal_types(self): assert PostgresVectorType(384) != "vector" def test_hash(self): - assert hash(PostgresVectorType(384)) == hash((type(PostgresVectorType(384)), 384)) + assert hash(PostgresVectorType(384)) == hash(PostgresVectorType(384)._type_params()) class TestPostgresArrayType: def test_is_equivalent_matching_element(self): - arr1 = PostgresArrayType(IntegerType(), dimensions=1) - arr2 = PostgresArrayType(IntegerType(), dimensions=3) + arr1 = PostgresArrayType(element_type=IntegerType(), dimensions=1) + arr2 = PostgresArrayType(element_type=IntegerType(), dimensions=3) assert arr1.is_equivalent(arr2) def test_is_equivalent_non_array(self): - arr = PostgresArrayType(IntegerType(), dimensions=1) + arr = PostgresArrayType(element_type=IntegerType(), dimensions=1) assert arr.is_equivalent(IntegerType()) is False assert arr.is_equivalent(None) is False def test_is_equivalent_different_element(self): - arr1 = PostgresArrayType(IntegerType(), dimensions=1) - arr2 = PostgresArrayType(VarCharType(), dimensions=1) + arr1 = PostgresArrayType(element_type=IntegerType(), dimensions=1) + arr2 = PostgresArrayType(element_type=VarCharType(), dimensions=1) assert arr1.is_equivalent(arr2) is False From cfbfcb0ff4a18144b1bd6c4dd64f83f94a2f19ef Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 12:00:54 +0800 Subject: [PATCH 09/22] fix(test): TimestampType precision keyword, ArrayType element_type keyword --- .../feature/backend/test_partition_explain.py | 2 +- .../feature/backend/test_partition_operations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 f3f18d2b..cc4480b7 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 @@ -70,7 +70,7 @@ def _create_parent_table_expression(dialect): columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "created_at", TimestampType(fsp=6, dialect=dialect), + ColumnDefinition(dialect, "created_at", TimestampType(precision=6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tenant_id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py index 0da10b98..ba245139 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py @@ -207,7 +207,7 @@ def _create_production_parent_sql(dialect): table=PRODUCTION_PARTITION_TABLE, columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), - ColumnDefinition(dialect, "created_at", TimestampType(fsp=6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), + ColumnDefinition(dialect, "created_at", TimestampType(precision=6, dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "tenant_id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "payload", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ], From 38292180335af595105b431ca1b819b962499bd6 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 13:04:13 +0800 Subject: [PATCH 10/22] fix(test): hash tests use (type, _type_params) tuple --- .../feature/backend/types/test_expression_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py index 4835f0a2..16dd3e39 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_expression_types.py @@ -54,7 +54,7 @@ def test_none_vs_value(self): assert PostgresBitType(None) != PostgresBitType(8) def test_hash(self): - assert hash(PostgresBitType(8)) == hash(PostgresBitType(8)._type_params()) + assert hash(PostgresBitType(8)) == hash((type(PostgresBitType(8)), PostgresBitType(8)._type_params())) class TestPostgresVarBitTypeEquality: @@ -69,7 +69,7 @@ def test_not_equal_types(self): assert PostgresVarBitType(16) != object() def test_hash(self): - assert hash(PostgresVarBitType(16)) == hash(PostgresVarBitType(16)._type_params()) + assert hash(PostgresVarBitType(16)) == hash((type(PostgresVarBitType(16)), PostgresVarBitType(16)._type_params())) class TestPostgresVectorTypeEquality: @@ -84,7 +84,7 @@ def test_not_equal_types(self): assert PostgresVectorType(384) != "vector" def test_hash(self): - assert hash(PostgresVectorType(384)) == hash(PostgresVectorType(384)._type_params()) + assert hash(PostgresVectorType(384)) == hash((type(PostgresVectorType(384)), PostgresVectorType(384)._type_params())) class TestPostgresArrayType: From 381ac53c95e8fa72dc6eba6df6ce33f490b8652f Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 14:55:44 +0800 Subject: [PATCH 11/22] fix(test): ArrayType dimensions keyword in test_core_type_mappings --- .../backend/postgres/test_core_type_mappings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index cc8631ef..63ce1194 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -134,14 +134,14 @@ def test_array_equality(self, dialect): from rhosocial.activerecord.backend.expression.types import ( IntegerType, VarCharType, ) - a1 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 2, dialect=dialect) - a2 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 2, dialect=dialect) - a3 = PostgresArrayType(element_type=VarCharType(length=255, dialect=dialect), 2, dialect=dialect) + a1 = PostgresArrayType(element_type=IntegerType(dialect=dialect), dimensions=2, dialect=dialect) + a2 = PostgresArrayType(element_type=IntegerType(dialect=dialect), dimensions=2, dialect=dialect) + a3 = PostgresArrayType(element_type=VarCharType(length=255, dialect=dialect), dimensions=2, dialect=dialect) assert a1 == a2 assert a1 != a3 assert hash(a1) == hash(a2) # PostgresArrayType.is_equivalent ignores dimensions - a4 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 1, dialect=dialect) + a4 = PostgresArrayType(element_type=IntegerType(dialect=dialect), dimensions=1, dialect=dialect) assert a1.is_equivalent(a4) def test_parse_array_bracket_suffix(self, dialect): @@ -198,8 +198,8 @@ def test_is_element_type_equivalent(self, dialect): from rhosocial.activerecord.backend.expression.types import ( IntegerType, SmallIntType, ) - a1 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 2, dialect=dialect) - a2 = PostgresArrayType(element_type=IntegerType(dialect=dialect), 1, dialect=dialect) # different dimension + a1 = PostgresArrayType(element_type=IntegerType(dialect=dialect), dimensions=2, dialect=dialect) + a2 = PostgresArrayType(element_type=IntegerType(dialect=dialect), dimensions=1, dialect=dialect) # different dimension a3 = PostgresArrayType(element_type=SmallIntType(dialect=dialect), dialect=dialect) # different element type # Same element, different dimension → equivalent assert a1.is_element_type_equivalent(a2) From b820f742a8557b0cc92b235c80ed885b69ebc6d2 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 17:02:21 +0800 Subject: [PATCH 12/22] fix(examples): adapt example code to dialect-first ColumnDefinition/DataType convention Update ColumnDefinition and DataType subclass constructors to use dialect-first signatures throughout examples: - ColumnDefinition(dialect, name, type, ...) - PostgresSerialType(dialect), ArrayType(dialect, ...), IntegerType(dialect), etc. Aligns with parameter-inline refactor in core library. --- .../examples/named_migrations/expressions.py | 19 +++++++++++-------- .../examples/schema_diff/array_type_diff.py | 8 ++++---- .../schema_diff/column_order_irrelevant.py | 8 ++++---- .../examples/schema_diff/index_type_diff.py | 6 +++--- .../schema_diff/postgres10_identity.py | 8 ++++---- .../schema_diff/serialization_roundtrip.py | 8 ++++---- .../examples/schema_diff/table_changes.py | 10 +++++----- 7 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py index a37101fd..09dae565 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/expressions.py @@ -26,12 +26,13 @@ def create_users_table(dialect): table="users", columns=[ ColumnDefinition( + dialect, "id", - PostgresSerialType(), + PostgresSerialType(dialect), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)], ), - ColumnDefinition("name", PostgresCharacterVaryingType(255)), - ColumnDefinition("email", PostgresCharacterVaryingType(255)), + ColumnDefinition(dialect, "name", PostgresCharacterVaryingType(dialect, length=255)), + ColumnDefinition(dialect, "email", PostgresCharacterVaryingType(dialect, length=255)), ], ) @@ -48,12 +49,13 @@ def create_posts_table(dialect): table="posts", columns=[ ColumnDefinition( + dialect, "id", - PostgresSerialType(), + PostgresSerialType(dialect), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)], ), - ColumnDefinition("title", PostgresCharacterVaryingType(255)), - ColumnDefinition("user_id", PostgresCharacterVaryingType(255)), + ColumnDefinition(dialect, "title", PostgresCharacterVaryingType(dialect, length=255)), + ColumnDefinition(dialect, "user_id", PostgresCharacterVaryingType(dialect, length=255)), ], ) @@ -74,11 +76,12 @@ def create_custom_table(dialect, table_name: str = "custom_table"): table=table_name, columns=[ ColumnDefinition( + dialect, "id", - PostgresSerialType(), + PostgresSerialType(dialect), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)], ), - ColumnDefinition("value", PostgresCharacterVaryingType(255)), + ColumnDefinition(dialect, "value", PostgresCharacterVaryingType(dialect, length=255)), ], ) 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 25d30404..3ee9ea7d 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 @@ -47,9 +47,9 @@ # One-dimensional array column scores_v1_table = CreateTableExpression( dialect=dialect, table="scores_v1", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("values", ArrayType(element_type=IntegerType(), dimensions=1)), + ColumnDefinition(dialect, "values", ArrayType(dialect, element_type=IntegerType(dialect), dimensions=1)), ] ) sql, params = scores_v1_table.to_sql() @@ -58,9 +58,9 @@ # Two-dimensional array column scores_v2_table = CreateTableExpression( dialect=dialect, table="scores_v2", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("values", ArrayType(element_type=IntegerType(), dimensions=2)), + ColumnDefinition(dialect, "values", ArrayType(dialect, element_type=IntegerType(dialect), dimensions=2)), ] ) sql, params = scores_v2_table.to_sql() 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 d321390f..86b9f629 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 @@ -46,10 +46,10 @@ demo_table = CreateTableExpression( dialect=dialect, table="demo", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(length=100)), - ColumnDefinition("email", VarCharType(length=200)), + ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), + ColumnDefinition(dialect, "email", VarCharType(dialect, length=200)), ] ) sql, params = demo_table.to_sql() @@ -71,7 +71,7 @@ # Add a column between name and email — shifts ordinal positions # but PostgresSchemaDiffer ignores ordinal_position. add_age = AlterTableExpression(dialect, "demo", [ - AddColumn(dialect, ColumnDefinition("age", IntegerType())) + AddColumn(dialect, ColumnDefinition(dialect, "age", IntegerType(dialect))) ]) sql, params = add_age.to_sql() backend.execute(sql, params) 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 2689021a..1c8598eb 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 @@ -46,10 +46,10 @@ demo_table = CreateTableExpression( dialect=dialect, table="demo", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(length=200)), - ColumnDefinition("tags", ArrayType(element_type=TextType(), dimensions=1)), + ColumnDefinition(dialect, "name", VarCharType(dialect, length=200)), + ColumnDefinition(dialect, "tags", ArrayType(dialect, element_type=TextType(dialect), dimensions=1)), ] ) sql, params = demo_table.to_sql() 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 72adc5c5..b1d11b9d 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 @@ -52,9 +52,9 @@ # PostgreSQL 9-compatible: SERIAL shorthand pg9_table = CreateTableExpression( dialect=dialect, table="pg9_style", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(length=100)), + ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), ] ) sql, params = pg9_table.to_sql() @@ -66,10 +66,10 @@ try: pg10_table = CreateTableExpression( dialect=dialect, table="pg10_style", columns=[ - ColumnDefinition("id", IntegerType(), + ColumnDefinition(dialect, "id", IntegerType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)], dialect_options={"identity": "ALWAYS"}), - ColumnDefinition("name", VarCharType(length=100)), + ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), ] ) sql, params = pg10_table.to_sql() 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 8c5ca1bd..c138e480 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 @@ -46,10 +46,10 @@ users_table = CreateTableExpression( dialect=dialect, table="users", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(length=100)), - ColumnDefinition("email", VarCharType(length=200)), + ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), + ColumnDefinition(dialect, "email", VarCharType(dialect, length=200)), ] ) sql, params = users_table.to_sql() @@ -75,7 +75,7 @@ # Modify the database add_phone = AlterTableExpression(dialect, "users", [ - AddColumn(dialect, ColumnDefinition("phone", VarCharType(length=20))) + AddColumn(dialect, ColumnDefinition(dialect, "phone", VarCharType(dialect, length=20))) ]) sql, params = add_phone.to_sql() backend.execute(sql, params) 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 c3626923..e6cc2703 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 @@ -58,9 +58,9 @@ users_table = CreateTableExpression( dialect=dialect, table="users", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", VarCharType(length=100), + ColumnDefinition(dialect, "name", VarCharType(dialect, length=100), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.NOT_NULL)]), ] ) @@ -69,10 +69,10 @@ orders_table = CreateTableExpression( dialect=dialect, table="orders", columns=[ - ColumnDefinition("id", PostgresSerialType(), + ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", IntegerType()), - ColumnDefinition("amount", DecimalType(10, 2)), + ColumnDefinition(dialect, "user_id", IntegerType(dialect)), + ColumnDefinition(dialect, "amount", DecimalType(dialect, precision=10, scale=2)), ], table_constraints=[ TableConstraint( From 7da14ea5830f176f4d856e962c7ed3e95a8b0be6 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 12 Sep 2026 23:54:19 +0800 Subject: [PATCH 13/22] test(protocol): add negative protocol list + partition completeness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Complete POSTGRES_PROTOCOLS base list with 5 previously-omitted generic protocols (AlterTableModifierSupport, DDLTypeSupport, GraphTableSupport, ILIKESupport, TriggerSupport). - Add POSTGRES_NOT_IMPLEMENTED (intentional: generic FunctionSupport [Postgres uses PostgresRoutineSupport]; known gaps: AutoIncrementSupport, GeneratedColumnSupport — see TODO comments). - Add negative isinstance test + partition completeness test. - Whitelist AlterTableModifierSupport/PostgresConstraintSupport and ILIKESupport/PostgresILIKESupport overlaps. --- .../test_postgres_protocol_conformance.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py index e5b10c15..50fc47a2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py @@ -114,6 +114,12 @@ def get_postgres_protocols(): dialect_protocols.IntrospectionSupport, dialect_protocols.TransactionControlSupport, dialect_protocols.SQLFunctionSupport, + # Generic protocols Postgres also satisfies (previously omitted). + dialect_protocols.AlterTableModifierSupport, + dialect_protocols.DDLTypeSupport, + dialect_protocols.GraphTableSupport, + dialect_protocols.ILIKESupport, + dialect_protocols.TriggerSupport, ] postgres_mro = postgres_dialect.PostgresDialect.__mro__ @@ -148,6 +154,69 @@ def test_implements_protocol(self, dialect, protocol): ) +# Generic protocols PostgresDialect intentionally does NOT implement. +# +# Listing them makes the omission a deliberate, tested contract: if Postgres +# ever satisfies one by accident, the negative test fails and forces a +# conscious decision (move to POSTGRES_PROTOCOLS or revert). +POSTGRES_NOT_IMPLEMENTED = [ + # --- Intentional non-support --- + # Postgres exposes routine DDL through its own PostgresRoutineSupport + # protocol rather than the generic SQL/PSM FunctionSupport. + dialect_protocols.FunctionSupport, + # --- Known gaps (feature exists, generic protocol not yet declared) --- + # TODO: Postgres supports SERIAL / GENERATED ... AS IDENTITY; compose + # AutoIncrementMixin and move this to POSTGRES_PROTOCOLS. + dialect_protocols.AutoIncrementSupport, + # TODO: Postgres supports STORED generated columns since 12; implement + # GeneratedColumnMixin overrides and move to POSTGRES_PROTOCOLS. + dialect_protocols.GeneratedColumnSupport, +] + + +def get_all_generic_protocols() -> dict: + """Discover every generic dialect protocol defined in protocols.py.""" + from typing import Protocol + + discovered = {} + for name, obj in inspect.getmembers(dialect_protocols, inspect.isclass): + if Protocol in getattr(obj, "__mro__", []) and name.endswith("Support"): + discovered[name] = obj + return discovered + + +class TestPostgresDialectNegativeProtocolConformance: + """Assert PostgresDialect does not implement intentionally-unsupported protocols.""" + + @pytest.fixture + def dialect(self): + return postgres_dialect.PostgresDialect() + + @pytest.mark.parametrize("protocol", POSTGRES_NOT_IMPLEMENTED) + def test_does_not_implement_protocol(self, dialect, protocol): + """PostgresDialect must NOT implement any protocol in POSTGRES_NOT_IMPLEMENTED.""" + assert not isinstance(dialect, protocol), ( + f"PostgresDialect unexpectedly implements {protocol.__name__}. " + f"If intentional, move it from POSTGRES_NOT_IMPLEMENTED to " + f"POSTGRES_PROTOCOLS (and implement the behaviour fully)." + ) + + def test_positive_and_negative_lists_partition_all_protocols(self): + """Every generic protocol must be classified for Postgres.""" + all_protos = set(get_all_generic_protocols()) + positive = {p.__name__ for p in POSTGRES_PROTOCOLS if p.__module__ == dialect_protocols.__name__} + negative = {p.__name__ for p in POSTGRES_NOT_IMPLEMENTED} + + overlap = positive & negative + assert not overlap, f"Protocols in BOTH lists: {sorted(overlap)}" + + unclassified = all_protos - positive - negative + assert not unclassified, ( + f"Generic protocols not classified for Postgres: {sorted(unclassified)}. " + f"Add each to POSTGRES_PROTOCOLS or POSTGRES_NOT_IMPLEMENTED." + ) + + class TestProtocolNonOverlap: """Assert protocols do not have overlapping method names.""" @@ -253,6 +322,11 @@ def test_no_interface_overlap_between_protocols(self): # PostgresFeaturesSupport shares some stored procedure methods ('PostgresFeaturesSupport', 'PostgresStoredProcedureSupport'), ('PostgresStoredProcedureSupport', 'PostgresFeaturesSupport'), + # Generic capabilities restated by PG-specific protocols + ('AlterTableModifierSupport', 'PostgresConstraintSupport'), + ('PostgresConstraintSupport', 'AlterTableModifierSupport'), + ('ILIKESupport', 'PostgresILIKESupport'), + ('PostgresILIKESupport', 'ILIKESupport'), } violations = [] From 31ee182dc5ddfadac4e34a3d7a619798c263acba Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 00:19:22 +0800 Subject: [PATCH 14/22] feat(dialect): declare AutoIncrement + GeneratedColumn protocol support Postgres supports SERIAL/IDENTITY and (12+) STORED generated columns but never declared the generic protocols. Add the missing methods and promote both protocols from POSTGRES_NOT_IMPLEMENTED to the positive list. --- .../backend/impl/postgres/dialect.py | 28 +++++++++++++++++++ .../test_postgres_protocol_conformance.py | 12 ++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 291ca007..bfda9cfd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -744,6 +744,34 @@ def supports_json_arrow_operators(self) -> bool: """PostgreSQL supports -> and ->> operators for JSON/JSONB access.""" return True + def supports_auto_increment(self) -> bool: + """Whether AUTO_INCREMENT/IDENTITY column attributes are supported. + + PostgreSQL supports SERIAL and GENERATED ... AS IDENTITY. + """ + return True + + def supports_generated_columns(self) -> bool: + """Whether generated (computed) columns are supported. + + PostgreSQL supports STORED generated columns since version 12. + """ + return self.version >= (12, 0, 0) + + def supports_stored_generated_columns(self) -> bool: + """Whether STORED generated columns are supported. + + PostgreSQL generated columns are always STORED. + """ + return self.supports_generated_columns() + + def supports_virtual_generated_columns(self) -> bool: + """Whether VIRTUAL generated columns are supported. + + PostgreSQL has no VIRTUAL generated columns. + """ + return False + def supports_lock_strength(self, strength) -> bool: """ Check if a specific lock strength is supported. diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py index 50fc47a2..f35f2f52 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_protocol_conformance.py @@ -120,6 +120,8 @@ def get_postgres_protocols(): dialect_protocols.GraphTableSupport, dialect_protocols.ILIKESupport, dialect_protocols.TriggerSupport, + dialect_protocols.AutoIncrementSupport, + dialect_protocols.GeneratedColumnSupport, ] postgres_mro = postgres_dialect.PostgresDialect.__mro__ @@ -164,13 +166,6 @@ def test_implements_protocol(self, dialect, protocol): # Postgres exposes routine DDL through its own PostgresRoutineSupport # protocol rather than the generic SQL/PSM FunctionSupport. dialect_protocols.FunctionSupport, - # --- Known gaps (feature exists, generic protocol not yet declared) --- - # TODO: Postgres supports SERIAL / GENERATED ... AS IDENTITY; compose - # AutoIncrementMixin and move this to POSTGRES_PROTOCOLS. - dialect_protocols.AutoIncrementSupport, - # TODO: Postgres supports STORED generated columns since 12; implement - # GeneratedColumnMixin overrides and move to POSTGRES_PROTOCOLS. - dialect_protocols.GeneratedColumnSupport, ] @@ -327,6 +322,9 @@ def test_no_interface_overlap_between_protocols(self): ('PostgresConstraintSupport', 'AlterTableModifierSupport'), ('ILIKESupport', 'PostgresILIKESupport'), ('PostgresILIKESupport', 'ILIKESupport'), + # Generic generated-column capability also declared by PG features protocol + ('GeneratedColumnSupport', 'PostgresFeaturesSupport'), + ('PostgresFeaturesSupport', 'GeneratedColumnSupport'), } violations = [] From 400ee4c53a34292fb72fb52b6d9077b396d4b5f8 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 09:51:54 +0800 Subject: [PATCH 15/22] fix(dialect): adapt format_fulltext_match/array_expression to expression-based signature Core protocol changed from scatter args to (self, expr). Update backend overrides and tests to construct expression objects. --- .../backend/impl/postgres/mixins/ddl/index.py | 15 ++++---- .../feature/backend/postgres/test_index_pg.py | 35 ++++++++++++++++--- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py index 3e576e0b..ee654b26 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py @@ -18,6 +18,9 @@ CreateIndexExpression, DropIndexExpression, ) + from rhosocial.activerecord.backend.expression.statements.fulltext_match import ( + FulltextMatchExpression, + ) class PostgresIndexMixin: @@ -236,25 +239,23 @@ def supports_fulltext_query_expansion(self) -> bool: return False def format_fulltext_match( - self, columns: List[str], search_term: str, mode: Optional[str] = None + self, expr: "FulltextMatchExpression" ) -> Tuple[str, Tuple]: """Format tsvector @@ tsquery expression for PostgreSQL full-text search. Produces: to_tsvector('config', col1 || ' ' || col2) @@ tsquery_func(search_term) Args: - columns: Columns to include in the tsvector expression - search_term: Search term to convert to tsquery - mode: Search mode: - - None / 'NATURAL LANGUAGE': plainto_tsquery (plain text) - - 'BOOLEAN': to_tsquery (operators &, |, !) - - 'PHRASE': phraseto_tsquery (phrase matching) + expr: FulltextMatchExpression node carrying columns, search_term, and mode. Returns: Tuple of (SQL string, parameters tuple) """ config = self._FT_DEFAULT_CONFIG ph = self.get_parameter_placeholder() + columns = expr.columns + search_term = expr.search_term + mode = expr.mode if len(columns) == 1: tsvector_expr = f"to_tsvector('{config}', {self.format_identifier(columns[0])})" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py index f79732e9..8ccf4742 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_index_pg.py @@ -505,32 +505,57 @@ def test_supports_fulltext_boolean_mode(self): assert PostgresDialect().supports_fulltext_boolean_mode() is True def test_format_fulltext_match_natural_language(self): - sql, params = PostgresDialect().format_fulltext_match(["col"], "search") + from rhosocial.activerecord.backend.expression.statements.fulltext_match import ( + FulltextMatchExpression, + ) + dialect = PostgresDialect() + expr = FulltextMatchExpression(dialect, ["col"], "search") + sql, params = dialect.format_fulltext_match(expr) assert "to_tsvector" in sql assert "plainto_tsquery" in sql assert "@@" in sql assert params == ("search",) def test_format_fulltext_match_boolean(self): - sql, params = PostgresDialect().format_fulltext_match(["col"], "a & b", mode="BOOLEAN") + from rhosocial.activerecord.backend.expression.statements.fulltext_match import ( + FulltextMatchExpression, + ) + dialect = PostgresDialect() + expr = FulltextMatchExpression(dialect, ["col"], "a & b", mode="BOOLEAN") + sql, params = dialect.format_fulltext_match(expr) assert "to_tsvector" in sql assert "to_tsquery" in sql assert "plainto_tsquery" not in sql assert params == ("a & b",) def test_format_fulltext_match_phrase(self): - sql, params = PostgresDialect().format_fulltext_match(["col"], "cat dog", mode="PHRASE") + from rhosocial.activerecord.backend.expression.statements.fulltext_match import ( + FulltextMatchExpression, + ) + dialect = PostgresDialect() + expr = FulltextMatchExpression(dialect, ["col"], "cat dog", mode="PHRASE") + sql, params = dialect.format_fulltext_match(expr) assert "phraseto_tsquery" in sql assert "plainto_tsquery" not in sql assert params == ("cat dog",) def test_format_fulltext_match_unknown_mode_uses_plain(self): - sql, params = PostgresDialect().format_fulltext_match(["col"], "search", mode="CUSTOM") + from rhosocial.activerecord.backend.expression.statements.fulltext_match import ( + FulltextMatchExpression, + ) + dialect = PostgresDialect() + expr = FulltextMatchExpression(dialect, ["col"], "search", mode="CUSTOM") + sql, params = dialect.format_fulltext_match(expr) assert "plainto_tsquery" in sql assert params == ("search",) def test_format_fulltext_match_multi_column(self): - sql, params = PostgresDialect().format_fulltext_match(["a", "b"], "search") + from rhosocial.activerecord.backend.expression.statements.fulltext_match import ( + FulltextMatchExpression, + ) + dialect = PostgresDialect() + expr = FulltextMatchExpression(dialect, ["a", "b"], "search") + sql, params = dialect.format_fulltext_match(expr) assert " || " in sql assert "to_tsvector" in sql assert params == ("search",) From f0245a2af044d495c16378fb62e1cb337a3ccb6e Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 15:51:14 +0800 Subject: [PATCH 16/22] feat(identifier): add reserved words and opt-out identifier quoting - Add backend reserved words and wire format_identifier(need_quote=...) through SQLDialectBase, warning when a reserved word is left unquoted - Rename supports_table_like_syntax to supports_create_table_like - Add identifier quoting tests --- .../backend/impl/postgres/dialect.py | 17 +++++- .../backend/impl/postgres/mixins/table.py | 2 +- .../backend/impl/postgres/protocols/table.py | 2 +- .../backend/impl/postgres/reserved_words.py | 24 ++++++++ .../backend/test_identifier_quoting.py | 57 +++++++++++++++++++ 5 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_identifier_quoting.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index bfda9cfd..e15ccb83 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -191,6 +191,7 @@ ) from .collation import validate_postgres_collation_name +from .reserved_words import POSTGRESQL_RESERVED_WORDS # PostgreSQL-specific imports from .protocols import ( @@ -368,7 +369,7 @@ class PostgresDialect( PostgresExtensionMixin, PostgresMaterializedViewMixin, PostgresAlterColumnModifierMixin, # Before TableMixin/ConstraintMixin to override format_*_action - PostgresTableMixin, # Before TableMixin to override supports_table_like_syntax + PostgresTableMixin, # Before TableMixin to override supports_create_table_like TableMixin, ConstraintMixin, PostgresPartitionMixin, @@ -615,6 +616,7 @@ def __init__(self, version: Optional[Tuple[int, int, int]] = None): """ super().__init__() + self._reserved_words = POSTGRESQL_RESERVED_WORDS if version is not None: self.version = version @@ -859,7 +861,7 @@ def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tu # endregion # region Custom Implementations for PostgreSQL-specific behavior - def format_identifier(self, identifier: str) -> str: + def format_identifier(self, identifier: str, need_quote: bool = True) -> str: """ Format identifier using PostgreSQL's double quote quoting mechanism. @@ -870,6 +872,17 @@ def format_identifier(self, identifier: str) -> str: Quoted identifier with escaped internal quotes """ + if not need_quote: + if self.is_reserved_word(identifier): + import warnings + from rhosocial.activerecord.backend.warnings import IdentifierQuotingWarning + warnings.warn( + f"Identifier '{identifier}' is a reserved word in {self.name} " + f"and may cause SQL errors without quoting.", + IdentifierQuotingWarning, + stacklevel=2, + ) + return identifier # Escape any internal double quotes by doubling them escaped = identifier.replace('"', '""') return f'"{escaped}"' diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py index e58a33b7..258f35fa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py @@ -36,7 +36,7 @@ def supports_table_tablespace(self) -> bool: """TABLESPACE specification is supported in all versions.""" return True - def supports_table_like_syntax(self) -> bool: + def supports_create_table_like(self) -> bool: """PostgreSQL supports CREATE TABLE (LIKE ...) with INCLUDING/EXCLUDING options.""" return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py index 9ff7b6a0..5d65ea23 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py @@ -46,7 +46,7 @@ def supports_unlogged_table(self) -> bool: """ ... - def supports_table_like_syntax(self) -> bool: + def supports_create_table_like(self) -> bool: """Whether CREATE TABLE (LIKE ...) syntax is supported. PostgreSQL supports CREATE TABLE (LIKE other_table) with diff --git a/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py b/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py new file mode 100644 index 00000000..35686517 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py @@ -0,0 +1,24 @@ +# src/rhosocial/activerecord/backend/impl/postgresql/reserved_words.py +""" +PostgreSQL reserved words list. + +Source: PostgreSQL 16 Documentation (https://www.postgresql.org/docs/16/sql-keywords-appendix.html) +""" + +POSTGRESQL_RESERVED_WORDS = frozenset({ + "all", "analyse", "analyze", "and", "any", "array", "as", "asc", + "asymmetric", "both", "case", "cast", "check", "collate", "column", + "constraint", "create", "cross", "current_catalog", "current_date", + "current_role", "current_schema", "current_time", "current_timestamp", + "current_user", "default", "deferrable", "desc", "distinct", "do", + "else", "end", "except", "false", "fetch", "for", "foreign", "from", + "full", "grant", "group", "having", "ilike", "in", "initially", + "inner", "intersect", "into", "is", "isnull", "join", "lateral", + "leading", "left", "like", "limit", "localtime", "localtimestamp", + "natural", "not", "notnull", "null", "offset", "on", "only", "or", + "order", "outer", "overlaps", "placing", "primary", "references", + "returning", "right", "select", "session_user", "similar", "some", + "symmetric", "table", "then", "to", "trailing", "true", "union", + "unique", "user", "using", "variadic", "verbose", "when", "where", + "window", "with", +}) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identifier_quoting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identifier_quoting.py new file mode 100644 index 00000000..3b51b45a --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identifier_quoting.py @@ -0,0 +1,57 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_identifier_quoting.py +""" +Tests for PostgresDialect identifier quoting. + +Verifies that format_identifier correctly applies double-quote quoting, +escapes internal double quotes, respects need_quote, and warns about +reserved words used without quoting. +""" + +import pytest + +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.warnings import IdentifierQuotingWarning + + +class TestPostgreSQLIdentifierQuoting: + """Test PostgresDialect format_identifier and reserved words.""" + + def test_format_identifier_default_double_quote(self): + d = PostgresDialect() + assert d.format_identifier("users") == '"users"' + + def test_format_identifier_need_quote_false(self): + d = PostgresDialect() + assert d.format_identifier("users", need_quote=False) == "users" + + def test_format_identifier_escapes_internal_quotes(self): + d = PostgresDialect() + assert d.format_identifier('my"table') == '"my""table"' + + def test_format_identifier_need_quote_false_no_escaping(self): + d = PostgresDialect() + assert d.format_identifier('my"table', need_quote=False) == 'my"table' + + def test_reserved_words_is_frozenset(self): + d = PostgresDialect() + assert isinstance(d.reserved_words, frozenset) + + def test_is_reserved_word_case_insensitive(self): + d = PostgresDialect() + assert d.is_reserved_word("SELECT") is True + assert d.is_reserved_word("select") is True + + def test_is_reserved_word_non_reserved(self): + d = PostgresDialect() + assert d.is_reserved_word("users") is False + + def test_reserved_word_warning_emitted(self): + d = PostgresDialect() + with pytest.warns(IdentifierQuotingWarning, match="select"): + d.format_identifier("select", need_quote=False) + + def test_balanced_quotes_security(self): + d = PostgresDialect() + for ident in ["users", 'my"table', 'a""b']: + result = d.format_identifier(ident) + assert result.count('"') % 2 == 0, f"Unbalanced quotes: {result}" From c5052261e93358981ad195d7c034ed538042641a Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 18:12:26 +0800 Subject: [PATCH 17/22] refactor(identifier): remove IdentifierMixin and fix format_column with _need_quote --- .../backend/impl/postgres/dialect.py | 14 +++-- .../mixins/types/data_type_formatting.py | 53 ------------------- 2 files changed, 6 insertions(+), 61 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index e15ccb83..ff9e17ce 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -46,7 +46,6 @@ ConstraintMixin, PartitionMixin, # New Mixins - IdentifierMixin, PredicateMixin, ExpressionMixin, DateTimeMixin, @@ -438,7 +437,6 @@ class PostgresDialect( PostgresStoredProcedureMixin, PostgresAdvisoryLockMixin, # New Mixins - IdentifierMixin, PredicateMixin, ExpressionMixin, DateTimeMixin, @@ -908,17 +906,17 @@ def format_column(self, expr) -> Tuple[str, Tuple]: # must use the alias — schema_name is irrelevant in this context. if schema_name and not alias: col_sql = ( - f"{self.format_identifier(schema_name)}." - f"{self.format_identifier(table)}." - f"{self.format_identifier(name)}" + f"{self.format_identifier(schema_name, expr.schema_need_quote)}." + f"{self.format_identifier(table, expr.table_need_quote)}." + f"{self.format_identifier(name, expr.name_need_quote)}" ) else: - col_sql = f"{self.format_identifier(table)}.{self.format_identifier(name)}" + col_sql = f"{self.format_identifier(table, expr.table_need_quote)}.{self.format_identifier(name, expr.name_need_quote)}" else: - col_sql = self.format_identifier(name) + col_sql = self.format_identifier(name, expr.name_need_quote) if alias: - col_sql = f"{col_sql} AS {self.format_identifier(alias)}" + col_sql = f"{col_sql} AS {self.format_identifier(alias, expr.alias_need_quote)}" return col_sql, () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index e6e51d76..aafe9250 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -37,60 +37,7 @@ ) from ...expression.types import ( PostgresArrayType, - PostgresBigSerialType, - PostgresBitType, - PostgresBoxType, - PostgresByteaType, - PostgresCIDType, - PostgresCircleType, - PostgresDateMultirangeType, - PostgresDateRangeType, - PostgresGeographyType, - PostgresGeometryType, - PostgresHstoreType, - PostgresInetType, - PostgresInt4MultirangeType, - PostgresInt4RangeType, - PostgresInt8MultirangeType, - PostgresInt8RangeType, - PostgresJsonPathType, - PostgresLineSegmentType, - PostgresLineType, - PostgresMacAddr8Type, - PostgresMacAddrType, - PostgresMoneyType, - PostgresNumMultirangeType, - PostgresNumRangeType, - PostgresOIDType, - PostgresPathType, - PostgresPgLSNType, - PostgresPointType, - PostgresPolygonType, - PostgresRegClassType, - PostgresRegTypeType, - PostgresSerialType, - PostgresSmallSerialType, - PostgresTIDType, - PostgresTSQueryType, - PostgresTSVectorType, - PostgresTsMultirangeType, - PostgresTsRangeType, - PostgresTsTzMultirangeType, - PostgresTsTzRangeType, - PostgresUUIDType, - PostgresVarBitType, - PostgresVectorType, - PostgresHalfvecType, - PostgresSparsevecType, - PostgresXID8Type, - PostgresXIDType, - PostgresXMLType, - PostgresCidrType, PostgresCharacterVaryingType, - PostgresCitextType, - PostgresCubeType, - PostgresLtreeType, - PostgresRasterType, ) if TYPE_CHECKING: From d7d78ce0e161250cfa4df23b062e18d1fcc7b8f3 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 20:42:04 +0800 Subject: [PATCH 18/22] refactor(postgres): remove 11 duplicate dialect methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete format_identifier, format_commit/rollback_transaction, format_savepoint/release_savepoint, format_drop_view_statement, format_drop_materialized_view_statement, supports_generated_columns, format_truncate_statement, format_refresh_materialized_view_statement, format_explain_statement — all equivalent to implementations provided by SQLDialectBase, TransactionControlMixin, ViewMixin, TruncateMixin, PostgresFeaturesMixin, and ExplainMixin respectively. dialect.py: 1877 → 1866 (-11) --- .../backend/impl/postgres/dialect.py | 220 ------------------ 1 file changed, 220 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index ff9e17ce..4e16e767 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -690,45 +690,6 @@ def validate_collation_name(self, expr: "CollateExpression") -> str: return f"{self.format_identifier(str(schema))}.{self.format_identifier(expr.collation_name)}" return self.format_identifier(expr.collation_name) - def format_explain_statement(self, explain_expr: "ExplainExpression") -> tuple: - """Build the PostgreSQL EXPLAIN SQL string and return (sql, params). - - PostgreSQL syntax: ``EXPLAIN [ ( option [, ...] ) ] statement`` - - Supported options: - - ``ANALYZE`` - - ``FORMAT { TEXT | XML | JSON | YAML }`` - - ``QUERY PLAN`` type — silently omitted (plain EXPLAIN is equivalent). - - Args: - explain_expr: ExplainExpression instance - - Returns: - Tuple of (SQL string, params tuple) - - """ - from rhosocial.activerecord.backend.expression.statements import ExplainType - - statement_sql, statement_params = explain_expr.statement.to_sql() - options = explain_expr.options - if options is None: - return f"EXPLAIN {statement_sql}", statement_params - - opts: list = [] - - if options.analyze: - opts.append("ANALYZE") - - if options.format is not None: - fmt_name = options.format.name if hasattr(options.format, "name") else str(options.format) - opts.append(f"FORMAT {fmt_name.upper()}") - elif options.type is not None and options.type == ExplainType.QUERY_PLAN: - # PostgreSQL has no QUERY PLAN keyword; plain EXPLAIN is equivalent - pass - - if opts: - return "EXPLAIN (" + ", ".join(opts) + ") " + statement_sql, statement_params - return f"EXPLAIN {statement_sql}", statement_params def supports_for_update(self) -> bool: """Whether FOR UPDATE clause is supported in SELECT statements. @@ -751,12 +712,6 @@ def supports_auto_increment(self) -> bool: """ return True - def supports_generated_columns(self) -> bool: - """Whether generated (computed) columns are supported. - - PostgreSQL supports STORED generated columns since version 12. - """ - return self.version >= (12, 0, 0) def supports_stored_generated_columns(self) -> bool: """Whether STORED generated columns are supported. @@ -831,59 +786,10 @@ def format_ilike_expression(self, column: Any, pattern: str, negate: bool = Fals # region Truncate Support - def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tuple]: - """Format TRUNCATE statement for PostgreSQL. - - - ``expr.table_name`` — target table. - - ``expr.restart_identity`` — add ``RESTART IDENTITY`` (PG 8.4+). - - ``expr.cascade`` — add ``CASCADE``. - - Args: - expr: TruncateExpression instance - - Returns: - Tuple of (SQL string, empty params tuple) - - """ - parts = ["TRUNCATE TABLE"] - parts.append(self.format_identifier(expr.table_name)) - - if expr.restart_identity and self.supports_truncate_restart_identity(): - parts.append("RESTART IDENTITY") - - if expr.cascade: - parts.append("CASCADE") - - return " ".join(parts), () # endregion # region Custom Implementations for PostgreSQL-specific behavior - def format_identifier(self, identifier: str, need_quote: bool = True) -> str: - """ - Format identifier using PostgreSQL's double quote quoting mechanism. - - Args: - identifier: Raw identifier string - - Returns: - Quoted identifier with escaped internal quotes - - """ - if not need_quote: - if self.is_reserved_word(identifier): - import warnings - from rhosocial.activerecord.backend.warnings import IdentifierQuotingWarning - warnings.warn( - f"Identifier '{identifier}' is a reserved word in {self.name} " - f"and may cause SQL errors without quoting.", - IdentifierQuotingWarning, - stacklevel=2, - ) - return identifier - # Escape any internal double quotes by doubling them - escaped = identifier.replace('"', '""') - return f'"{escaped}"' def format_column(self, expr) -> Tuple[str, Tuple]: """Format column reference for PostgreSQL. @@ -1034,27 +940,6 @@ def format_create_view_statement(self, expr: "CreateViewExpression") -> Tuple[st return " ".join(parts), query_params - def format_drop_view_statement(self, expr: "DropViewExpression") -> Tuple[str, tuple]: - """Format DROP VIEW statement for PostgreSQL. - - - ``expr.if_exists`` — add ``IF EXISTS``. - - ``expr.view_name`` — view name (identifier). - - ``expr.cascade`` — add ``CASCADE``. - - Args: - expr: DropViewExpression instance - - Returns: - Tuple of (SQL string, empty params tuple) - - """ - parts = ["DROP VIEW"] - if expr.if_exists: - parts.append("IF EXISTS") - parts.append(self.format_identifier(expr.view_name)) - if expr.cascade: - parts.append("CASCADE") - return " ".join(parts), () def format_create_materialized_view_statement(self, expr: "CreateMaterializedViewExpression") -> Tuple[str, tuple]: """Format CREATE MATERIALIZED VIEW statement for PostgreSQL. @@ -1099,51 +984,7 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie return " ".join(parts), query_params - def format_drop_materialized_view_statement(self, expr: "DropMaterializedViewExpression") -> Tuple[str, tuple]: - """Format DROP MATERIALIZED VIEW statement for PostgreSQL. - - - ``expr.if_exists`` — add ``IF EXISTS``. - - ``expr.view_name`` — view name (identifier). - - ``expr.cascade`` — add ``CASCADE``. - Args: - expr: DropMaterializedViewExpression instance - - Returns: - Tuple of (SQL string, empty params tuple) - - """ - parts = ["DROP MATERIALIZED VIEW"] - if expr.if_exists: - parts.append("IF EXISTS") - parts.append(self.format_identifier(expr.view_name)) - if expr.cascade: - parts.append("CASCADE") - return " ".join(parts), () - - def format_refresh_materialized_view_statement( - self, expr: "RefreshMaterializedViewExpression" - ) -> Tuple[str, tuple]: - """Format REFRESH MATERIALIZED VIEW statement for PostgreSQL. - - - ``expr.concurrent`` — add ``CONCURRENTLY`` (PG 9.4+). - - ``expr.view_name`` — view name (identifier). - - ``expr.with_data`` — ``WITH DATA`` / ``WITH NO DATA``. - - Args: - expr: RefreshMaterializedViewExpression instance - - Returns: - Tuple of (SQL string, empty params tuple) - - """ - parts = ["REFRESH MATERIALIZED VIEW"] - if expr.concurrent and self.supports_materialized_view_concurrent_refresh(): - parts.append("CONCURRENTLY") - parts.append(self.format_identifier(expr.view_name)) - if expr.with_data is not None: - parts.append("WITH DATA" if expr.with_data else "WITH NO DATA") - return " ".join(parts), () # endregion @@ -1765,70 +1606,9 @@ def format_begin_transaction( return " ".join(parts), () - def format_commit_transaction( - self, expr: "CommitTransactionExpression" - ) -> Tuple[str, tuple]: - """Format COMMIT TRANSACTION statement for PostgreSQL. - - Always returns ``COMMIT``; ``expr`` is ignored. - - Args: - expr: CommitTransactionExpression instance (unused) - - Returns: - Tuple of ("COMMIT", ()) - - """ - return "COMMIT", () - - def format_rollback_transaction( - self, expr: "RollbackTransactionExpression" - ) -> Tuple[str, tuple]: - """Format ROLLBACK TRANSACTION statement for PostgreSQL. - - Supports ROLLBACK [ TO SAVEPOINT savepoint_name ]. - """ - params = expr.get_params() - savepoint = params.get("savepoint") - if savepoint: - return f"ROLLBACK TO SAVEPOINT {self.format_identifier(savepoint)}", () - return "ROLLBACK", () - - def format_savepoint( - self, expr: "SavepointExpression" - ) -> Tuple[str, tuple]: - """Format SAVEPOINT statement for PostgreSQL. - - ``expr.get_params()["name"]`` — savepoint name (identifier). - - Args: - expr: SavepointExpression instance - - Returns: - Tuple of (SQL string, empty params tuple) - - """ - params = expr.get_params() - name = params.get("name", "") - return f"SAVEPOINT {self.format_identifier(name)}", () - - def format_release_savepoint( - self, expr: "ReleaseSavepointExpression" - ) -> Tuple[str, tuple]: - """Format RELEASE SAVEPOINT statement for PostgreSQL. - - ``expr.get_params()["name"]`` — savepoint name (identifier). - Args: - expr: ReleaseSavepointExpression instance - Returns: - Tuple of (SQL string, empty params tuple) - """ - params = expr.get_params() - name = params.get("name", "") - return f"RELEASE SAVEPOINT {self.format_identifier(name)}", () def format_set_transaction( self, expr: "SetTransactionExpression" From dcd433e7e23b80fb75443f63877f8e8d64a8f5d9 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 21:33:04 +0800 Subject: [PATCH 19/22] fix(mixins): restore Postgres EXPLAIN and TRUNCATE formatters The dialect methods were removed without moving the behavior into PostgresExplainMixin/PostgresTruncateMixin, so the generic core formatters won: EXPLAIN options rendered invalid SQL (COSTS/FORMAT) and TRUNCATE ignored the 8.4 restart-identity gate. Restore both. --- .../backend/impl/postgres/mixins/explain.py | 33 +++++++++++++++++++ .../backend/impl/postgres/mixins/truncate.py | 20 +++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/explain.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/explain.py index 65236cad..3f30f887 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/explain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/explain.py @@ -15,3 +15,36 @@ def supports_explain_format(self, format_type: str) -> bool: format_type_upper = format_type.upper() supported_formats = ["TEXT", "XML", "JSON", "YAML"] return format_type_upper in supported_formats + + def format_explain_statement(self, explain_expr) -> tuple: + """Build the PostgreSQL EXPLAIN SQL string and return (sql, params). + + PostgreSQL syntax: ``EXPLAIN [ ( option [, ...] ) ] statement`` + + Supported options: + - ``ANALYZE`` + - ``FORMAT { TEXT | XML | JSON | YAML }`` + - ``QUERY PLAN`` type — silently omitted (plain EXPLAIN is equivalent). + """ + from rhosocial.activerecord.backend.expression.statements import ExplainType + + statement_sql, statement_params = explain_expr.statement.to_sql() + options = explain_expr.options + if options is None: + return f"EXPLAIN {statement_sql}", statement_params + + opts: list = [] + + if options.analyze: + opts.append("ANALYZE") + + if options.format is not None: + fmt_name = options.format.name if hasattr(options.format, "name") else str(options.format) + opts.append(f"FORMAT {fmt_name.upper()}") + elif options.type is not None and options.type == ExplainType.QUERY_PLAN: + # PostgreSQL has no QUERY PLAN keyword; plain EXPLAIN is equivalent + pass + + if opts: + return "EXPLAIN (" + ", ".join(opts) + ") " + statement_sql, statement_params + return f"EXPLAIN {statement_sql}", statement_params diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py index 3e260464..d8394b9d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py @@ -1,6 +1,8 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py """PostgreSQL truncate feature support implementation.""" +from typing import Tuple + class PostgresTruncateMixin: """PostgreSQL truncate override implementation. @@ -13,3 +15,21 @@ def supports_truncate_restart_identity(self) -> bool: def supports_truncate_cascade(self) -> bool: return True + + def format_truncate_statement(self, expr) -> Tuple[str, tuple]: + """Format TRUNCATE statement for PostgreSQL. + + - ``expr.table_name`` — target table. + - ``expr.restart_identity`` — add ``RESTART IDENTITY`` (PG 8.4+). + - ``expr.cascade`` — add ``CASCADE``. + """ + parts = ["TRUNCATE TABLE"] + parts.append(self.format_identifier(expr.table_name)) + + if expr.restart_identity and self.supports_truncate_restart_identity(): + parts.append("RESTART IDENTITY") + + if expr.cascade: + parts.append("CASCADE") + + return " ".join(parts), () From 4f11a48cadd8c33c1efac650fe7afc8572d2f70b Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 13 Sep 2026 21:52:17 +0800 Subject: [PATCH 20/22] fix(protocols): declare Postgres EXPLAIN/TRUNCATE formatters The reverse-coverage conformance test requires the mixin formatters to be declared on PostgresExplainSupport/PostgresTruncateSupport. --- .../activerecord/backend/impl/postgres/protocols/explain.py | 2 ++ .../activerecord/backend/impl/postgres/protocols/truncate.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/explain.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/explain.py index 470f9a55..ec2752c6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/explain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/explain.py @@ -11,3 +11,5 @@ class PostgresExplainSupport(Protocol): def supports_explain_analyze(self)-> bool: ... def supports_explain_format(self, format_type: str)-> bool: ... + + def format_explain_statement(self, explain_expr) -> tuple: ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py index ee45d091..ae44b85c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py @@ -11,3 +11,5 @@ class PostgresTruncateSupport(Protocol): def supports_truncate_restart_identity(self)-> bool: ... def supports_truncate_cascade(self)-> bool: ... + + def format_truncate_statement(self, expr) -> tuple: ... From 391712eaefe029c70e2a93387ca036b4147701dc Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 14 Sep 2026 00:29:55 +0800 Subject: [PATCH 21/22] refactor(postgres): split dialect methods into feature mixins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move 30 methods from dialect.py into 17 mixin files: - datetime.py, collation.py, dql.py, json.py, generated_column.py, expression.py, function.py (new) - Extended: ilike.py, upsert.py, view.py, materialized_view.py, ddl/column.py, ddl/index.py, table.py, ddl/constraint.py, dml/locking.py, transaction.py dialect.py: 1657 → 645 lines (retains __init__, _validate_data_type, get_parameter_placeholder, get_server_version, create_schema_differ). --- .../backend/impl/postgres/dialect.py | 1056 +---------------- .../backend/impl/postgres/mixins/__init__.py | 12 + .../backend/impl/postgres/mixins/collation.py | 15 + .../backend/impl/postgres/mixins/datetime.py | 36 + .../impl/postgres/mixins/ddl/column.py | 35 + .../impl/postgres/mixins/ddl/constraint.py | 135 ++- .../backend/impl/postgres/mixins/ddl/index.py | 30 + .../impl/postgres/mixins/dml/locking.py | 28 + .../backend/impl/postgres/mixins/dql.py | 16 + .../impl/postgres/mixins/expression.py | 53 + .../backend/impl/postgres/mixins/function.py | 171 +++ .../impl/postgres/mixins/generated_column.py | 27 + .../backend/impl/postgres/mixins/ilike.py | 24 + .../backend/impl/postgres/mixins/json.py | 10 + .../impl/postgres/mixins/materialized_view.py | 43 + .../backend/impl/postgres/mixins/table.py | 82 +- .../impl/postgres/mixins/transaction.py | 88 ++ .../backend/impl/postgres/mixins/upsert.py | 71 ++ .../backend/impl/postgres/mixins/view.py | 41 + 19 files changed, 925 insertions(+), 1048 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/datetime.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/dql.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/function.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/json.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 4e16e767..3a8ca700 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -6,14 +6,13 @@ based on the PostgreSQL version provided at initialization. """ -from typing import Any, Dict, Tuple, Optional, TYPE_CHECKING +from typing import Tuple, Optional, TYPE_CHECKING if TYPE_CHECKING: from rhosocial.activerecord.backend.expression.collation import CollateExpression from .function_versions import FunctionSupportInfo, FunctionVersionRequirement from rhosocial.activerecord.backend.dialect.base import SQLDialectBase -from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.dialect.mixins import ( SQLXMLMixin, CollationMixin, @@ -187,9 +186,15 @@ # Introspection capability mixin PostgresIntrospectionCapabilityMixin, PostgresAlterColumnModifierMixin, + # Newly extracted mixins + PostgresDateTimeMixin, + PostgresDQLMixin, + PostgresJSONMixin, + PostgresGeneratedColumnMixin, + PostgresExpressionMixin, + PostgresFunctionMixin, ) -from .collation import validate_postgres_collation_name from .reserved_words import POSTGRESQL_RESERVED_WORDS # PostgreSQL-specific imports @@ -285,32 +290,16 @@ PostgresLockingSupport, ) -if TYPE_CHECKING: - from rhosocial.activerecord.backend.expression.statements import ( - CreateTableExpression, - CreateViewExpression, - DropViewExpression, - TruncateExpression, - CreateMaterializedViewExpression, - DropMaterializedViewExpression, - RefreshMaterializedViewExpression, - ExplainExpression, - AddTableConstraint, - AddIndex, - DropIndex as DropIndexAction, # noqa: F401 - TableConstraint, - ) - from rhosocial.activerecord.backend.expression.transaction import ( - BeginTransactionExpression, - CommitTransactionExpression, - RollbackTransactionExpression, - SavepointExpression, - ReleaseSavepointExpression, - SetTransactionExpression, - ) class PostgresDialect( SQLDialectBase, + # PG-specific mixins (before global mixins to override) + PostgresDateTimeMixin, + PostgresDQLMixin, + PostgresJSONMixin, + PostgresGeneratedColumnMixin, + PostgresExpressionMixin, + PostgresFunctionMixin, # Per-feature mixins PostgresCTEMixin, PostgresWindowMixin, @@ -602,6 +591,13 @@ class PostgresDialect( Use introspect_and_adapt() to detect installed extensions automatically. """ + # PostgreSQL function version support: function_name -> (min_version, max_version) + # Function version requirements are defined in function_versions.py, + # categorized by topic (JSON Path, Range, hstore, pgvector, PostGIS, etc.) + # and assembled into POSTGRES_FUNCTION_VERSIONS. + from .function_versions import POSTGRES_FUNCTION_VERSIONS as _FV + _POSTGRES_FUNCTION_VERSIONS = _FV + def __init__(self, version: Optional[Tuple[int, int, int]] = None): """ Initialize PostgreSQL dialect with specific version. @@ -647,1011 +643,3 @@ def create_schema_differ(self): ) return PostgresSchemaDiffer() - - def format_datetime_diff_expression(self, expr: "Any") -> Tuple[str, Tuple]: - start_sql, start_params = expr.start.to_sql() - end_sql, end_params = expr.end.to_sql() - seconds_sql = f"EXTRACT(EPOCH FROM ({end_sql} - {start_sql}))" - factors = { - "second": "1", - "minute": "60", - "hour": "3600", - "day": "86400", - "week": "604800", - } - if expr.unit.value in factors: - sql = f"({seconds_sql} / {factors[expr.unit.value]})" - params = end_params + start_params - elif expr.unit.value == "month": - sql = ( - f"((EXTRACT(YEAR FROM {end_sql}) - " - f"EXTRACT(YEAR FROM {start_sql})) * 12 + " - f"(EXTRACT(MONTH FROM {end_sql}) - " - f"EXTRACT(MONTH FROM {start_sql})))" - ) - params = end_params + start_params + end_params + start_params - else: - sql = ( - f"(EXTRACT(YEAR FROM {end_sql}) - " - f"EXTRACT(YEAR FROM {start_sql}))" - ) - params = end_params + start_params - return self.apply_alias(sql, params, expr) - - def validate_collation_name(self, expr: "CollateExpression") -> str: - """Validate PostgreSQL collation names and return their SQL representation.""" - schema = expr.collation_options.get("schema") - unsupported = set(expr.collation_options) - {"schema"} - if unsupported: - options = ", ".join(sorted(unsupported)) - raise UnsupportedFeatureError(self.name, f"COLLATE options: {options}") - validate_postgres_collation_name(expr.collation_name, getattr(self, "version", None)) - if schema is not None: - return f"{self.format_identifier(str(schema))}.{self.format_identifier(expr.collation_name)}" - return self.format_identifier(expr.collation_name) - - - def supports_for_update(self) -> bool: - """Whether FOR UPDATE clause is supported in SELECT statements. - - PostgreSQL supports FOR UPDATE since early versions. The clause locks - selected rows preventing other transactions from modifying them. - PostgreSQL also supports FOR UPDATE OF, FOR UPDATE NOWAIT, and - FOR UPDATE SKIP LOCKED (since 9.5). - """ - return True - - def supports_json_arrow_operators(self) -> bool: - """PostgreSQL supports -> and ->> operators for JSON/JSONB access.""" - return True - - def supports_auto_increment(self) -> bool: - """Whether AUTO_INCREMENT/IDENTITY column attributes are supported. - - PostgreSQL supports SERIAL and GENERATED ... AS IDENTITY. - """ - return True - - - def supports_stored_generated_columns(self) -> bool: - """Whether STORED generated columns are supported. - - PostgreSQL generated columns are always STORED. - """ - return self.supports_generated_columns() - - def supports_virtual_generated_columns(self) -> bool: - """Whether VIRTUAL generated columns are supported. - - PostgreSQL has no VIRTUAL generated columns. - """ - return False - - def supports_lock_strength(self, strength) -> bool: - """ - Check if a specific lock strength is supported. - - PostgreSQL lock strength support by version: - - FOR UPDATE: All versions - - FOR NO KEY UPDATE: PostgreSQL 9.0+ - - FOR SHARE: PostgreSQL 9.0+ - - FOR KEY SHARE: PostgreSQL 9.3+ - - Args: - strength: The LockStrength enum value to check - - Returns: - True if the lock strength is supported, False otherwise - - """ - from rhosocial.activerecord.backend.impl.postgres.expression.locking import LockStrength - - if strength == LockStrength.UPDATE: - return True # All PostgreSQL versions support FOR UPDATE - elif strength == LockStrength.NO_KEY_UPDATE: - return self.version >= (9, 0, 0) - elif strength == LockStrength.SHARE: - return self.version >= (9, 0, 0) - elif strength == LockStrength.KEY_SHARE: - return self.version >= (9, 3, 0) - return False - - # region ILIKE Support - - def format_ilike_expression(self, column: Any, pattern: str, negate: bool = False) -> Tuple[str, Tuple]: - """Format ILIKE expression for PostgreSQL. - - Args: - column: Column name string or expression with ``to_sql()``. - pattern: Right-hand-side pattern (replaced with ``%s`` placeholder). - negate: If ``True``, produces ``NOT ILIKE`` instead of ``ILIKE``. - - Returns: - Tuple of (SQL string, (pattern,) params tuple) - - """ - if isinstance(column, str): - col_sql = self.format_identifier(column) - else: - col_sql, col_params = column.to_sql() if hasattr(column, "to_sql") else (str(column), ()) - - if negate: - sql = f"{col_sql} NOT ILIKE %s" - else: - sql = f"{col_sql} ILIKE %s" - - return sql, (pattern,) - - # endregion - - # region Truncate Support - - - # endregion - - # region Custom Implementations for PostgreSQL-specific behavior - - def format_column(self, expr) -> Tuple[str, Tuple]: - """Format column reference for PostgreSQL. - - PostgreSQL rules for column references: - - When the table has an alias (used in FROM/JOIN), column references - must use the alias, not the schema-qualified name. For example, - when FROM clause has ``public.users AS "users"``, the column - reference must be ``"users"."id"``, not ``"public"."users"."id"``. - - When no alias is present but schema is specified, use the - full three-segment form: ``"schema"."table"."column"``. - - Otherwise use the standard two-segment or single-segment form. - """ - name = expr.name - table = expr.table - alias = expr.alias - schema_name = expr.schema_name - if table: - # In PostgreSQL, when a table has an alias, column references - # must use the alias — schema_name is irrelevant in this context. - if schema_name and not alias: - col_sql = ( - f"{self.format_identifier(schema_name, expr.schema_need_quote)}." - f"{self.format_identifier(table, expr.table_need_quote)}." - f"{self.format_identifier(name, expr.name_need_quote)}" - ) - else: - col_sql = f"{self.format_identifier(table, expr.table_need_quote)}.{self.format_identifier(name, expr.name_need_quote)}" - else: - col_sql = self.format_identifier(name, expr.name_need_quote) - - if alias: - col_sql = f"{col_sql} AS {self.format_identifier(alias, expr.alias_need_quote)}" - - return col_sql, () - - def format_on_conflict_clause(self, expr) -> Tuple[str, tuple]: - """Format ON CONFLICT clause for PostgreSQL. - - Overrides the base implementation to handle EXCLUDED pseudo-table - references without quoting, as EXCLUDED is a special PostgreSQL - keyword in ON CONFLICT context and must not be double-quoted. - - - ``expr.conflict_target`` — optional list of column names / expressions. - - ``expr.do_nothing`` — ``DO NOTHING``. - - ``expr.update_assignments`` — dict of ``{column: expression}`` for ``DO UPDATE SET``. - - ``expr.update_where`` — optional WHERE predicate for the update. - - Args: - expr: OnConflictClause expression instance - - Returns: - Tuple of (SQL string, params tuple) - - """ - from rhosocial.activerecord.backend.expression import bases - from rhosocial.activerecord.backend.expression.core import Column - - all_params = [] - parts = ["ON CONFLICT"] - - # Add conflict target if specified - if expr.conflict_target: - target_parts = [] - for target in expr.conflict_target: - if isinstance(target, str): - target_parts.append(self.format_identifier(target)) - elif hasattr(target, 'to_sql'): - target_sql, target_params = target.to_sql() - target_parts.append(target_sql) - all_params.extend(target_params) - else: - target_parts.append(self.format_identifier(str(target))) - if target_parts: - parts.append(f"({', '.join(target_parts)})") - - # Add DO NOTHING or DO UPDATE - if expr.do_nothing: - parts.append("DO NOTHING") - elif expr.update_assignments: - update_parts = [] - for col, expr_val in expr.update_assignments.items(): - if isinstance(expr_val, Column) and getattr(expr_val, 'table', None) == 'EXCLUDED': - # EXCLUDED is a special pseudo-table in PostgreSQL ON CONFLICT. - # It must NOT be double-quoted, only the column name should be quoted. - val_sql = f'EXCLUDED.{self.format_identifier(expr_val.name)}' - update_parts.append(f"{self.format_identifier(col)} = {val_sql}") - elif isinstance(expr_val, bases.BaseExpression): - val_sql, val_params = expr_val.to_sql() - update_parts.append(f"{self.format_identifier(col)} = {val_sql}") - all_params.extend(val_params) - else: - update_parts.append(f"{self.format_identifier(col)} = {self.get_parameter_placeholder()}") - all_params.append(expr_val) - - parts.append(f"DO UPDATE SET {', '.join(update_parts)}") - - # Add WHERE clause if specified - if expr.update_where: - where_sql, where_params = expr.update_where.to_sql() - parts.append(f"WHERE {where_sql}") - all_params.extend(where_params) - else: - parts.append("DO NOTHING") - - return " ".join(parts), tuple(all_params) - - # region View Support - - def format_create_view_statement(self, expr: "CreateViewExpression") -> Tuple[str, tuple]: - """Format CREATE VIEW statement for PostgreSQL. - - - ``expr.temporary`` — add ``TEMPORARY``. - - ``expr.replace`` — add ``OR REPLACE``. - - ``expr.view_name`` — view name (identifier). - - ``expr.column_aliases`` — optional list of column aliases. - - ``expr.query`` — source SELECT expression. - - ``expr.options.check_option`` — ``WITH {LOCAL|CASCADED} CHECK OPTION``. - - Args: - expr: CreateViewExpression instance - - Returns: - Tuple of (SQL string, params tuple) - - """ - parts = ["CREATE"] - - if expr.temporary: - parts.append("TEMPORARY") - - if expr.replace: - parts.append("OR REPLACE") - - parts.append("VIEW") - parts.append(self.format_identifier(expr.view_name)) - - if expr.column_aliases: - cols = ", ".join(self.format_identifier(c) for c in expr.column_aliases) - parts.append(f"({cols})") - - query_sql, query_params = expr.query.to_sql() - parts.append(f"AS {query_sql}") - - if expr.options and expr.options.check_option: - check_option = expr.options.check_option.value - parts.append(f"WITH {check_option} CHECK OPTION") - - return " ".join(parts), query_params - - - def format_create_materialized_view_statement(self, expr: "CreateMaterializedViewExpression") -> Tuple[str, tuple]: - """Format CREATE MATERIALIZED VIEW statement for PostgreSQL. - - - ``expr.view_name`` — view name (identifier). - - ``expr.column_aliases`` — optional list of column aliases. - - ``expr.tablespace`` — optional tablespace. - - ``expr.storage_options`` — optional dict of storage parameters (``WITH (… )``). - - ``expr.query`` — source SELECT expression. - - ``expr.with_data`` — ``WITH DATA`` / ``WITH NO DATA``. - - Args: - expr: CreateMaterializedViewExpression instance - - Returns: - Tuple of (SQL string, params tuple) - - """ - parts = ["CREATE MATERIALIZED VIEW"] - parts.append(self.format_identifier(expr.view_name)) - - if expr.column_aliases: - cols = ", ".join(self.format_identifier(c) for c in expr.column_aliases) - parts.append(f"({cols})") - - if expr.tablespace and self.supports_materialized_view_tablespace(): - parts.append(f"TABLESPACE {self.format_identifier(expr.tablespace)}") - - if expr.storage_options and self.supports_materialized_view_storage_options(): - storage_parts = [] - for key, value in expr.storage_options.items(): - storage_parts.append(f"{key.upper()} = {value}") - parts.append(f"WITH ({', '.join(storage_parts)})") - - query_sql, query_params = expr.query.to_sql() - parts.append(f"AS {query_sql}") - - if expr.with_data: - parts.append("WITH DATA") - else: - parts.append("WITH NO DATA") - - return " ".join(parts), query_params - - - - # endregion - - # region Index Support - - def format_add_index_action(self, action: "AddIndex") -> Tuple[str, tuple]: - """PostgreSQL does not support ALTER TABLE ADD INDEX. - - Raises UnsupportedFeatureError — use CREATE INDEX instead. - """ - from rhosocial.activerecord.backend.dialect.exceptions import ( - UnsupportedFeatureError, - ) - - raise UnsupportedFeatureError( - self.name, - "ALTER TABLE ADD INDEX", - suggestion="Use CREATE INDEX to create an index on the table.", - ) - - def format_drop_index_action(self, action: "DropIndex") -> Tuple[str, tuple]: # noqa: F821 - """PostgreSQL does not support ALTER TABLE DROP INDEX. - - Raises UnsupportedFeatureError — use DROP INDEX statement instead. - """ - from rhosocial.activerecord.backend.dialect.exceptions import ( - UnsupportedFeatureError, - ) - - raise UnsupportedFeatureError( - self.name, - "ALTER TABLE DROP INDEX", - suggestion="Use DROP INDEX statement to remove an index.", - ) - - # endregion - - # region Table Support - - # Constraint capability overrides - - def format_create_table_statement(self, expr: "CreateTableExpression") -> Tuple[str, tuple]: - """ - Format CREATE TABLE statement for PostgreSQL. - - This formatter handles PostgreSQL-specific CREATE TABLE behavior before - delegating ordinary table generation to the base dialect: - - * PostgreSQL LIKE syntax with INCLUDING/EXCLUDING options. - * Declarative partitioning validation for ``partition`` expressions. - - PostgreSQL LIKE syntax supports INCLUDING/EXCLUDING options to control what - gets copied: DEFAULTS, CONSTRAINTS, INDEXES, IDENTITY, GENERATED, ALL, - COMMENTS, STORAGE, COMPRESSION. - - The behavior is controlled by the `dialect_options` parameter in - CreateTableExpression: - - 1. When `dialect_options` contains 'like_table' key: - - LIKE syntax takes highest priority - - All other parameters (columns, indexes, constraints, etc.) are IGNORED - - Only temporary and if_not_exists flags are considered - - Additional 'like_options' key controls INCLUDING/EXCLUDING behavior - - 2. When `dialect_options` does NOT contain 'like_table': - - Applies PostgreSQL partition validation when `partition` is present - - Falls back to base class implementation for SQL generation - - Standard CREATE TABLE and CREATE TABLE ... PARTITION BY statements - are generated by the base formatter - - Partition validation follows PostgreSQL declarative partitioning rules: - - - RANGE and LIST partitioned tables require PostgreSQL 10+. - - HASH partitioned tables require PostgreSQL 11+. - - MySQL-specific partition methods such as KEY and RANGE COLUMNS are rejected. - - The API is `partition=PartitionClause(...)`; PartitionClause is the - standard expression type for partition specification. - - The 'like_options' key supports two formats: - - a) Dictionary format (recommended): - { - 'including': ['DEFAULTS', 'CONSTRAINTS', 'INDEXES'], - 'excluding': ['COMMENTS'] - } - - b) List format (for backwards compatibility): - ['DEFAULTS', 'CONSTRAINTS'] # Defaults to INCLUDING - or - [('INCLUDING', 'DEFAULTS'), ('EXCLUDING', 'INDEXES')] - - Usage Examples: - # Basic LIKE syntax - CreateTableExpression( - dialect=postgres_dialect, - table_name="users_copy", - columns=[], # Ignored when like_table is present - dialect_options={'like_table': 'users'} - ) - # Generates: CREATE TABLE "users_copy" (LIKE "users") - - # LIKE with INCLUDING options (dictionary format - recommended) - CreateTableExpression( - dialect=postgres_dialect, - table_name="users_copy", - columns=[...], # Will be ignored - dialect_options={ - 'like_table': 'users', - 'like_options': { - 'including': ['DEFAULTS', 'CONSTRAINTS', 'INDEXES'], - 'excluding': ['COMMENTS'] - } - } - ) - # Generates: CREATE TABLE "users_copy" (LIKE "users", INCLUDING DEFAULTS, - # INCLUDING CONSTRAINTS, INCLUDING INDEXES, EXCLUDING COMMENTS) - - # LIKE with schema-qualified source table - CreateTableExpression( - dialect=postgres_dialect, - table_name="users_copy", - columns=[], - dialect_options={'like_table': ('public', 'users')} - ) - # Generates: CREATE TABLE "users_copy" (LIKE "public"."users") - - # LIKE with TEMPORARY and IF NOT EXISTS - CreateTableExpression( - dialect=postgres_dialect, - table_name="temp_users", - columns=[], - temporary=True, - if_not_exists=True, - dialect_options={'like_table': 'users'} - ) - # Generates: CREATE TEMPORARY TABLE IF NOT EXISTS "temp_users" (LIKE "users") - - # LIKE with INCLUDING ALL - CreateTableExpression( - dialect=postgres_dialect, - table_name="users_copy", - columns=[], - dialect_options={ - 'like_table': 'users', - 'like_options': {'including': ['ALL']} - } - ) - # Generates: CREATE TABLE "users_copy" (LIKE "users", INCLUDING ALL) - - # PostgreSQL declarative partitioned table - CreateTableExpression( - dialect=postgres_dialect, - table="events", - columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ], - partition=PartitionClause( - dialect=postgres_dialect, - method="RANGE", - keys=[Column(postgres_dialect, "created_at")], - ), - ) - # Generates: CREATE TABLE "events" (...) PARTITION BY RANGE ("created_at") - # PostgreSQL dialect validates version support before delegating to - # the base formatter. - - Args: - expr: CreateTableExpression instance - - Returns: - Tuple of (SQL string, parameters tuple) - - """ - # Check for LIKE syntax in dialect_options (highest priority) - if "like_table" in expr.dialect_options: - like_table = expr.dialect_options["like_table"] - like_options = expr.dialect_options.get("like_options", []) - - parts = ["CREATE"] - - if expr.temporary: - parts.append("TEMPORARY") - - parts.append("TABLE") - - if expr.if_not_exists: - parts.append("IF NOT EXISTS") - - parts.append(self.format_identifier(expr.table_name)) - - # Build LIKE clause with options - like_parts = [] - - # Handle schema-qualified table name: ('schema', 'table') - if isinstance(like_table, tuple): - schema, table = like_table - like_table_str = f"{self.format_identifier(schema)}.{self.format_identifier(table)}" - else: - like_table_str = self.format_identifier(like_table) - - like_parts.append(f"LIKE {like_table_str}") - - # Add INCLUDING/EXCLUDING options - # Format: dictionary with 'including' and 'excluding' keys - # Example: {'including': ['DEFAULTS', 'CONSTRAINTS'], 'excluding': ['INDEXES']} - if isinstance(like_options, dict): - # Handle dictionary format - including = like_options.get("including", []) - excluding = like_options.get("excluding", []) - - for option in including: - like_parts.append(f"INCLUDING {option.upper()}") - - for option in excluding: - like_parts.append(f"EXCLUDING {option.upper()}") - elif isinstance(like_options, list): - # Handle list format for backwards compatibility - for option in like_options: - if isinstance(option, tuple): - action, feature = option - like_parts.append(f"{action.upper()} {feature.upper()}") - else: - # Default to INCLUDING if just feature name provided - like_parts.append(f"INCLUDING {option.upper()}") - - parts.append(f"({', '.join(like_parts)})") - - return " ".join(parts), () - - if getattr(expr, "partition", None) is not None: - # Validate through the PartitionClause → format_partition_clause chain. - expr.partition.to_sql() - - # Otherwise, delegate to base implementation - return super().format_create_table_statement(expr) - - # endregion - - # region Type Casting Support (PostgreSQL-specific) - def format_cast_expression(self, expr) -> Tuple[str, Tuple]: - """Format type cast expression using PostgreSQL :: syntax. - - PostgreSQL supports both standard CAST(expr AS type) syntax and the - PostgreSQL-specific expr::type syntax. This method uses the more - concise :: syntax which is idiomatic in PostgreSQL. - - ``expr.expression`` renders through its own ``to_sql()``; the cast - target is the node's ``target_type`` construction parameter. - - Args: - expr: :class:`~...expression.core.CastExpression` instance. - - Returns: - Tuple of (SQL string, parameters) - - Example: - >>> Column(dialect, 'price').cast('numeric').to_sql() - # Returns: ('"price"::numeric', ()) - - Note: - For chained type conversions, each ::type is appended: - >>> col.cast('money').cast('numeric').cast('float8') - # Generates: "col"::money::numeric::float8 - - """ - expr_sql, params = expr.expression.to_sql() - sql = f"{expr_sql}::{expr.target_type}" - if expr.alias: - sql = f"{sql} AS {self.format_identifier(expr.alias)}" - return sql, params - - # endregion - - # region Operator Formatting (PostgreSQL-specific) - - def format_binary_operator(self, expr) -> Tuple[str, Tuple]: - """Format binary operator with psycopg placeholder escaping. - - psycopg uses %s as parameter placeholder. When the SQL operator itself - contains % (e.g., pg_trgm similarity operator), it must be escaped as %% - to prevent psycopg from interpreting it as a placeholder prefix. - """ - left_sql, left_params = expr.left.to_sql() - right_sql, right_params = expr.right.to_sql() - op = expr.op - # Escape % in operators for psycopg compatibility - escaped_op = op.replace('%', '%%') if '%' in op else op - sql = f"{left_sql} {escaped_op} {right_sql}" - return sql, left_params + right_params - - # endregion - - # region Constraint DDL Support (PostgreSQL-specific) - - def format_add_table_constraint_action( - self, action: "AddTableConstraint", - ) -> Tuple[str, tuple]: - """Format ADD CONSTRAINT action with PostgreSQL-specific extensions. - - Extends the base class implementation with: - - EXCLUDE constraint support (PG-specific constraint type) - - NOT VALID suffix (PG-specific: skip validation of existing rows) - """ - from rhosocial.activerecord.backend.expression.statements import ( - TableConstraintType, ConstraintValidation, - ) - - # Handle EXCLUDE constraint (PG-specific, not in base class) - if action.constraint.constraint_type == TableConstraintType.EXCLUDE: - parts = [] - exclude_sql, params = self._format_exclude_constraint(action.constraint) - parts.append(exclude_sql) - - # NOT VALID suffix - if action.constraint.dialect_options: - validation = action.constraint.dialect_options.get('validation') - if validation == ConstraintValidation.NOVALIDATE: - parts.append("NOT VALID") - - return f"ADD {' '.join(parts)}", tuple(params) - - # Use base class for standard formatting (includes DEFERRABLE) - sql, params = super().format_add_table_constraint_action(action) - - # PostgreSQL NOT VALID suffix - if action.constraint.dialect_options: - validation = action.constraint.dialect_options.get('validation') - if validation == ConstraintValidation.NOVALIDATE: - sql += " NOT VALID" - - return sql, params - - def _format_exclude_constraint( - self, constraint: "TableConstraint", - ) -> Tuple[str, tuple]: - """Format EXCLUDE constraint (PostgreSQL-specific). - - EXCLUDE constraints use the dialect_options dict to specify: - - 'exclude_elements': List of (expression, operator) tuples - e.g., [('range', '&&')] for EXCLUDE USING gist (range WITH &&) - - 'using': The index access method (default 'gist') - e.g., 'gist', 'btree', 'spgist' - - 'where': Optional predicate for partial exclusion constraints - - Example: - TableConstraint( - constraint_type=TableConstraintType.EXCLUDE, - name='exclude_range_overlap', - dialect_options={ - 'exclude_elements': [('range', '&&')], - 'using': 'gist', - } - ) - # Generates: EXCLUDE USING gist (range WITH &&) - - """ - params: list = [] - - if constraint.name: - parts = ["CONSTRAINT", self.format_identifier(constraint.name)] - else: - parts = [] - - # USING clause - validate index access method. - valid_using = frozenset({"gist", "btree", "spgist", "hash", "gin", "brin"}) - using = constraint.dialect_options.get("using", "gist") if constraint.dialect_options else "gist" - if using not in valid_using: - raise ValueError( - f"Invalid index access method '{using}': must be one of {valid_using}" - ) - parts.append(f"EXCLUDE USING {using}") - - # Elements: (expression, operator) pairs - validate operators. - valid_ops = frozenset({ - "=", "<", "<=", ">", ">=", "<>", - "&&", "@>", "<@", "<<", ">>", "&<", "&>", - "~=", "@@", "?|", "?&", "is", "is not", - }) - exclude_elements = [] - if constraint.dialect_options and "exclude_elements" in constraint.dialect_options: - for expr, op in constraint.dialect_options["exclude_elements"]: - if op not in valid_ops: - raise ValueError( - f"Invalid exclude operator '{op}': must be one of {valid_ops}" - ) - if isinstance(expr, str): - exclude_elements.append(f"{self.format_identifier(expr)} WITH {op}") - else: - expr_sql, expr_params = expr.to_sql() - params.extend(expr_params) - exclude_elements.append(f"{expr_sql} WITH {op}") - - if exclude_elements: - parts.append(f"({', '.join(exclude_elements)})") - - # WHERE clause for partial exclusion constraint - if constraint.dialect_options and 'where' in constraint.dialect_options: - where_expr = constraint.dialect_options['where'] - where_sql, where_params = where_expr.to_sql() - params.extend(where_params) - parts.append(f"WHERE ({where_sql})") - - # DEFERRABLE / NOT DEFERRABLE - if constraint.deferrable is True: - if constraint.initially_deferred is True: - parts.append("DEFERRABLE INITIALLY DEFERRED") - elif constraint.initially_deferred is False: - parts.append("DEFERRABLE INITIALLY IMMEDIATE") - else: - parts.append("DEFERRABLE") - elif constraint.deferrable is False: - parts.append("NOT DEFERRABLE") - - return ' '.join(parts), tuple(params) - - # endregion - - # region Transaction Control Support - - # PostgreSQL function version support: function_name -> (min_version, max_version) - # min_version: minimum supported version (inclusive), None = all versions - # Function version requirements are defined in function_versions.py, - # categorized by topic (JSON Path, Range, hstore, pgvector, PostGIS, etc.) - # and assembled into POSTGRES_FUNCTION_VERSIONS. - from .function_versions import POSTGRES_FUNCTION_VERSIONS as _FV - _POSTGRES_FUNCTION_VERSIONS = _FV - - def supports_functions(self) -> Dict[str, "FunctionSupportInfo"]: - """Return supported SQL functions with detailed support information. - - This method combines: - 1. Core functions from rhosocial.activerecord.backend.expression.functions - 2. PostgreSQL-specific functions from rhosocial.activerecord.backend.impl.postgres.functions - - Each function is mapped to a FunctionSupportInfo indicating: - - Whether the function is supported - - If not, the reason why (PG version, extension status, etc.) - - Returns: - Dict mapping function names to FunctionSupportInfo. - - """ - from .function_versions import FunctionSupportInfo - from rhosocial.activerecord.backend.expression.functions import ( - __all__ as core_functions, - ) - from rhosocial.activerecord.backend.impl.postgres import functions as postgres_functions - - expression_constructors = { - "xmlagg", - "xmlattributes", - "xmlcomment", - "xmlconcat", - "xmlelement", - "xmlexists", - "xmlforest", - "xmlparse", - "xmlpi", - "xmlquery", - "xmlroot", - "xmlserialize", - "xmltable", - } - result: Dict[str, FunctionSupportInfo] = {} - for func_name in core_functions: - if func_name not in expression_constructors: - result[func_name] = self._check_function_support(func_name) - - postgres_funcs = getattr(postgres_functions, "__all__", []) - for func_name in postgres_funcs: - if func_name not in result: - result[func_name] = self._check_function_support(func_name) - - return result - - def _check_function_support(self, func_name: str) -> "FunctionSupportInfo": - """Check function support status and return detailed information. - - Args: - func_name: Name of the function to check - - Returns: - FunctionSupportInfo with support status and reason if unsupported - - """ - from .function_versions import FunctionSupportInfo - - requirement = self._POSTGRES_FUNCTION_VERSIONS.get(func_name) - if requirement is None: - return FunctionSupportInfo(supported=True) - - # Check PostgreSQL server version - if requirement.min_pg_version is not None and self.version < requirement.min_pg_version: - return FunctionSupportInfo(supported=False, reason="pg_version_too_low") - if requirement.max_pg_version is not None and self.version > requirement.max_pg_version: - return FunctionSupportInfo(supported=False, reason="pg_version_too_high") - - # Check extension requirements - if requirement.extension is not None: - if not hasattr(self, "_extensions"): - return FunctionSupportInfo(supported=False, reason="extension_not_probed") - - if requirement.ext_feature is not None: - if not self.check_extension_feature(requirement.extension, requirement.ext_feature): - if not self.is_extension_installed(requirement.extension): - return FunctionSupportInfo(supported=False, reason="extension_not_installed") - return FunctionSupportInfo(supported=False, reason="extension_version_insufficient") - else: - if not self.is_extension_installed(requirement.extension): - return FunctionSupportInfo(supported=False, reason="extension_not_installed") - if requirement.min_ext_version is not None: - installed = self.get_extension_version(requirement.extension) - if installed is None or self._compare_versions(installed, requirement.min_ext_version) < 0: - return FunctionSupportInfo(supported=False, reason="extension_version_insufficient") - - return FunctionSupportInfo(supported=True) - - def _is_postgres_function_supported(self, func_name: str) -> bool: - """Check if a PostgreSQL function is supported based on version and extensions. - - Checks: - 1. PostgreSQL server version (for built-in and version-gated functions) - 2. Extension installation and version (for extension-provided functions) - - For extension functions, requires _extensions to have been populated - via introspect_and_adapt(). If _extensions is not available, - extension functions return False (cannot confirm availability). - - Args: - func_name: Name of the PostgreSQL function - - Returns: - True if supported, False otherwise - - """ - requirement = self._POSTGRES_FUNCTION_VERSIONS.get(func_name) - if requirement is None: - return True # Unregistered functions default to supported - - # Step 1: Check PostgreSQL server version - if requirement.min_pg_version is not None and self.version < requirement.min_pg_version: - return False - if requirement.max_pg_version is not None and self.version > requirement.max_pg_version: - return False - - # Step 2: Check extension requirements - if requirement.extension is not None: - return self._check_extension_requirement(requirement) - - return True - - def _check_extension_requirement( - self, requirement: "FunctionVersionRequirement" - ) -> bool: - """Check extension requirements based on introspect_and_adapt() results. - - Requires _extensions to have been populated via introspect_and_adapt(). - If _extensions is not available, returns False (cannot confirm - extension availability without probing the database). - """ - ext_name = requirement.extension - - # _extensions is populated by introspect_and_adapt() - # If the attribute doesn't exist, introspect_and_adapt() hasn't been called - if not hasattr(self, "_extensions"): - return False - - # Use feature-level check if ext_feature is specified - if requirement.ext_feature is not None: - return self.check_extension_feature(ext_name, requirement.ext_feature) - - # Check extension installed - if not self.is_extension_installed(ext_name): - return False - - # Check extension version - if requirement.min_ext_version is not None: - installed = self.get_extension_version(ext_name) - if installed is None: - return False - return self._compare_versions(installed, requirement.min_ext_version) >= 0 - - return True - - def format_begin_transaction( - self, expr: "BeginTransactionExpression" - ) -> Tuple[str, tuple]: - """Format BEGIN TRANSACTION statement for PostgreSQL. - - PostgreSQL syntax: - BEGIN [ ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE } ] - [ { READ WRITE | READ ONLY } ] - [ { NOT DEFERRABLE | DEFERRABLE } ] - - DEFERRABLE is only meaningful for SERIALIZABLE isolation level. - """ - params = expr.get_params() - parts = ["BEGIN"] - - isolation = params.get("isolation_level") - if isolation: - level_str = self.get_isolation_level_name(isolation) - parts.append(f"ISOLATION LEVEL {level_str}") - - mode = params.get("mode") - if mode: - mode_name = mode.name if hasattr(mode, "name") else str(mode) - if mode_name == "READ_ONLY": - parts.append("READ ONLY") - elif mode_name == "READ_WRITE": - parts.append("READ WRITE") - - deferrable = params.get("deferrable") - if deferrable is not None and isolation: - isolation_name = isolation.name if hasattr(isolation, "name") else str(isolation) - if isolation_name == "SERIALIZABLE": - parts.append("DEFERRABLE" if deferrable else "NOT DEFERRABLE") - - return " ".join(parts), () - - - - - - def format_set_transaction( - self, expr: "SetTransactionExpression" - ) -> Tuple[str, tuple]: - """Format SET TRANSACTION statement for PostgreSQL. - - PostgreSQL supports setting transaction characteristics for the current - transaction or for subsequent transactions. - - Syntax: - SET TRANSACTION { ISOLATION LEVEL { ... } | { READ WRITE | READ ONLY } | [ NOT ] DEFERRABLE } [, ...] - SET SESSION CHARACTERISTICS AS TRANSACTION { ... } - """ - params = expr.get_params() - parts = [] - - if params.get("session"): - parts.append("SET SESSION CHARACTERISTICS AS TRANSACTION") - else: - parts.append("SET TRANSACTION") - - options = [] - - isolation = params.get("isolation_level") - if isolation: - level_str = self.get_isolation_level_name(isolation) - options.append(f"ISOLATION LEVEL {level_str}") - - mode = params.get("mode") - if mode: - mode_name = mode.name if hasattr(mode, "name") else str(mode) - if mode_name == "READ_ONLY": - options.append("READ ONLY") - elif mode_name == "READ_WRITE": - options.append("READ WRITE") - - deferrable = params.get("deferrable") - if deferrable is not None: - options.append("DEFERRABLE" if deferrable else "NOT DEFERRABLE") - - if options: - parts.append(" ".join(options)) - - return " ".join(parts), () - - # endregion diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py index a43bfe77..a5742759 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py @@ -14,6 +14,12 @@ from .extension import PostgresExtensionMixin from .materialized_view import PostgresMaterializedViewMixin from .table import PostgresTableMixin +from .datetime import PostgresDateTimeMixin +from .dql import PostgresDQLMixin +from .json import PostgresJSONMixin +from .generated_column import PostgresGeneratedColumnMixin +from .expression import PostgresExpressionMixin +from .function import PostgresFunctionMixin # DDL mixins from .ddl.partition import PostgresPartitionMixin @@ -119,6 +125,12 @@ "PostgresExtensionMixin", "PostgresMaterializedViewMixin", "PostgresTableMixin", + "PostgresDateTimeMixin", + "PostgresDQLMixin", + "PostgresJSONMixin", + "PostgresGeneratedColumnMixin", + "PostgresExpressionMixin", + "PostgresFunctionMixin", # DDL mixins "PostgresPartitionMixin", "PostgresIndexMixin", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py index 0ce2a6b2..bbd40943 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py @@ -10,3 +10,18 @@ class PostgresCollationMixin: def supports_collate_expression(self) -> bool: return True + + def validate_collation_name(self, expr) -> str: + """Validate PostgreSQL collation names and return their SQL representation.""" + from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError + from ..collation import validate_postgres_collation_name + + schema = expr.collation_options.get("schema") + unsupported = set(expr.collation_options) - {"schema"} + if unsupported: + options = ", ".join(sorted(unsupported)) + raise UnsupportedFeatureError(self.name, f"COLLATE options: {options}") + validate_postgres_collation_name(expr.collation_name, getattr(self, "version", None)) + if schema is not None: + return f"{self.format_identifier(str(schema))}.{self.format_identifier(expr.collation_name)}" + return self.format_identifier(expr.collation_name) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/datetime.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/datetime.py new file mode 100644 index 00000000..9b5dd4c7 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/datetime.py @@ -0,0 +1,36 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/datetime.py +"""PostgreSQL datetime formatting mixin.""" + + +class PostgresDateTimeMixin: + """PostgreSQL datetime diff expression formatting.""" + + def format_datetime_diff_expression(self, expr: "Any") -> tuple: + start_sql, start_params = expr.start.to_sql() + end_sql, end_params = expr.end.to_sql() + seconds_sql = f"EXTRACT(EPOCH FROM ({end_sql} - {start_sql}))" + factors = { + "second": "1", + "minute": "60", + "hour": "3600", + "day": "86400", + "week": "604800", + } + if expr.unit.value in factors: + sql = f"({seconds_sql} / {factors[expr.unit.value]})" + params = end_params + start_params + elif expr.unit.value == "month": + sql = ( + f"((EXTRACT(YEAR FROM {end_sql}) - " + f"EXTRACT(YEAR FROM {start_sql})) * 12 + " + f"(EXTRACT(MONTH FROM {end_sql}) - " + f"EXTRACT(MONTH FROM {start_sql})))" + ) + params = end_params + start_params + end_params + start_params + else: + sql = ( + f"(EXTRACT(YEAR FROM {end_sql}) - " + f"EXTRACT(YEAR FROM {start_sql}))" + ) + params = end_params + start_params + return self.apply_alias(sql, params, expr) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py index 6c5923de..ee2550ff 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py @@ -97,3 +97,38 @@ def format_alter_column_action(self, action) -> Tuple[str, tuple]: else: modified = f"{sql} USING ({using_sql})" return modified, tuple(params) + tuple(using_params) + + def format_column(self, expr) -> Tuple[str, tuple]: + """Format column reference for PostgreSQL. + + PostgreSQL rules for column references: + - When the table has an alias (used in FROM/JOIN), column references + must use the alias, not the schema-qualified name. For example, + when FROM clause has ``public.users AS "users"``, the column + reference must be ``"users"."id"``, not ``"public"."users"."id"``. + - When no alias is present but schema is specified, use the + full three-segment form: ``"schema"."table"."column"``. + - Otherwise use the standard two-segment or single-segment form. + """ + name = expr.name + table = expr.table + alias = expr.alias + schema_name = expr.schema_name + if table: + # In PostgreSQL, when a table has an alias, column references + # must use the alias — schema_name is irrelevant in this context. + if schema_name and not alias: + col_sql = ( + f"{self.format_identifier(schema_name, expr.schema_need_quote)}." + f"{self.format_identifier(table, expr.table_need_quote)}." + f"{self.format_identifier(name, expr.name_need_quote)}" + ) + else: + col_sql = f"{self.format_identifier(table, expr.table_need_quote)}.{self.format_identifier(name, expr.name_need_quote)}" + else: + col_sql = self.format_identifier(name, expr.name_need_quote) + + if alias: + col_sql = f"{col_sql} AS {self.format_identifier(alias, expr.alias_need_quote)}" + + return col_sql, () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py index 213e74f5..d71b6a99 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py @@ -6,14 +6,16 @@ SQL standard constraint capability defaults are defined in the core ConstraintMixin (rhosocial.activerecord.backend.dialect.mixins). - -Note: The format_add_table_constraint_action method is defined directly -in PostgresDialect rather than in this mixin, because Python's C3 MRO -places SQLDialectBase before mixins in the inheritance chain, which -prevents mixin methods from overriding base class methods of the same name. -This mixin provides capability detection defaults only. """ +from typing import Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.expression.statements import ( + AddTableConstraint, + TableConstraint, + ) + class PostgresConstraintMixin: """PostgreSQL-proprietary constraint capability defaults. @@ -49,3 +51,124 @@ def supports_drop_constraint_if_exists(self) -> bool: ``DROP CONSTRAINT`` since 9.6. """ return True + + def format_add_table_constraint_action( + self, action: "AddTableConstraint", + ) -> Tuple[str, tuple]: + """Format ADD CONSTRAINT action with PostgreSQL-specific extensions. + + Extends the base class implementation with: + - EXCLUDE constraint support (PG-specific constraint type) + - NOT VALID suffix (PG-specific: skip validation of existing rows) + """ + from rhosocial.activerecord.backend.expression.statements import ( + TableConstraintType, ConstraintValidation, + ) + + # Handle EXCLUDE constraint (PG-specific, not in base class) + if action.constraint.constraint_type == TableConstraintType.EXCLUDE: + parts = [] + exclude_sql, params = self._format_exclude_constraint(action.constraint) + parts.append(exclude_sql) + + # NOT VALID suffix + if action.constraint.dialect_options: + validation = action.constraint.dialect_options.get('validation') + if validation == ConstraintValidation.NOVALIDATE: + parts.append("NOT VALID") + + return f"ADD {' '.join(parts)}", tuple(params) + + # Use base class for standard formatting (includes DEFERRABLE) + sql, params = super().format_add_table_constraint_action(action) + + # PostgreSQL NOT VALID suffix + if action.constraint.dialect_options: + validation = action.constraint.dialect_options.get('validation') + if validation == ConstraintValidation.NOVALIDATE: + sql += " NOT VALID" + + return sql, params + + def _format_exclude_constraint( + self, constraint: "TableConstraint", + ) -> Tuple[str, tuple]: + """Format EXCLUDE constraint (PostgreSQL-specific). + + EXCLUDE constraints use the dialect_options dict to specify: + - 'exclude_elements': List of (expression, operator) tuples + e.g., [('range', '&&')] for EXCLUDE USING gist (range WITH &&) + - 'using': The index access method (default 'gist') + e.g., 'gist', 'btree', 'spgist' + - 'where': Optional predicate for partial exclusion constraints + + Example: + TableConstraint( + constraint_type=TableConstraintType.EXCLUDE, + name='exclude_range_overlap', + dialect_options={ + 'exclude_elements': [('range', '&&')], + 'using': 'gist', + } + ) + # Generates: EXCLUDE USING gist (range WITH &&) + + """ + params: list = [] + + if constraint.name: + parts = ["CONSTRAINT", self.format_identifier(constraint.name)] + else: + parts = [] + + # USING clause - validate index access method. + valid_using = frozenset({"gist", "btree", "spgist", "hash", "gin", "brin"}) + using = constraint.dialect_options.get("using", "gist") if constraint.dialect_options else "gist" + if using not in valid_using: + raise ValueError( + f"Invalid index access method '{using}': must be one of {valid_using}" + ) + parts.append(f"EXCLUDE USING {using}") + + # Elements: (expression, operator) pairs - validate operators. + valid_ops = frozenset({ + "=", "<", "<=", ">", ">=", "<>", + "&&", "@>", "<@", "<<", ">>", "&<", "&>", + "~=", "@@", "?|", "?&", "is", "is not", + }) + exclude_elements = [] + if constraint.dialect_options and "exclude_elements" in constraint.dialect_options: + for expr, op in constraint.dialect_options["exclude_elements"]: + if op not in valid_ops: + raise ValueError( + f"Invalid exclude operator '{op}': must be one of {valid_ops}" + ) + if isinstance(expr, str): + exclude_elements.append(f"{self.format_identifier(expr)} WITH {op}") + else: + expr_sql, expr_params = expr.to_sql() + params.extend(expr_params) + exclude_elements.append(f"{expr_sql} WITH {op}") + + if exclude_elements: + parts.append(f"({', '.join(exclude_elements)})") + + # WHERE clause for partial exclusion constraint + if constraint.dialect_options and 'where' in constraint.dialect_options: + where_expr = constraint.dialect_options['where'] + where_sql, where_params = where_expr.to_sql() + params.extend(where_params) + parts.append(f"WHERE ({where_sql})") + + # DEFERRABLE / NOT DEFERRABLE + if constraint.deferrable is True: + if constraint.initially_deferred is True: + parts.append("DEFERRABLE INITIALLY DEFERRED") + elif constraint.initially_deferred is False: + parts.append("DEFERRABLE INITIALLY IMMEDIATE") + else: + parts.append("DEFERRABLE") + elif constraint.deferrable is False: + parts.append("NOT DEFERRABLE") + + return ' '.join(parts), tuple(params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py index ee654b26..fe60a722 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py @@ -670,3 +670,33 @@ def format_create_index_pg_statement( parts.append(f"WHERE {where_clause}") return (" ".join(parts), tuple(all_params)) + + def format_add_index_action(self, action) -> Tuple[str, tuple]: + """PostgreSQL does not support ALTER TABLE ADD INDEX. + + Raises UnsupportedFeatureError — use CREATE INDEX instead. + """ + from rhosocial.activerecord.backend.dialect.exceptions import ( + UnsupportedFeatureError, + ) + + raise UnsupportedFeatureError( + self.name, + "ALTER TABLE ADD INDEX", + suggestion="Use CREATE INDEX to create an index on the table.", + ) + + def format_drop_index_action(self, action) -> Tuple[str, tuple]: + """PostgreSQL does not support ALTER TABLE DROP INDEX. + + Raises UnsupportedFeatureError — use DROP INDEX statement instead. + """ + from rhosocial.activerecord.backend.dialect.exceptions import ( + UnsupportedFeatureError, + ) + + raise UnsupportedFeatureError( + self.name, + "ALTER TABLE DROP INDEX", + suggestion="Use DROP INDEX statement to remove an index.", + ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/locking.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/locking.py index 56c6eb40..bae51098 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/locking.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/locking.py @@ -94,3 +94,31 @@ def format_for_update_clause(self, clause) -> Tuple[str, tuple]: sql_parts.append("SKIP LOCKED") return " ".join(sql_parts), tuple(all_params) + + def supports_lock_strength(self, strength) -> bool: + """Check if a specific lock strength is supported. + + PostgreSQL lock strength support by version: + - FOR UPDATE: All versions + - FOR NO KEY UPDATE: PostgreSQL 9.0+ + - FOR SHARE: PostgreSQL 9.0+ + - FOR KEY SHARE: PostgreSQL 9.3+ + + Args: + strength: The LockStrength enum value to check + + Returns: + True if the lock strength is supported, False otherwise + + """ + from rhosocial.activerecord.backend.impl.postgres.expression.locking import LockStrength + + if strength == LockStrength.UPDATE: + return True # All PostgreSQL versions support FOR UPDATE + elif strength == LockStrength.NO_KEY_UPDATE: + return self.version >= (9, 0, 0) + elif strength == LockStrength.SHARE: + return self.version >= (9, 0, 0) + elif strength == LockStrength.KEY_SHARE: + return self.version >= (9, 3, 0) + return False diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dql.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dql.py new file mode 100644 index 00000000..9154ad29 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dql.py @@ -0,0 +1,16 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/dql.py +"""PostgreSQL DQL feature support mixin.""" + + +class PostgresDQLMixin: + """PostgreSQL DQL feature support.""" + + def supports_for_update(self) -> bool: + """Whether FOR UPDATE clause is supported in SELECT statements. + + PostgreSQL supports FOR UPDATE since early versions. The clause locks + selected rows preventing other transactions from modifying them. + PostgreSQL also supports FOR UPDATE OF, FOR UPDATE NOWAIT, and + FOR UPDATE SKIP LOCKED (since 9.5). + """ + return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py new file mode 100644 index 00000000..46239ac0 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py @@ -0,0 +1,53 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py +"""PostgreSQL expression formatting mixin.""" + + +class PostgresExpressionMixin: + """PostgreSQL-specific expression formatting.""" + + def format_cast_expression(self, expr) -> tuple: + """Format type cast expression using PostgreSQL :: syntax. + + PostgreSQL supports both standard CAST(expr AS type) syntax and the + PostgreSQL-specific expr::type syntax. This method uses the more + concise :: syntax which is idiomatic in PostgreSQL. + + ``expr.expression`` renders through its own ``to_sql()``; the cast + target is the node's ``target_type`` construction parameter. + + Args: + expr: :class:`~...expression.core.CastExpression` instance. + + Returns: + Tuple of (SQL string, parameters) + + Example: + >>> Column(dialect, 'price').cast('numeric').to_sql() + # Returns: ('"price"::numeric', ()) + + Note: + For chained type conversions, each ::type is appended: + >>> col.cast('money').cast('numeric').cast('float8') + # Generates: "col"::money::numeric::float8 + + """ + expr_sql, params = expr.expression.to_sql() + sql = f"{expr_sql}::{expr.target_type}" + if expr.alias: + sql = f"{sql} AS {self.format_identifier(expr.alias)}" + return sql, params + + def format_binary_operator(self, expr) -> tuple: + """Format binary operator with psycopg placeholder escaping. + + psycopg uses %s as parameter placeholder. When the SQL operator itself + contains % (e.g., pg_trgm similarity operator), it must be escaped as %% + to prevent psycopg from interpreting it as a placeholder prefix. + """ + left_sql, left_params = expr.left.to_sql() + right_sql, right_params = expr.right.to_sql() + op = expr.op + # Escape % in operators for psycopg compatibility + escaped_op = op.replace('%', '%%') if '%' in op else op + sql = f"{left_sql} {escaped_op} {right_sql}" + return sql, left_params + right_params diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/function.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/function.py new file mode 100644 index 00000000..a565b481 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/function.py @@ -0,0 +1,171 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/function.py +"""PostgreSQL function support detection mixin.""" + +from typing import Dict, TYPE_CHECKING + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.impl.postgres.function_versions import ( + FunctionSupportInfo, + FunctionVersionRequirement, + ) + + +class PostgresFunctionMixin: + """PostgreSQL function support detection.""" + + def supports_functions(self) -> Dict[str, "FunctionSupportInfo"]: + """Return supported SQL functions with detailed support information. + + This method combines: + 1. Core functions from rhosocial.activerecord.backend.expression.functions + 2. PostgreSQL-specific functions from rhosocial.activerecord.backend.impl.postgres.functions + + Each function is mapped to a FunctionSupportInfo indicating: + - Whether the function is supported + - If not, the reason why (PG version, extension status, etc.) + + Returns: + Dict mapping function names to FunctionSupportInfo. + + """ + from ..function_versions import FunctionSupportInfo + from rhosocial.activerecord.backend.expression.functions import ( + __all__ as core_functions, + ) + from rhosocial.activerecord.backend.impl.postgres import functions as postgres_functions + + expression_constructors = { + "xmlagg", + "xmlattributes", + "xmlcomment", + "xmlconcat", + "xmlelement", + "xmlexists", + "xmlforest", + "xmlparse", + "xmlpi", + "xmlquery", + "xmlroot", + "xmlserialize", + "xmltable", + } + result: Dict[str, FunctionSupportInfo] = {} + for func_name in core_functions: + if func_name not in expression_constructors: + result[func_name] = self._check_function_support(func_name) + + postgres_funcs = getattr(postgres_functions, "__all__", []) + for func_name in postgres_funcs: + if func_name not in result: + result[func_name] = self._check_function_support(func_name) + + return result + + def _check_function_support(self, func_name: str) -> "FunctionSupportInfo": + """Check function support status and return detailed information. + + Args: + func_name: Name of the function to check + + Returns: + FunctionSupportInfo with support status and reason if unsupported + + """ + from ..function_versions import FunctionSupportInfo + + requirement = self._POSTGRES_FUNCTION_VERSIONS.get(func_name) + if requirement is None: + return FunctionSupportInfo(supported=True) + + # Check PostgreSQL server version + if requirement.min_pg_version is not None and self.version < requirement.min_pg_version: + return FunctionSupportInfo(supported=False, reason="pg_version_too_low") + if requirement.max_pg_version is not None and self.version > requirement.max_pg_version: + return FunctionSupportInfo(supported=False, reason="pg_version_too_high") + + # Check extension requirements + if requirement.extension is not None: + if not hasattr(self, "_extensions"): + return FunctionSupportInfo(supported=False, reason="extension_not_probed") + + if requirement.ext_feature is not None: + if not self.check_extension_feature(requirement.extension, requirement.ext_feature): + if not self.is_extension_installed(requirement.extension): + return FunctionSupportInfo(supported=False, reason="extension_not_installed") + return FunctionSupportInfo(supported=False, reason="extension_version_insufficient") + else: + if not self.is_extension_installed(requirement.extension): + return FunctionSupportInfo(supported=False, reason="extension_not_installed") + if requirement.min_ext_version is not None: + installed = self.get_extension_version(requirement.extension) + if installed is None or self._compare_versions(installed, requirement.min_ext_version) < 0: + return FunctionSupportInfo(supported=False, reason="extension_version_insufficient") + + return FunctionSupportInfo(supported=True) + + def _is_postgres_function_supported(self, func_name: str) -> bool: + """Check if a PostgreSQL function is supported based on version and extensions. + + Checks: + 1. PostgreSQL server version (for built-in and version-gated functions) + 2. Extension installation and version (for extension-provided functions) + + For extension functions, requires _extensions to have been populated + via introspect_and_adapt(). If _extensions is not available, + extension functions return False (cannot confirm availability). + + Args: + func_name: Name of the PostgreSQL function + + Returns: + True if supported, False otherwise + + """ + requirement = self._POSTGRES_FUNCTION_VERSIONS.get(func_name) + if requirement is None: + return True # Unregistered functions default to supported + + # Step 1: Check PostgreSQL server version + if requirement.min_pg_version is not None and self.version < requirement.min_pg_version: + return False + if requirement.max_pg_version is not None and self.version > requirement.max_pg_version: + return False + + # Step 2: Check extension requirements + if requirement.extension is not None: + return self._check_extension_requirement(requirement) + + return True + + def _check_extension_requirement( + self, requirement: "FunctionVersionRequirement" + ) -> bool: + """Check extension requirements based on introspect_and_adapt() results. + + Requires _extensions to have been populated via introspect_and_adapt(). + If _extensions is not available, returns False (cannot confirm + extension availability without probing the database). + """ + ext_name = requirement.extension + + # _extensions is populated by introspect_and_adapt() + # If the attribute doesn't exist, introspect_and_adapt() hasn't been called + if not hasattr(self, "_extensions"): + return False + + # Use feature-level check if ext_feature is specified + if requirement.ext_feature is not None: + return self.check_extension_feature(ext_name, requirement.ext_feature) + + # Check extension installed + if not self.is_extension_installed(ext_name): + return False + + # Check extension version + if requirement.min_ext_version is not None: + installed = self.get_extension_version(ext_name) + if installed is None: + return False + return self._compare_versions(installed, requirement.min_ext_version) >= 0 + + return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py new file mode 100644 index 00000000..cd32328b --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py @@ -0,0 +1,27 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py +"""PostgreSQL generated column feature support mixin.""" + + +class PostgresGeneratedColumnMixin: + """PostgreSQL generated column feature support.""" + + def supports_auto_increment(self) -> bool: + """Whether AUTO_INCREMENT/IDENTITY column attributes are supported. + + PostgreSQL supports SERIAL and GENERATED ... AS IDENTITY. + """ + return True + + def supports_stored_generated_columns(self) -> bool: + """Whether STORED generated columns are supported. + + PostgreSQL generated columns are always STORED. + """ + return self.supports_generated_columns() + + def supports_virtual_generated_columns(self) -> bool: + """Whether VIRTUAL generated columns are supported. + + PostgreSQL has no VIRTUAL generated columns. + """ + return False diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py index ce9033a6..db9cafa5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py @@ -10,3 +10,27 @@ class PostgresILIKEMixin: def supports_ilike(self) -> bool: return True + + def format_ilike_expression(self, column, pattern: str, negate: bool = False) -> tuple: + """Format ILIKE expression for PostgreSQL. + + Args: + column: Column name string or expression with ``to_sql()``. + pattern: Right-hand-side pattern (replaced with ``%s`` placeholder). + negate: If ``True``, produces ``NOT ILIKE`` instead of ``ILIKE``. + + Returns: + Tuple of (SQL string, (pattern,) params tuple) + + """ + if isinstance(column, str): + col_sql = self.format_identifier(column) + else: + col_sql, col_params = column.to_sql() if hasattr(column, "to_sql") else (str(column), ()) + + if negate: + sql = f"{col_sql} NOT ILIKE %s" + else: + sql = f"{col_sql} ILIKE %s" + + return sql, (pattern,) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/json.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/json.py new file mode 100644 index 00000000..b44b38c9 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/json.py @@ -0,0 +1,10 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/json.py +"""PostgreSQL JSON feature support mixin.""" + + +class PostgresJSONMixin: + """PostgreSQL JSON feature support.""" + + def supports_json_arrow_operators(self) -> bool: + """PostgreSQL supports -> and ->> operators for JSON/JSONB access.""" + return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py index 53562079..899c3eff 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py @@ -12,6 +12,49 @@ def supports_materialized_view_concurrent_refresh(self) -> bool: """CONCURRENTLY is supported since PostgreSQL 9.4.""" return self.version >= (9, 4, 0) + def format_create_materialized_view_statement(self, expr) -> tuple: + """Format CREATE MATERIALIZED VIEW statement for PostgreSQL. + + - ``expr.view_name`` — view name (identifier). + - ``expr.column_aliases`` — optional list of column aliases. + - ``expr.tablespace`` — optional tablespace. + - ``expr.storage_options`` — optional dict of storage parameters (``WITH (… )``). + - ``expr.query`` — source SELECT expression. + - ``expr.with_data`` — ``WITH DATA`` / ``WITH NO DATA``. + + Args: + expr: CreateMaterializedViewExpression instance + + Returns: + Tuple of (SQL string, params tuple) + + """ + parts = ["CREATE MATERIALIZED VIEW"] + parts.append(self.format_identifier(expr.view_name)) + + if expr.column_aliases: + cols = ", ".join(self.format_identifier(c) for c in expr.column_aliases) + parts.append(f"({cols})") + + if expr.tablespace and self.supports_materialized_view_tablespace(): + parts.append(f"TABLESPACE {self.format_identifier(expr.tablespace)}") + + if expr.storage_options and self.supports_materialized_view_storage_options(): + storage_parts = [] + for key, value in expr.storage_options.items(): + storage_parts.append(f"{key.upper()} = {value}") + parts.append(f"WITH ({', '.join(storage_parts)})") + + query_sql, query_params = expr.query.to_sql() + parts.append(f"AS {query_sql}") + + if expr.with_data: + parts.append("WITH DATA") + else: + parts.append("WITH NO DATA") + + return " ".join(parts), query_params + def format_refresh_materialized_view_pg_statement( self, expr: "PostgresRefreshMaterializedViewExpression" ) -> Tuple[str, tuple]: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py index 258f35fa..409e9cc3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py @@ -41,16 +41,82 @@ def supports_create_table_like(self) -> bool: return True def format_create_table_statement(self, expr) -> Tuple[str, tuple]: - """Render CREATE TABLE, injecting the UNLOGGED qualifier. - - The core ``TableMixin.format_create_table_statement`` does not know - about PostgreSQL's ``UNLOGGED`` table classes (WAL-avoiding tables). - Callers opt in via ``dialect_options={"unlogged": True}`` on the - ``CreateTableExpression``; the qualifier is version-gated at 9.5+. - ``UNLOGGED`` and ``TEMPORARY`` are mutually exclusive qualifiers in - PostgreSQL's grammar; TEMPORARY wins when both are requested. + """Render CREATE TABLE for PostgreSQL. + + Handles PostgreSQL-specific CREATE TABLE behavior: + * PostgreSQL LIKE syntax with INCLUDING/EXCLUDING options. + * Declarative partitioning validation for ``partition`` expressions. + * UNLOGGED table qualifier. + + The LIKE syntax is controlled by ``dialect_options['like_table']``. + When present, all other parameters (columns, indexes, constraints) + are ignored. The ``like_options`` key controls INCLUDING/EXCLUDING + behavior (dict or list format). + + Partition validation follows PostgreSQL declarative partitioning rules: + - RANGE and LIST require PostgreSQL 10+. + - HASH requires PostgreSQL 11+. + - MySQL-specific methods (KEY, RANGE COLUMNS) are rejected. """ + # Check for LIKE syntax in dialect_options (highest priority) + if "like_table" in expr.dialect_options: + like_table = expr.dialect_options["like_table"] + like_options = expr.dialect_options.get("like_options", []) + + parts = ["CREATE"] + + if expr.temporary: + parts.append("TEMPORARY") + + parts.append("TABLE") + + if expr.if_not_exists: + parts.append("IF NOT EXISTS") + + parts.append(self.format_identifier(expr.table_name)) + + # Build LIKE clause with options + like_parts = [] + + # Handle schema-qualified table name: ('schema', 'table') + if isinstance(like_table, tuple): + schema, table = like_table + like_table_str = f"{self.format_identifier(schema)}.{self.format_identifier(table)}" + else: + like_table_str = self.format_identifier(like_table) + + like_parts.append(f"LIKE {like_table_str}") + + # Add INCLUDING/EXCLUDING options + if isinstance(like_options, dict): + including = like_options.get("including", []) + excluding = like_options.get("excluding", []) + + for option in including: + like_parts.append(f"INCLUDING {option.upper()}") + + for option in excluding: + like_parts.append(f"EXCLUDING {option.upper()}") + elif isinstance(like_options, list): + for option in like_options: + if isinstance(option, tuple): + action, feature = option + like_parts.append(f"{action.upper()} {feature.upper()}") + else: + like_parts.append(f"INCLUDING {option.upper()}") + + parts.append(f"({', '.join(like_parts)})") + + return " ".join(parts), () + + if getattr(expr, "partition", None) is not None: + # Validate through the PartitionClause -> format_partition_clause chain. + expr.partition.to_sql() + + # Delegate to base implementation (TableMixin) sql, params = super().format_create_table_statement(expr) + + # Apply UNLOGGED qualifier if requested if not (getattr(expr, "dialect_options", None) or {}).get("unlogged_table"): return sql, params if getattr(expr, "temporary", False): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py index aa7eafb2..4b24681e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py @@ -1,6 +1,14 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py """PostgreSQL transaction feature support implementation.""" +from typing import Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.expression.transaction import ( + BeginTransactionExpression, + SetTransactionExpression, + ) + class PostgresTransactionMixin: """PostgreSQL transaction override implementation. @@ -22,3 +30,83 @@ def supports_deferrable_transaction(self) -> bool: def supports_savepoint(self) -> bool: return True + + def format_begin_transaction( + self, expr: "BeginTransactionExpression" + ) -> Tuple[str, tuple]: + """Format BEGIN TRANSACTION statement for PostgreSQL. + + PostgreSQL syntax: + BEGIN [ ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE } ] + [ { READ WRITE | READ ONLY } ] + [ { NOT DEFERRABLE | DEFERRABLE } ] + + DEFERRABLE is only meaningful for SERIALIZABLE isolation level. + """ + params = expr.get_params() + parts = ["BEGIN"] + + isolation = params.get("isolation_level") + if isolation: + level_str = self.get_isolation_level_name(isolation) + parts.append(f"ISOLATION LEVEL {level_str}") + + mode = params.get("mode") + if mode: + mode_name = mode.name if hasattr(mode, "name") else str(mode) + if mode_name == "READ_ONLY": + parts.append("READ ONLY") + elif mode_name == "READ_WRITE": + parts.append("READ WRITE") + + deferrable = params.get("deferrable") + if deferrable is not None and isolation: + isolation_name = isolation.name if hasattr(isolation, "name") else str(isolation) + if isolation_name == "SERIALIZABLE": + parts.append("DEFERRABLE" if deferrable else "NOT DEFERRABLE") + + return " ".join(parts), () + + def format_set_transaction( + self, expr: "SetTransactionExpression" + ) -> Tuple[str, tuple]: + """Format SET TRANSACTION statement for PostgreSQL. + + PostgreSQL supports setting transaction characteristics for the current + transaction or for subsequent transactions. + + Syntax: + SET TRANSACTION { ISOLATION LEVEL { ... } | { READ WRITE | READ ONLY } | [ NOT ] DEFERRABLE } [, ...] + SET SESSION CHARACTERISTICS AS TRANSACTION { ... } + """ + params = expr.get_params() + parts = [] + + if params.get("session"): + parts.append("SET SESSION CHARACTERISTICS AS TRANSACTION") + else: + parts.append("SET TRANSACTION") + + options = [] + + isolation = params.get("isolation_level") + if isolation: + level_str = self.get_isolation_level_name(isolation) + options.append(f"ISOLATION LEVEL {level_str}") + + mode = params.get("mode") + if mode: + mode_name = mode.name if hasattr(mode, "name") else str(mode) + if mode_name == "READ_ONLY": + options.append("READ ONLY") + elif mode_name == "READ_WRITE": + options.append("READ WRITE") + + deferrable = params.get("deferrable") + if deferrable is not None: + options.append("DEFERRABLE" if deferrable else "NOT DEFERRABLE") + + if options: + parts.append(" ".join(options)) + + return " ".join(parts), () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py index 31612d0e..ef74295f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py @@ -17,3 +17,74 @@ def supports_on_conflict_clause(self) -> bool: def supports_multiple_on_conflict_clauses(self) -> bool: """PostgreSQL grammar allows only a single ON CONFLICT clause per INSERT.""" return False + + def format_on_conflict_clause(self, expr) -> tuple: + """Format ON CONFLICT clause for PostgreSQL. + + Overrides the base implementation to handle EXCLUDED pseudo-table + references without quoting, as EXCLUDED is a special PostgreSQL + keyword in ON CONFLICT context and must not be double-quoted. + + - ``expr.conflict_target`` — optional list of column names / expressions. + - ``expr.do_nothing`` — ``DO NOTHING``. + - ``expr.update_assignments`` — dict of ``{column: expression}`` for ``DO UPDATE SET``. + - ``expr.update_where`` — optional WHERE predicate for the update. + + Args: + expr: OnConflictClause expression instance + + Returns: + Tuple of (SQL string, params tuple) + + """ + from rhosocial.activerecord.backend.expression import bases + from rhosocial.activerecord.backend.expression.core import Column + + all_params = [] + parts = ["ON CONFLICT"] + + # Add conflict target if specified + if expr.conflict_target: + target_parts = [] + for target in expr.conflict_target: + if isinstance(target, str): + target_parts.append(self.format_identifier(target)) + elif hasattr(target, 'to_sql'): + target_sql, target_params = target.to_sql() + target_parts.append(target_sql) + all_params.extend(target_params) + else: + target_parts.append(self.format_identifier(str(target))) + if target_parts: + parts.append(f"({', '.join(target_parts)})") + + # Add DO NOTHING or DO UPDATE + if expr.do_nothing: + parts.append("DO NOTHING") + elif expr.update_assignments: + update_parts = [] + for col, expr_val in expr.update_assignments.items(): + if isinstance(expr_val, Column) and getattr(expr_val, 'table', None) == 'EXCLUDED': + # EXCLUDED is a special pseudo-table in PostgreSQL ON CONFLICT. + # It must NOT be double-quoted, only the column name should be quoted. + val_sql = f'EXCLUDED.{self.format_identifier(expr_val.name)}' + update_parts.append(f"{self.format_identifier(col)} = {val_sql}") + elif isinstance(expr_val, bases.BaseExpression): + val_sql, val_params = expr_val.to_sql() + update_parts.append(f"{self.format_identifier(col)} = {val_sql}") + all_params.extend(val_params) + else: + update_parts.append(f"{self.format_identifier(col)} = {self.get_parameter_placeholder()}") + all_params.append(expr_val) + + parts.append(f"DO UPDATE SET {', '.join(update_parts)}") + + # Add WHERE clause if specified + if expr.update_where: + where_sql, where_params = expr.update_where.to_sql() + parts.append(f"WHERE {where_sql}") + all_params.extend(where_params) + else: + parts.append("DO NOTHING") + + return " ".join(parts), tuple(all_params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/view.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/view.py index 14dcd2a0..0e8f83d0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/view.py @@ -31,3 +31,44 @@ def supports_materialized_view_tablespace(self) -> bool: def supports_materialized_view_storage_options(self) -> bool: return True + + def format_create_view_statement(self, expr) -> tuple: + """Format CREATE VIEW statement for PostgreSQL. + + - ``expr.temporary`` — add ``TEMPORARY``. + - ``expr.replace`` — add ``OR REPLACE``. + - ``expr.view_name`` — view name (identifier). + - ``expr.column_aliases`` — optional list of column aliases. + - ``expr.query`` — source SELECT expression. + - ``expr.options.check_option`` — ``WITH {LOCAL|CASCADED} CHECK OPTION``. + + Args: + expr: CreateViewExpression instance + + Returns: + Tuple of (SQL string, params tuple) + + """ + parts = ["CREATE"] + + if expr.temporary: + parts.append("TEMPORARY") + + if expr.replace: + parts.append("OR REPLACE") + + parts.append("VIEW") + parts.append(self.format_identifier(expr.view_name)) + + if expr.column_aliases: + cols = ", ".join(self.format_identifier(c) for c in expr.column_aliases) + parts.append(f"({cols})") + + query_sql, query_params = expr.query.to_sql() + parts.append(f"AS {query_sql}") + + if expr.options and expr.options.check_option: + check_option = expr.options.check_option.value + parts.append(f"WITH {check_option} CHECK OPTION") + + return " ".join(parts), query_params From efbcd3d42293bbcfec04b16b62a4933ee3c28be3 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 14 Sep 2026 09:40:39 +0800 Subject: [PATCH 22/22] feat(postgres): format_* signature compliance - ILIKE + Enum + Multirange expressions - Add ILikeExpression for format_ilike_expression - Add EnumTypeNameExpression, EnumValuesExpression - Add CreateEnumTypeRawExpression, DropEnumTypeRawExpression, AlterEnumAddValueRawExpression - Add CreateMultirangeTypeStatementExpression, MultirangeAggFunctionExpression - Update format methods to accept expressions - Add 16 new tests --- .../impl/postgres/expression/__init__.py | 3 + .../impl/postgres/expression/ddl/__init__.py | 17 ++ .../postgres/expression/ddl/multirange.py | 104 +++++++++ .../impl/postgres/expression/ddl/type.py | 203 ++++++++++++++++++ .../backend/impl/postgres/expression/ilike.py | 51 +++++ .../impl/postgres/mixins/expression.py | 6 +- .../backend/impl/postgres/mixins/ilike.py | 26 ++- .../impl/postgres/mixins/types/enum.py | 84 ++++++++ .../postgres/mixins/types/jsonb_enhanced.py | 4 +- .../impl/postgres/mixins/types/multirange.py | 44 +++- .../expression/test_expression_signatures.py | 186 ++++++++++++++++ 11 files changed, 721 insertions(+), 7 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/multirange.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/expression/ilike.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expression_signatures.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py index 7d79bfc2..fbae29cd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py @@ -19,6 +19,7 @@ AdvisoryLockType, ) from .locking import LockStrength +from .ilike import ILIKEExpression from .ddl import ( PostgresVacuumExpression, PostgresAnalyzeExpression, @@ -110,6 +111,8 @@ "AdvisoryLockType", # locking "LockStrength", + # ilike + "ILIKEExpression", # ddl "PostgresVacuumExpression", "PostgresAnalyzeExpression", 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 1c4be6a0..2f45fd40 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py @@ -85,8 +85,17 @@ PostgresAlterEnumTypeAddValueExpression, PostgresAlterEnumTypeRenameValueExpression, PostgresCreateRangeTypeExpression, + EnumTypeNameExpression, + EnumValuesExpression, + CreateEnumTypeExpression, + DropEnumTypeExpression, + AlterEnumAddValueExpression, ) from .extension import PostgresCreateExtensionExpression, PostgresDropExtensionExpression +from .multirange import ( + CreateMultirangeTypeExpression, + MultirangeAggFunctionExpression, +) from .pg_partman import ( PostgresPgPartmanCreateParentExpression, PostgresPgPartmanRunMaintenanceExpression, @@ -166,6 +175,14 @@ "PostgresAlterEnumTypeAddValueExpression", "PostgresAlterEnumTypeRenameValueExpression", "PostgresCreateRangeTypeExpression", + "EnumTypeNameExpression", + "EnumValuesExpression", + "CreateEnumTypeExpression", + "DropEnumTypeExpression", + "AlterEnumAddValueExpression", + # multirange + "CreateMultirangeTypeExpression", + "MultirangeAggFunctionExpression", # extension "PostgresCreateExtensionExpression", "PostgresDropExtensionExpression", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/multirange.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/multirange.py new file mode 100644 index 00000000..b9bd702b --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/multirange.py @@ -0,0 +1,104 @@ +# src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/multirange.py +"""PostgreSQL multirange type DDL expressions. + +Version Requirements: +- CREATE TYPE ... AS MULTIRANGE: PostgreSQL 14+ +- multirange_agg(): PostgreSQL 14+ +""" + +from typing import Any, Dict, Optional, TYPE_CHECKING + +from rhosocial.activerecord.backend.expression.bases import BaseExpression + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.dialect import SQLDialectBase + + +class CreateMultirangeTypeExpression(BaseExpression): + """Expression for CREATE TYPE ... AS MULTIRANGE statement. + + PostgreSQL automatically creates multirange types when you create + a range type. This expression is for explicit creation if needed. + + Attributes: + name: Multirange type name. + range_type: Associated range type. + schema: Optional schema name. + + Example: + >>> expr = CreateMultirangeTypeExpression( + ... dialect, name="my_multirange", range_type="my_range", + ... ) + >>> sql, params = expr.to_sql() + >>> sql + 'CREATE TYPE my_multirange AS MULTIRANGE (my_range)' + + Note: + Requires PostgreSQL 14+. + """ + + def __init__( + self, + dialect: "SQLDialectBase", + name: str, + range_type: str, + schema: Optional[str] = None, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.name = name + self.range_type = range_type + self.schema = schema + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_multirange_type_statement" + + +class MultirangeAggFunctionExpression(BaseExpression): + """Expression for multirange_agg aggregate function call. + + Aggregates multiple ranges into a multirange. + + Attributes: + range_column: Range column to aggregate. + table_name: Table name. + where_clause: Optional WHERE clause. + schema: Optional schema name. + + Example: + >>> expr = MultirangeAggFunctionExpression( + ... dialect, range_column="period", table_name="events", + ... ) + >>> sql, params = expr.to_sql() + >>> sql + 'SELECT multirange_agg(period) FROM events' + + Note: + Requires PostgreSQL 14+. + """ + + def __init__( + self, + dialect: "SQLDialectBase", + range_column: str, + table_name: str, + where_clause: Optional[str] = None, + schema: Optional[str] = None, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.range_column = range_column + self.table_name = table_name + self.where_clause = where_clause + self.schema = schema + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_multirange_agg_function" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py index a783a5bb..728cd154 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py @@ -32,6 +32,12 @@ "PostgresAlterEnumTypeRenameValueExpression", # Range type "PostgresCreateRangeTypeExpression", + # Format-signature-compliant enum expressions + "EnumTypeNameExpression", + "EnumValuesExpression", + "CreateEnumTypeExpression", + "DropEnumTypeExpression", + "AlterEnumAddValueExpression", ] @@ -374,3 +380,200 @@ def __init__( def format_method(self) -> str: """The dialect formatting method that renders this expression.""" return "format_create_range_type" + + +# ============================================================================= +# Format-signature-compliant enum expressions +# ============================================================================= + + +class EnumTypeNameExpression(BaseExpression): + """Expression for formatting an enum type name with optional schema. + + Attributes: + name: Enum type name. + schema: Optional schema name. + + Example: + >>> expr = EnumTypeNameExpression(dialect, name="status", schema="app") + >>> sql, params = expr.to_sql() + >>> sql + 'app.status' + + """ + + def __init__( + self, + dialect: "SQLDialectBase", + name: str, + schema: Optional[str] = None, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.name = name + self.schema = schema + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_enum_type_name" + + +class EnumValuesExpression(BaseExpression): + """Expression for formatting a list of enum values for SQL. + + Attributes: + values: List of enum value strings. + + Example: + >>> expr = EnumValuesExpression(dialect, values=["a", "b", "c"]) + >>> sql, params = expr.to_sql() + >>> sql + "'a', 'b', 'c'" + + """ + + def __init__( + self, + dialect: "SQLDialectBase", + values: List[str], + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.values = values + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_enum_values" + + +class CreateEnumTypeExpression(BaseExpression): + """Expression for CREATE TYPE ... AS ENUM statement. + + Attributes: + name: Enum type name. + values: List of allowed values. + schema: Optional schema name. + if_not_exists: Add IF NOT EXISTS clause. + + Example: + >>> expr = CreateEnumTypeExpression( + ... dialect, name="status", values=["active", "inactive"], + ... ) + >>> sql, params = expr.to_sql() + >>> sql + "CREATE TYPE status AS ENUM ('active', 'inactive')" + + """ + + def __init__( + self, + dialect: "SQLDialectBase", + name: str, + values: List[str], + schema: Optional[str] = None, + if_not_exists: bool = False, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.name = name + self.values = values + self.schema = schema + self.if_not_exists = if_not_exists + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_create_enum_type_raw" + + +class DropEnumTypeExpression(BaseExpression): + """Expression for DROP TYPE statement. + + Attributes: + name: Enum type name. + schema: Optional schema name. + if_exists: Add IF EXISTS clause. + cascade: Add CASCADE clause. + + Example: + >>> expr = DropEnumTypeExpression(dialect, name="status") + >>> sql, params = expr.to_sql() + >>> sql + "DROP TYPE status" + + """ + + def __init__( + self, + dialect: "SQLDialectBase", + name: str, + schema: Optional[str] = None, + if_exists: bool = False, + cascade: bool = False, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.name = name + self.schema = schema + self.if_exists = if_exists + self.cascade = cascade + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_drop_enum_type_raw" + + +class AlterEnumAddValueExpression(BaseExpression): + """Expression for ALTER TYPE ... ADD VALUE statement. + + Attributes: + type_name: Enum type name. + new_value: New value to add. + schema: Optional schema name. + before: Add before this existing value. + after: Add after this existing value. + + Example: + >>> expr = AlterEnumAddValueExpression( + ... dialect, type_name="status", new_value="archived", + ... ) + >>> sql, params = expr.to_sql() + >>> sql + "ALTER TYPE status ADD VALUE 'archived'" + + """ + + def __init__( + self, + dialect: "SQLDialectBase", + type_name: str, + new_value: str, + schema: Optional[str] = None, + before: Optional[str] = None, + after: Optional[str] = None, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.type_name = type_name + self.new_value = new_value + self.schema = schema + self.before = before + self.after = after + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_alter_enum_add_value_raw" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ilike.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ilike.py new file mode 100644 index 00000000..753496ef --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ilike.py @@ -0,0 +1,51 @@ +# src/rhosocial/activerecord/backend/impl/postgres/expression/ilike.py +"""PostgreSQL ILIKE expression class.""" + +from typing import Any, Dict, Optional, TYPE_CHECKING + +from rhosocial.activerecord.backend.expression.bases import BaseExpression + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.dialect import SQLDialectBase + + +class ILIKEExpression(BaseExpression): + """PostgreSQL ILIKE expression for case-insensitive pattern matching. + + Attributes: + column: Column name or expression with ``to_sql()``. + pattern: Pattern string (replaced with ``%s`` placeholder). + negate: If True, produces ``NOT ILIKE`` instead of ``ILIKE``. + + Example: + >>> expr = ILIKEExpression(dialect, column="name", pattern="%foo%") + >>> sql, params = expr.to_sql() + >>> sql + '"name" ILIKE %s' + >>> params + ('%foo%',) + + Note: + Requires PostgreSQL backend. The ``ILIKE`` operator is + PostgreSQL-specific and not portable to other databases. + """ + + def __init__( + self, + dialect: "SQLDialectBase", + column: Any, + pattern: str, + negate: bool = False, + *, + dialect_options: Optional[Dict[str, Any]] = None, + ): + super().__init__(dialect) + self.column = column + self.pattern = pattern + self.negate = negate + self.dialect_options = dialect_options or {} + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_ilike_expression" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py index 46239ac0..b9c8173c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py @@ -1,11 +1,13 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/expression.py """PostgreSQL expression formatting mixin.""" +from typing import Tuple + class PostgresExpressionMixin: """PostgreSQL-specific expression formatting.""" - def format_cast_expression(self, expr) -> tuple: + def format_cast_expression(self, expr) -> Tuple[str, tuple]: """Format type cast expression using PostgreSQL :: syntax. PostgreSQL supports both standard CAST(expr AS type) syntax and the @@ -37,7 +39,7 @@ def format_cast_expression(self, expr) -> tuple: sql = f"{sql} AS {self.format_identifier(expr.alias)}" return sql, params - def format_binary_operator(self, expr) -> tuple: + def format_binary_operator(self, expr) -> Tuple[str, tuple]: """Format binary operator with psycopg placeholder escaping. psycopg uses %s as parameter placeholder. When the SQL operator itself diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py index db9cafa5..5d5e0e79 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py @@ -1,6 +1,13 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/ilike.py """PostgreSQL ilike feature support implementation.""" +from typing import Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.impl.postgres.expression.ilike import ( + ILIKEExpression, + ) + class PostgresILIKEMixin: """PostgreSQL ilike override implementation. @@ -11,18 +18,33 @@ class PostgresILIKEMixin: def supports_ilike(self) -> bool: return True - def format_ilike_expression(self, column, pattern: str, negate: bool = False) -> tuple: + def format_ilike_expression(self, column, pattern: str = "", negate: bool = False) -> tuple: """Format ILIKE expression for PostgreSQL. + Accepts either raw parameters or an ILIKEExpression object. + Args: - column: Column name string or expression with ``to_sql()``. + column: Column name string, expression with ``to_sql()``, + or an :class:`ILIKEExpression` instance. pattern: Right-hand-side pattern (replaced with ``%s`` placeholder). + Ignored when *column* is an ``ILIKEExpression``. negate: If ``True``, produces ``NOT ILIKE`` instead of ``ILIKE``. + Ignored when *column* is an ``ILIKEExpression``. Returns: Tuple of (SQL string, (pattern,) params tuple) """ + from rhosocial.activerecord.backend.impl.postgres.expression.ilike import ( + ILIKEExpression, + ) + + if isinstance(column, ILIKEExpression): + expr = column + column = expr.column + pattern = expr.pattern + negate = expr.negate + if isinstance(column, str): col_sql = self.format_identifier(column) else: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py index accea382..40e6ab7b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py @@ -12,6 +12,11 @@ PostgresCreateEnumTypeExpression, PostgresDropEnumTypeExpression, PostgresAlterEnumAddValueExpression, + EnumTypeNameExpression, + EnumValuesExpression, + CreateEnumTypeExpression, + DropEnumTypeExpression, + AlterEnumAddValueExpression, ) @@ -36,6 +41,18 @@ def format_enum_type_name(self, name: str, schema: Optional[str] = None) -> str: return f"{schema}.{name}" return name + def format_enum_type_name_expression(self, expr: "EnumTypeNameExpression") -> Tuple[str, tuple]: + """Format enum type name from expression object. + + Args: + expr: :class:`EnumTypeNameExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + return (self.format_enum_type_name(expr.name, expr.schema), ()) + def format_enum_values(self, values: List[str]) -> str: """Format enum values list for SQL. @@ -48,6 +65,18 @@ def format_enum_values(self, values: List[str]) -> str: """ return ", ".join(f"'{v}'" for v in values) + def format_enum_values_expression(self, expr: "EnumValuesExpression") -> Tuple[str, tuple]: + """Format enum values list from expression object. + + Args: + expr: :class:`EnumValuesExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + return (self.format_enum_values(expr.values), ()) + def format_create_enum_type_raw( self, name: str, values: List[str], schema: Optional[str] = None, if_not_exists: bool = False ) -> str: @@ -68,6 +97,24 @@ def format_create_enum_type_raw( exists_clause = "IF NOT EXISTS " if if_not_exists else "" return f"CREATE TYPE {exists_clause}{full_name} AS ENUM ({values_str})" + def format_create_enum_type_raw_expression(self, expr: "CreateEnumTypeExpression") -> Tuple[str, tuple]: + """Format CREATE TYPE ... AS ENUM from expression object. + + Args: + expr: :class:`CreateEnumTypeExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + sql = self.format_create_enum_type_raw( + expr.name, + expr.values, + expr.schema, + expr.if_not_exists, + ) + return (sql, ()) + def format_drop_enum_type_raw( self, name: str, schema: Optional[str] = None, if_exists: bool = False, cascade: bool = False ) -> str: @@ -88,6 +135,24 @@ def format_drop_enum_type_raw( cascade_clause = " CASCADE" if cascade else "" return f"DROP TYPE {exists_clause}{full_name}{cascade_clause}" + def format_drop_enum_type_raw_expression(self, expr: "DropEnumTypeExpression") -> Tuple[str, tuple]: + """Format DROP TYPE from expression object. + + Args: + expr: :class:`DropEnumTypeExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + sql = self.format_drop_enum_type_raw( + expr.name, + expr.schema, + expr.if_exists, + expr.cascade, + ) + return (sql, ()) + def format_alter_enum_add_value_raw( self, type_name: str, new_value: str, schema: Optional[str] = None, before: Optional[str] = None, after: Optional[str] = None @@ -113,6 +178,25 @@ def format_alter_enum_add_value_raw( sql += f" AFTER '{after}'" return sql + def format_alter_enum_add_value_raw_expression(self, expr: "AlterEnumAddValueExpression") -> Tuple[str, tuple]: + """Format ALTER TYPE ADD VALUE from expression object. + + Args: + expr: :class:`AlterEnumAddValueExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + sql = self.format_alter_enum_add_value_raw( + expr.type_name, + expr.new_value, + expr.schema, + expr.before, + expr.after, + ) + return (sql, ()) + # ========================================================================= # Convenience methods for EnumTypeSupport protocol # ========================================================================= diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py index 53c2e236..4074e3c7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py @@ -5,7 +5,7 @@ Implements the PostgresJSONBEnhancedSupport protocol. """ -from typing import TYPE_CHECKING +from typing import Tuple, TYPE_CHECKING if TYPE_CHECKING: from rhosocial.activerecord.backend.expression import JSONExpression @@ -18,7 +18,7 @@ class PostgresJSONBEnhancedMixin: JSON and JSONB features beyond the standard JSONSupport protocol. """ - def format_json_expression(self, expr: "JSONExpression"): + def format_json_expression(self, expr: "JSONExpression") -> Tuple[str, tuple]: from rhosocial.activerecord.backend.expression import bases if isinstance(expr.column, bases.BaseExpression): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/multirange.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/multirange.py index d11537e6..73fac075 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/multirange.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/multirange.py @@ -14,7 +14,13 @@ - format_multirange_agg_function: Complete SELECT query template """ -from typing import Optional, Tuple +from typing import Optional, Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.impl.postgres.expression.ddl.multirange import ( + CreateMultirangeTypeExpression, + MultirangeAggFunctionExpression, + ) class MultirangeMixin: @@ -90,3 +96,39 @@ def format_multirange_agg_function( if where_clause: sql += f" WHERE {where_clause}" return (sql, ()) + + # ========================================================================= + # Expression-based format methods + # ========================================================================= + + def format_create_multirange_type_statement_expression( + self, expr: "CreateMultirangeTypeExpression" + ) -> Tuple[str, tuple]: + """Format CREATE TYPE ... AS MULTIRANGE from expression object. + + Args: + expr: :class:`CreateMultirangeTypeExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + return self.format_create_multirange_type_statement( + expr.name, expr.range_type, expr.schema + ) + + def format_multirange_agg_function_expression( + self, expr: "MultirangeAggFunctionExpression" + ) -> Tuple[str, tuple]: + """Format multirange_agg aggregate function call from expression object. + + Args: + expr: :class:`MultirangeAggFunctionExpression` instance. + + Returns: + Tuple of (SQL string, empty params tuple). + + """ + return self.format_multirange_agg_function( + expr.range_column, expr.table_name, expr.where_clause, expr.schema + ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expression_signatures.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expression_signatures.py new file mode 100644 index 00000000..dcfbee34 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expression_signatures.py @@ -0,0 +1,186 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expression_signatures.py +"""SQL-snapshot tests for format-signature-compliant expression classes. + +These tests build expressions with a bare ``PostgresDialect`` (no DB +connection) and assert on the generated SQL, so they run without a server. +""" +import pytest + +from rhosocial.activerecord.backend.impl.postgres.expression.ilike import ( + ILIKEExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.type import ( + EnumTypeNameExpression, + EnumValuesExpression, + CreateEnumTypeExpression, + DropEnumTypeExpression, + AlterEnumAddValueExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.multirange import ( + CreateMultirangeTypeExpression, + MultirangeAggFunctionExpression, +) + + +class TestILIKEExpression: + """Tests for ILIKEExpression.""" + + def test_basic_ilike(self, postgres_dialect): + expr = ILIKEExpression(postgres_dialect, column="name", pattern="%foo%") + sql, params = expr.to_sql() + assert sql == '"name" ILIKE %s' + assert params == ('%foo%',) + + def test_negated_ilike(self, postgres_dialect): + expr = ILIKEExpression(postgres_dialect, column="name", pattern="%bar%", negate=True) + sql, params = expr.to_sql() + assert sql == '"name" NOT ILIKE %s' + assert params == ('%bar%',) + + def test_ilike_dispatches_to_postgres_format(self, postgres_dialect): + """Verify ILIKEExpression.to_sql() dispatches to format_ilike_expression.""" + expr = ILIKEExpression(postgres_dialect, column="col", pattern="pat") + sql, params = expr.to_sql() + assert "ILIKE" in sql + assert params == ("pat",) + + def test_ilike_generates_native_ilike_not_lower(self, postgres_dialect): + """Verify generated SQL uses native ILIKE, not LOWER() LIKE LOWER().""" + expr = ILIKEExpression(postgres_dialect, column="c", pattern="%x%") + sql, _ = expr.to_sql() + assert "LOWER" not in sql.upper() + assert "ILIKE" in sql + + +class TestEnumTypeNameExpression: + """Tests for EnumTypeNameExpression.""" + + def test_name_without_schema(self, postgres_dialect): + expr = EnumTypeNameExpression(postgres_dialect, name="status") + sql, params = expr.to_sql() + assert sql == "status" + assert params == () + + def test_name_with_schema(self, postgres_dialect): + expr = EnumTypeNameExpression(postgres_dialect, name="status", schema="app") + sql, params = expr.to_sql() + assert sql == "app.status" + assert params == () + + +class TestEnumValuesExpression: + """Tests for EnumValuesExpression.""" + + def test_single_value(self, postgres_dialect): + expr = EnumValuesExpression(postgres_dialect, values=["active"]) + sql, params = expr.to_sql() + assert sql == "'active'" + assert params == () + + def test_multiple_values(self, postgres_dialect): + expr = EnumValuesExpression(postgres_dialect, values=["a", "b", "c"]) + sql, params = expr.to_sql() + assert sql == "'a', 'b', 'c'" + assert params == () + + +class TestCreateEnumTypeExpression: + """Tests for CreateEnumTypeExpression.""" + + def test_basic_create(self, postgres_dialect): + expr = CreateEnumTypeExpression( + postgres_dialect, name="status", values=["active", "inactive"], + ) + sql, params = expr.to_sql() + assert sql == "CREATE TYPE status AS ENUM ('active', 'inactive')" + assert params == () + + def test_create_with_schema_and_if_not_exists(self, postgres_dialect): + expr = CreateEnumTypeExpression( + postgres_dialect, name="priority", values=["low", "high"], + schema="app", if_not_exists=True, + ) + sql, params = expr.to_sql() + assert sql == "CREATE TYPE IF NOT EXISTS app.priority AS ENUM ('low', 'high')" + assert params == () + + +class TestDropEnumTypeExpression: + """Tests for DropEnumTypeExpression.""" + + def test_basic_drop(self, postgres_dialect): + expr = DropEnumTypeExpression(postgres_dialect, name="status") + sql, params = expr.to_sql() + assert sql == "DROP TYPE status" + assert params == () + + def test_drop_with_if_exists_and_cascade(self, postgres_dialect): + expr = DropEnumTypeExpression( + postgres_dialect, name="status", schema="app", + if_exists=True, cascade=True, + ) + sql, params = expr.to_sql() + assert sql == "DROP TYPE IF EXISTS app.status CASCADE" + assert params == () + + +class TestAlterEnumAddValueExpression: + """Tests for AlterEnumAddValueExpression.""" + + def test_basic_add_value(self, postgres_dialect): + expr = AlterEnumAddValueExpression( + postgres_dialect, type_name="status", new_value="archived", + ) + sql, params = expr.to_sql() + assert sql == "ALTER TYPE status ADD VALUE 'archived'" + assert params == () + + def test_add_value_with_before_after(self, postgres_dialect): + expr = AlterEnumAddValueExpression( + postgres_dialect, type_name="status", new_value="pending", + schema="app", before="active", + ) + sql, params = expr.to_sql() + assert sql == "ALTER TYPE app.status ADD VALUE 'pending' BEFORE 'active'" + assert params == () + + +class TestCreateMultirangeTypeExpression: + """Tests for CreateMultirangeTypeExpression.""" + + def test_basic_create(self, postgres_dialect): + expr = CreateMultirangeTypeExpression( + postgres_dialect, name="my_multirange", range_type="my_range", + ) + sql, params = expr.to_sql() + assert sql == "CREATE TYPE my_multirange AS MULTIRANGE (my_range)" + assert params == () + + def test_create_with_schema(self, postgres_dialect): + expr = CreateMultirangeTypeExpression( + postgres_dialect, name="mr", range_type="int4range", schema="app", + ) + sql, params = expr.to_sql() + assert sql == "CREATE TYPE app.mr AS MULTIRANGE (int4range)" + assert params == () + + +class TestMultirangeAggFunctionExpression: + """Tests for MultirangeAggFunctionExpression.""" + + def test_basic_agg(self, postgres_dialect): + expr = MultirangeAggFunctionExpression( + postgres_dialect, range_column="period", table_name="events", + ) + sql, params = expr.to_sql() + assert sql == "SELECT multirange_agg(period) FROM events" + assert params == () + + def test_agg_with_where_and_schema(self, postgres_dialect): + expr = MultirangeAggFunctionExpression( + postgres_dialect, range_column="period", table_name="events", + where_clause="status = 'active'", schema="public", + ) + sql, params = expr.to_sql() + assert sql == "SELECT multirange_agg(period) FROM public.events WHERE status = 'active'" + assert params == ()