From 249a5cb33b6393a08b7dc89e28f447f2a630baa7 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 6 Oct 2026 17:30:07 +0800 Subject: [PATCH 1/8] refactor(postgres): carry catalogue objects instead of parsed names Core's expression and dialect layers were reworked: a relation is split into the object it names and the source that reads it, the namespace shape became a protocol the dialect states once, and to_sql() now inspects what answers a format lookup instead of calling whatever is callable. Qualified names are carried here as schema objects rather than parsed strings, which is what made the object and the statement that acts on it able to disagree about what "the same table" meant. Six types this backend does not have were being exported. COMMENT ON now renders from the object it annotates. The round-trip matrix this branch added was reporting nothing, which the refactor made visible rather than causing. Three classes it pinned as raising AttributeError name a format_method whose formatter PostgreSQL does not have, and core now reports a missing formatter as UnsupportedFeatureError -- the spelling every other capability probe in the tree already used. The pins move with it, and each now names the formatter it is missing so the entry says which gap it is. Walking PostgresDialect.__mro__ to resolve every format_* name to the class that defines it shows none of the 411 resolving to a Protocol: the stub format_* a protocol declares in the MRO are each overridden by a real mixin ahead of it. That includes CreateRoutineSupport and DropRoutineSupport, whose presence in the base list used to answer a dispatch with a protocol body returning None; the core CreateFunctionExpression and DropFunctionExpression now render. --- .claude/backend_development.md | 79 +- .github/workflows/test.yml | 16 +- .../protocol_support.md | 86 +- docs/zh_CN/ddl/materialized_view.md | 5 +- .../protocol_support.md | 84 +- .../backend/impl/postgres/cli/info.py | 18 +- .../backend/impl/postgres/dialect.py | 161 +- .../impl/postgres/enum_type_manager.py | 13 +- .../examples/connection/quickstart.py | 22 +- .../impl/postgres/examples/ddl/alter_table.py | 17 +- .../postgres/examples/ddl/create_index.py | 13 +- .../postgres/examples/ddl/create_table.py | 7 +- .../impl/postgres/examples/ddl/drop_table.py | 7 +- .../examples/ddl/materialized_view.py | 80 +- .../impl/postgres/examples/ddl/partition.py | 16 +- .../postgres/examples/ddl/unique_index.py | 22 +- .../impl/postgres/examples/ddl/view.py | 22 +- .../impl/postgres/examples/delete/basic.py | 14 +- .../postgres/examples/extensions/bloom.py | 25 +- .../postgres/examples/extensions/btree_gin.py | 19 +- .../examples/extensions/btree_gist.py | 19 +- .../postgres/examples/extensions/citext.py | 15 +- .../impl/postgres/examples/extensions/cube.py | 22 +- .../examples/extensions/earthdistance.py | 14 +- .../postgres/examples/extensions/hstore.py | 37 +- .../postgres/examples/extensions/hypopg.py | 9 +- .../postgres/examples/extensions/intarray.py | 22 +- .../postgres/examples/extensions/ltree.py | 22 +- .../postgres/examples/extensions/orafce.py | 23 +- .../examples/extensions/pg_partman.py | 7 +- .../examples/extensions/pg_stat_statements.py | 14 +- .../postgres/examples/extensions/pg_trgm.py | 23 +- .../postgres/examples/extensions/pgrouting.py | 9 +- .../postgres/examples/extensions/pgvector.py | 9 +- .../postgres/examples/extensions/postgis.py | 23 +- .../examples/extensions/postgis_raster.py | 14 +- .../postgres/examples/extensions/tablefunc.py | 13 +- .../impl/postgres/examples/insert/batch.py | 14 +- .../impl/postgres/examples/insert/single.py | 11 +- .../impl/postgres/examples/insert/upsert.py | 20 +- .../examples/insert/with_returning.py | 14 +- .../named_expressions/order_expressions.py | 18 +- .../examples/named_migrations/expressions.py | 13 +- .../pgq/recursive_cte_graph_traversal.py | 35 +- .../impl/postgres/examples/query/aggregate.py | 14 +- .../postgres/examples/query/array_func.py | 13 +- .../impl/postgres/examples/query/basic.py | 16 +- .../impl/postgres/examples/query/cte.py | 22 +- .../impl/postgres/examples/query/distinct.py | 15 +- .../impl/postgres/examples/query/explain.py | 18 +- .../impl/postgres/examples/query/join.py | 21 +- .../postgres/examples/query/pagination.py | 14 +- .../impl/postgres/examples/query/predicate.py | 20 +- .../impl/postgres/examples/query/subquery.py | 21 +- .../postgres/examples/query/text_search.py | 13 +- .../impl/postgres/examples/query/union.py | 15 +- .../impl/postgres/examples/query/window.py | 15 +- .../examples/query/window_functions.py | 4 +- .../examples/schema_diff/array_type_diff.py | 9 +- .../schema_diff/column_order_irrelevant.py | 9 +- .../examples/schema_diff/index_type_diff.py | 11 +- .../schema_diff/postgres10_identity.py | 9 +- .../schema_diff/serialization_roundtrip.py | 7 +- .../examples/schema_diff/table_changes.py | 12 +- .../examples/sqlxml/standard_expressions.py | 12 +- .../postgres/examples/transaction/basic.py | 24 +- .../examples/transaction/exclusive.py | 26 +- .../examples/transaction/for_update.py | 22 +- .../examples/types/array_postgres84.py | 13 +- .../postgres/examples/types/json_basic.py | 13 +- .../impl/postgres/examples/types/uuid.py | 14 +- .../impl/postgres/examples/update/basic.py | 13 +- .../impl/postgres/expression/ddl/comment.py | 41 +- .../impl/postgres/expression/ddl/domain.py | 14 +- .../expression/ddl/index_definition.py | 9 +- .../impl/postgres/expression/ddl/mv.py | 84 +- .../impl/postgres/expression/ddl/type.py | 27 +- .../backend/impl/postgres/functions/orafce.py | 52 +- .../impl/postgres/functions/pg_repack.py | 5 +- .../impl/postgres/functions/vector_search.py | 8 +- .../backend/impl/postgres/mixins/__init__.py | 8 + .../backend/impl/postgres/mixins/catalog.py | 24 + .../backend/impl/postgres/mixins/collation.py | 8 +- .../impl/postgres/mixins/ddl/cluster.py | 16 +- .../impl/postgres/mixins/ddl/collation.py | 18 +- .../impl/postgres/mixins/ddl/comment.py | 33 +- .../impl/postgres/mixins/ddl/domain.py | 50 +- .../impl/postgres/mixins/ddl/foreign_table.py | 16 +- .../backend/impl/postgres/mixins/ddl/index.py | 124 +- .../impl/postgres/mixins/ddl/partition.py | 71 +- .../impl/postgres/mixins/ddl/policy.py | 13 +- .../impl/postgres/mixins/ddl/rls_config.py | 15 +- .../impl/postgres/mixins/ddl/routine.py | 142 +- .../postgres/mixins/ddl/table_settings.py | 16 +- .../impl/postgres/mixins/ddl/trigger.py | 40 +- .../backend/impl/postgres/mixins/ddl/type.py | 46 +- .../impl/postgres/mixins/ddl_database.py | 43 +- .../backend/impl/postgres/mixins/ddl_table.py | 20 + .../backend/impl/postgres/mixins/ddl_view.py | 16 +- .../mixins/dml/extended_statistics.py | 20 +- .../postgres/mixins/dml/stored_procedure.py | 18 +- .../impl/postgres/mixins/dml/vacuum.py | 18 +- .../impl/postgres/mixins/extensions/ltree.py | 5 +- .../impl/postgres/mixins/extensions/orafce.py | 42 + .../postgres/mixins/extensions/pg_repack.py | 43 + .../postgres/mixins/extensions/pg_trgm.py | 5 +- .../postgres/mixins/extensions/pgvector.py | 5 +- .../postgres/mixins/extensions/postgis.py | 5 +- .../impl/postgres/mixins/materialized_view.py | 83 +- .../backend/impl/postgres/mixins/namespace.py | 111 ++ .../impl/postgres/mixins/object_name.py | 45 + .../backend/impl/postgres/mixins/schema.py | 22 +- .../backend/impl/postgres/mixins/truncate.py | 20 +- .../impl/postgres/mixins/types/enum.py | 23 +- .../impl/postgres/mixins/types/multirange.py | 5 +- .../impl/postgres/protocols/__init__.py | 3 + .../impl/postgres/protocols/catalog.py | 53 + .../impl/postgres/protocols/ddl/domain.py | 13 +- .../impl/postgres/protocols/ddl/index.py | 24 +- .../impl/postgres/protocols/ddl/trigger.py | 13 +- .../impl/postgres/protocols/ddl/type.py | 15 +- .../postgres/protocols/materialized_view.py | 8 +- .../backend/impl/postgres/protocols/schema.py | 16 +- .../backend/impl/postgres/protocols/table.py | 15 +- .../impl/postgres/type_values/__init__.py | 6 - tests/providers/fixtures/_common.py | 4 +- tests/providers/fixtures/basic.py | 39 +- tests/providers/fixtures/events.py | 5 +- tests/providers/fixtures/mixins.py | 9 +- tests/providers/fixtures/query.py | 41 +- tests/providers/partition.py | 5 +- .../dialect/test_alter_table_if_exists.py | 13 +- .../dialect/test_drop_table_cascade.py | 9 +- .../test_insert_on_conflict_clauses.py | 9 +- .../backend/dialect/test_materialized_view.py | 116 +- .../dialect/test_object_kind_guards.py | 288 ++++ .../test_property_graph_query_format.py | 127 +- .../backend/dialect/test_schema_support.py | 91 +- .../feature/backend/postgres/conftest.py | 10 +- .../backend/postgres/extensions/test_bloom.py | 9 +- .../extensions/test_bloom_integration.py | 50 +- .../postgres/extensions/test_btree_gin.py | 5 +- .../extensions/test_btree_gin_integration.py | 50 +- .../postgres/extensions/test_btree_gist.py | 9 +- .../extensions/test_btree_gist_integration.py | 50 +- .../extensions/test_citext_integration.py | 74 +- .../extensions/test_cube_integration.py | 50 +- .../test_earthdistance_integration.py | 17 +- .../test_fuzzystrmatch_integration.py | 30 +- .../extensions/test_hstore_integration.py | 74 +- .../extensions/test_hypopg_integration.py | 13 +- .../extensions/test_intarray_integration.py | 74 +- .../extensions/test_ltree_integration.py | 106 +- .../extensions/test_orafce_integration.py | 38 +- .../test_pg_stat_statements_integration.py | 6 +- .../extensions/test_pg_trgm_integration.py | 66 +- .../extensions/test_pgcrypto_integration.py | 38 +- .../extensions/test_pgrouting_integration.py | 13 +- .../extensions/test_pgvector_integration.py | 122 +- .../extensions/test_postgis_integration.py | 66 +- .../test_postgis_raster_integration.py | 22 +- .../extensions/test_tablefunc_integration.py | 25 +- .../extensions/test_uuid_ossp_integration.py | 34 +- .../backend/postgres/test_ddl_alter_config.py | 9 +- .../backend/postgres/test_ddl_expressions.py | 54 +- .../backend/postgres/test_ddl_objects.py | 42 +- .../backend/postgres/test_ddl_protocols.py | 27 +- .../postgres/test_ddl_source_collection.py | 5 +- .../backend/postgres/test_extensions.py | 4 +- .../feature/backend/postgres/test_index_pg.py | 75 +- .../test_materialized_view_integration.py | 44 +- .../backend/postgres/test_model_truncate.py | 9 +- .../backend/postgres/test_pg19_copy_repack.py | 4 +- .../test_pg19_copy_repack_integration.py | 7 +- .../test_property_graph_query_scenarios.py | 130 +- .../test_recursive_cte_graph_traversal.py | 71 +- .../backend/postgres/test_schema_namespace.py | 24 +- .../test_set_operation_and_truncate.py | 11 +- .../feature/backend/postgres/test_table.py | 3 +- .../feature/backend/postgres/test_trigger.py | 75 +- .../backend/postgres/test_view_execution.py | 87 +- .../backend/test_auto_increment_ddl.py | 7 +- .../feature/backend/test_create_table_like.py | 59 +- .../backend/test_expression_roundtrip_all.py | 1346 ++++++++++++++++- .../test_partition_advanced_operations.py | 26 +- .../feature/backend/test_partition_explain.py | 12 +- .../backend/test_partition_operations.py | 26 +- .../backend/test_postgres_database_ddl.py | 7 +- ...gres_datetime_interval_explain_examples.py | 10 +- .../backend/test_postgres_ddl_improvements.py | 2 +- .../backend/test_postgres_dialect_security.py | 13 +- .../test_postgres_protocol_conformance.py | 203 ++- 192 files changed, 5164 insertions(+), 2191 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/catalog.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/namespace.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/object_name.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/protocols/catalog.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_object_kind_guards.py diff --git a/.claude/backend_development.md b/.claude/backend_development.md index 9eb5e353..10323150 100644 --- a/.claude/backend_development.md +++ b/.claude/backend_development.md @@ -273,31 +273,80 @@ Every dialect inherits from: |----------|-------|-------------| | `WindowFunctionSupport` | `WindowFunctionMixin` | Window functions (OVER, PARTITION BY) | | `CTESupport` | `CTEMixin` | Common Table Expressions (WITH clause) | -| `AdvancedGroupingSupport` | `AdvancedGroupingMixin` | ROLLUP, CUBE, GROUPING SETS | -| `ReturningSupport` | `ReturningMixin` | RETURNING clause | +| `AdvancedGroupingSupport` | `DQLMixin` | ROLLUP, CUBE, GROUPING SETS | +| `ReturningSupport` | `DMLMixin` | RETURNING clause | | `UpsertSupport` | `UpsertMixin` | UPSERT operations (ON CONFLICT) | | `LateralJoinSupport` | `LateralJoinMixin` | LATERAL joins | | `ArraySupport` | `ArrayMixin` | Array types and operations | | `JSONSupport` | `JSONMixin` | JSON types and operations | | `ExplainSupport` | `ExplainMixin` | EXPLAIN statement | -| `FilterClauseSupport` | `FilterClauseMixin` | FILTER clause for aggregates | -| `OrderedSetAggregationSupport` | `OrderedSetAggregationMixin` | WITHIN GROUP (ORDER BY) | +| `FilterClauseSupport` | `ExpressionMixin` | FILTER clause for aggregates | +| `OrderedSetAggregationSupport` | `ExpressionMixin` | WITHIN GROUP (ORDER BY) | | `MergeSupport` | `MergeMixin` | MERGE statement | | `TemporalTableSupport` | `TemporalTableMixin` | FOR SYSTEM_TIME queries | -| `QualifyClauseSupport` | `QualifyClauseMixin` | QUALIFY clause | -| `LockingSupport` | `LockingMixin` | FOR UPDATE, SKIP LOCKED | +| `QualifyClauseSupport` | `DQLMixin` | QUALIFY clause | +| `LockingSupport` | `DQLMixin` | FOR UPDATE, SKIP LOCKED | | `GraphSupport` | `GraphMixin` | Graph queries (MATCH) | | `JoinSupport` | `JoinMixin` | JOIN operations | | `SetOperationSupport` | `SetOperationMixin` | UNION, INTERSECT, EXCEPT | | `ILIKESupport` | `ILIKEMixin` | Case-insensitive LIKE | -| `TableSupport` | `TableMixin` | CREATE/DROP/ALTER TABLE | -| `ViewSupport` | `ViewMixin` | CREATE/DROP VIEW | +| `CreateTableSupport` | `TableMixin` | CREATE TABLE | +| `DropTableSupport` | `TableMixin` | DROP TABLE | +| `AlterTableSupport` | `TableMixin` | ALTER TABLE | +| `CreateViewSupport` | `ViewMixin` | CREATE VIEW | +| `DropViewSupport` | `ViewMixin` | DROP VIEW | +| `MaterializedViewSupport` | `ViewMixin` | CREATE / DROP / REFRESH MATERIALIZED VIEW | | `TruncateSupport` | `TruncateMixin` | TRUNCATE TABLE | -| `SchemaSupport` | `SchemaMixin` | CREATE/DROP SCHEMA | -| `IndexSupport` | `IndexMixin` | CREATE/DROP INDEX | -| `SequenceSupport` | `SequenceMixin` | CREATE/DROP/ALTER SEQUENCE | -| `TriggerSupport` | `TriggerMixin` | CREATE/DROP TRIGGER (SQL:1999) | -| `FunctionSupport` | `FunctionMixin` | CREATE/DROP FUNCTION (SQL/PSM) | +| `CreateIndexSupport` | `IndexMixin` | CREATE INDEX | +| `DropIndexSupport` | `IndexMixin` | DROP INDEX | +| `FulltextIndexSupport` | `IndexMixin` | CREATE / DROP FULLTEXT INDEX | +| `CreateSequenceSupport` | `SequenceMixin` | CREATE SEQUENCE | +| `DropSequenceSupport` | `SequenceMixin` | DROP SEQUENCE | +| `AlterSequenceSupport` | `SequenceMixin` | ALTER SEQUENCE | +| `CreateTriggerSupport` | `TriggerMixin` | CREATE TRIGGER (SQL:1999) | +| `DropTriggerSupport` | `TriggerMixin` | DROP TRIGGER | +| `CreateRoutineSupport` | `FunctionMixin` | CREATE FUNCTION (SQL/PSM) | +| `DropRoutineSupport` | `FunctionMixin` | DROP FUNCTION | +| `CreateSchemaSupport` | `SchemaMixin` | CREATE SCHEMA | +| `DropSchemaSupport` | `SchemaMixin` | DROP SCHEMA | +| `CreateDatabaseSupport` | `DatabaseMixin` | CREATE DATABASE | +| `DropDatabaseSupport` | `DatabaseMixin` | DROP DATABASE | +| `AlterDatabaseSupport` | `DatabaseMixin` | ALTER DATABASE | + +##### Naming is its own protocol + +Naming was split out of the DDL protocols above, because "does the engine have +schemas" and "may a name be qualified with one" are different questions with +different owners and an engine can answer them differently. + +| Protocol | Mixin | Description | +|----------|-------|-------------| +| `NamespaceSupport` | `NamespaceMixin` | Whether a name may be qualified, and how it is spelled | +| `TableObjectSupport` | `TableNameMixin` | Render a `Table` as its name | +| `ViewObjectSupport` | `ViewNameMixin` | Render a `View` | +| `MaterializedViewObjectSupport` | `MaterializedViewNameMixin` | Render a `MaterializedView` | +| `ForeignTableObjectSupport` | `ForeignTableNameMixin` | Render a `ForeignTable` | +| `IndexObjectSupport` | `IndexNameMixin` | Render an `Index` | +| `SequenceObjectSupport` | `SequenceNameMixin` | Render a `Sequence` | +| `TriggerObjectSupport` | `TriggerNameMixin` | Render a `Trigger` | +| `TypeObjectSupport` | `TypeNameMixin` | Render a `Type` | +| `RoutineObjectSupport` | `FunctionNameMixin` / `ProcedureNameMixin` | Render a `Function` or `Procedure` | + +`NamespaceSupport` declares two halves that used to be one method: + +* `validate_namespace(expr)` -- the check. Returns `None` or raises + `UnsupportedFeatureError`. It is called while *rendering*, not while + constructing, because at construction the dialect may not be settled and the + object's slots may not be complete. +* `format_qualified_name(expr)` -- the spelling. Builds the qualified name and + joins the levels with `self.separator` (a class attribute, `"."` by default). + +A dialect's own answers go in **at most one** naming-side mixin, ` +NamespaceMixin`, holding only what differs from core's default. Backends that mix +in core's `NamespaceMixin` unchanged put it after the `*NameMixin` block in their +base list — those subclasses it, and a subclass precedes its base, so anything +listed first wins the MRO lookup. That ordering is load-bearing: getting it wrong +produces a dialect that answers a question from the wrong class, silently. ##### Principles for Adding New Protocols/Mixins @@ -315,8 +364,8 @@ Every dialect inherits from: | Feature | SQL Standard? | Location | |---------|---------------|----------| -| `CREATE TRIGGER` | Yes (SQL:1999) | Main Package (`TriggerSupport`) | -| `CREATE FUNCTION` | Yes (SQL/PSM) | Main Package (`FunctionSupport`) | +| `CREATE TRIGGER` | Yes (SQL:1999) | Main Package (`CreateTriggerSupport`) | +| `CREATE FUNCTION` | Yes (SQL/PSM) | Main Package (`CreateRoutineSupport`) | | `COMMENT ON` | No (PostgreSQL/Oracle) | PostgreSQL Extension | | `CREATE TYPE ... AS ENUM` | No (PostgreSQL-specific) | PostgreSQL Extension | | `AUTO_INCREMENT` | No (MySQL-specific) | MySQL Extension | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6818502..8933152a 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/expression-schema-object-refactor" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - 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/expression-schema-object-refactor" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - 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/expression-schema-object-refactor" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - name: Create postgres scenario config run: | @@ -489,8 +489,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/expression-schema-object-refactor" + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - name: Create postgres scenario config run: | diff --git a/docs/en_US/postgres_specific_features/protocol_support.md b/docs/en_US/postgres_specific_features/protocol_support.md index 7798dfdd..b4753a2b 100644 --- a/docs/en_US/postgres_specific_features/protocol_support.md +++ b/docs/en_US/postgres_specific_features/protocol_support.md @@ -33,14 +33,77 @@ The PostgreSQL dialect implements numerous protocols from the `rhosocial.activer ### DDL-Related Protocols +> The DDL protocols were split per statement: one protocol per statement +> expression. The umbrella names `TableSupport`, `ViewSupport`, `SchemaSupport`, +> `IndexSupport` and `SequenceSupport` no longer exist — use the rows below. + | Protocol | Support Status | Version Requirements | Notes | |----------|---------------|---------------------|-------| -| **TableSupport** | ✅ Full | All versions | CREATE TABLE, DROP TABLE, ALTER TABLE with all features | -| **ViewSupport** | ✅ Full | All versions | CREATE VIEW, DROP VIEW with all options | +| **CreateTableSupport** | ✅ Full | All versions | CREATE TABLE with all features | +| **DropTableSupport** | ✅ Full | All versions | DROP TABLE; IF EXISTS, CASCADE | +| **AlterTableSupport** | ✅ Full | All versions | ALTER TABLE and its actions | +| **CreateViewSupport** | ✅ Full | All versions | CREATE VIEW; OR REPLACE, TEMPORARY | +| **DropViewSupport** | ✅ Full | All versions | DROP VIEW; IF EXISTS, CASCADE | +| **MaterializedViewSupport** | ✅ Full | ≥ 9.3 | Materialized views; CONCURRENTLY / IF NOT EXISTS ≥ 9.4 | | **TruncateSupport** | ✅ Full | All versions | TRUNCATE TABLE; RESTART IDENTITY ≥ 8.4; CASCADE | -| **SchemaSupport** | ✅ Full | All versions | CREATE SCHEMA, DROP SCHEMA with all options | -| **IndexSupport** | ✅ Full | All versions | CREATE INDEX, DROP INDEX with all features | -| **SequenceSupport** | ✅ Full | All versions | CREATE SEQUENCE, DROP SEQUENCE | +| **CreateSchemaSupport** | ✅ Full | All versions | CREATE SCHEMA with all options | +| **DropSchemaSupport** | ✅ Full | All versions | DROP SCHEMA with all options | +| **CreateIndexSupport** | ✅ Full | All versions | CREATE INDEX; INCLUDE ≥ 11; NULLS NOT DISTINCT ≥ 15 | +| **DropIndexSupport** | ✅ Full | All versions | DROP INDEX | +| **FulltextIndexSupport** | ✅ Full | ≥ 9.6 | Full-text search as GIN + to_tsvector | +| **CreateSequenceSupport** | ✅ Full | All versions | CREATE SEQUENCE with all options | +| **DropSequenceSupport** | ✅ Full | All versions | DROP SEQUENCE | +| **AlterSequenceSupport** | ✅ Full | All versions | ALTER SEQUENCE | +| **CreateTriggerSupport** | ✅ Full | All versions | CREATE TRIGGER; EXECUTE FUNCTION | +| **DropTriggerSupport** | ✅ Full | All versions | DROP TRIGGER | +| **CreateTypeSupport** | ✅ Full | ≥ 9.6 | CREATE TYPE; COMPOSITE / RANGE / BASE / SHELL | +| **AlterTypeSupport** | ✅ Full | ≥ 9.6 | ALTER TYPE actions | +| **DropTypeSupport** | ✅ Full | ≥ 9.6 | DROP TYPE | +| **CreateDomainSupport** | ✅ Full | All versions | CREATE DOMAIN | +| **AlterDomainSupport** | ✅ Full | All versions | ALTER DOMAIN | +| **DropDomainSupport** | ✅ Full | All versions | DROP DOMAIN | +| **CreateRoutineSupport** | ✅ Full | ≥ 9.6 | CREATE FUNCTION | +| **DropRoutineSupport** | ✅ Full | ≥ 9.6 | DROP FUNCTION | +| **CreateDatabaseSupport** | ✅ Full | All versions | CREATE DATABASE with all options | +| **DropDatabaseSupport** | ✅ Full | All versions | DROP DATABASE; WITH (FORCE) ≥ 13 | +| **AlterDatabaseSupport** | ✅ Full | All versions | ALTER DATABASE | +| **CommentSupport** | ✅ Full | All versions | COMMENT ON | +| **ConstraintSupport** | ✅ Full | All versions | Constraint declaration and alteration | +| **PartitionSupport** | ✅ Full | ≥ 10.0 | Declarative partitioning | + +### Namespace Protocols (separate from DDL) + +Naming and DDL are two questions with two owners, so they have two sets of +protocols. "Does the engine have schemas" is answered by `CreateSchemaSupport` / +`DropSchemaSupport` above. "May a *name* be qualified" is answered by +`NamespaceSupport`. PostgreSQL answers both — which is exactly why they cannot be +one switch: an engine can offer `CREATE SCHEMA` and still refuse to qualify +names, and reading the wrong one gives no clue which case you are in. + +| Protocol | Support Status | Notes | +|----------|---------------|-------| +| **NamespaceSupport** | ✅ Full | PostgreSQL is two-level: an outer database and an inner schema, joined with `.` | +| **TableObjectSupport** | ✅ Full | Renders a `Table` | +| **ViewObjectSupport** | ✅ Full | Renders a `View` | +| **MaterializedViewObjectSupport** | ✅ Full | Renders a `MaterializedView` | +| **ForeignTableObjectSupport** | ✅ Full | Renders a `ForeignTable` | +| **IndexObjectSupport** | ✅ Full | Renders an `Index` | +| **SequenceObjectSupport** | ✅ Full | Renders a `Sequence` | +| **TriggerObjectSupport** | ✅ Full | Renders a `Trigger` | +| **TypeObjectSupport** | ✅ Full | Renders a `Type` | +| **RoutineObjectSupport** | ✅ Full | Renders a `Function` / `Procedure` | + +`NamespaceSupport` now separates two things that used to be one method: + +- `validate_namespace(expr)` -- the check. Returns `None` or raises + `UnsupportedFeatureError`. It is called while *rendering*, not while + constructing, because at construction the dialect may not be settled and the + object's slots may not be complete. +- `format_qualified_name(expr)` -- the spelling. Joins the levels with + `self.separator` (a class attribute, `"."` by default). + +To check ahead of time, use `isinstance(dialect, NamespaceSupport)`: these +protocols are `runtime_checkable`, so the runtime check still works. ## Detailed Feature Support by Protocol @@ -210,15 +273,15 @@ In addition to standard protocols, PostgreSQL provides database-specific protoco | Protocol | Description | Min Version | Documentation | |----------|-------------|-------------|---------------| | **PostgresPartitionSupport** | Advanced partitioning features | PG 10+ | [Partitioning Docs](https://www.postgresql.org/docs/current/ddl-partitioning.html) | -| **PostgresPropertyGraphQueryMixin** | SQL/PGQ compatibility gates | Explicit override | [SQL/PGQ Availability](property_graph_query.md) | +| `PostgresPropertyGraphQueryMixin` (a mixin, not a protocol) | SQL/PGQ compatibility gates | Explicit override | [SQL/PGQ Availability](property_graph_query.md) | | **PostgresPgPartmanSupport** | pg_partman auto-partitioning | PG 10+ | [Partitioning](partition.md) | -| **PostgresXMLMixin** | SQL/XML standard expressions | PG 8.3+ | [Dialect](dialect.md) | -| **PostgresCollationMixin** | Expression-level COLLATE | All | [Dialect](dialect.md) | +| **PostgresXMLSupport** | SQL/XML standard expressions | PG 8.3+ | [Dialect](dialect.md) | +| **PostgresCollationSupport** | Expression-level COLLATE | All | [Dialect](dialect.md) | | **PostgresIndexSupport** | Index enhancements | PG 10+ | [Index Docs](https://www.postgresql.org/docs/current/indexes.html) | | **PostgresVacuumSupport** | VACUUM improvements | PG 13+ | [VACUUM Docs](https://www.postgresql.org/docs/current/sql-vacuum.html) | | **PostgresQueryOptimizationSupport** | Query optimization features | PG 11+ | [Query Docs](https://www.postgresql.org/docs/current/runtime-config-query.html) | | **PostgresDataTypeSupport** | Data type enhancements | PG 11+ | [Data Types Docs](https://www.postgresql.org/docs/current/datatype.html) | -| **PostgresSQLSyntaxSupport** | SQL syntax enhancements | PG 12+ | [SQL Syntax Docs](https://www.postgresql.org/docs/current/sql-syntax.html) | +| **PostgresFeaturesSupport** | Generated columns, CTE SEARCH/CYCLE, FETCH WITH TIES | Generated columns PG 12+ | [SQL Syntax Docs](https://www.postgresql.org/docs/current/sql-syntax.html) | | **PostgresLogicalReplicationSupport** | Logical replication features | PG 10+ | [Replication Docs](https://www.postgresql.org/docs/current/logical-replication.html) | | **PostgresMaterializedViewSupport** | Materialized views (CREATE/REFRESH/ALTER/DROP, IF NOT EXISTS, CONCURRENTLY, TABLESPACE, storage parameters) | PG 9.3+ (CONCURRENTLY / IF NOT EXISTS: 9.4+) | [MV DDL Docs](https://www.postgresql.org/docs/current/sql-creatematerializedview.html) · [guide](../../ddl/materialized_view.md) | | **PostgresTableSupport** | Table-specific features | All | [Table Docs](https://www.postgresql.org/docs/current/ddl.html) | @@ -303,10 +366,13 @@ PostgreSQL index features beyond standard SQL: **Official Documentation**: https://www.postgresql.org/docs/current/datatype.html -### PostgresSQLSyntaxSupport +### PostgresFeaturesSupport **Feature Source**: Native support (no extension required) +SQL-syntax-level enhancements live here. They belong to no single DDL/DML +statement, which is why there is no umbrella protocol for them. + | Feature | Support | Version | Description | |---------|---------|---------|-------------| | `supports_generated_columns()` | ✅ | ≥ 12.0 | GENERATED ALWAYS AS columns | diff --git a/docs/zh_CN/ddl/materialized_view.md b/docs/zh_CN/ddl/materialized_view.md index 100fd1c1..daaf586e 100644 --- a/docs/zh_CN/ddl/materialized_view.md +++ b/docs/zh_CN/ddl/materialized_view.md @@ -23,7 +23,8 @@ ## 创建物化视图 ```python -from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateMaterializedViewExpression, @@ -36,7 +37,7 @@ summary = QueryExpression( FunctionCall(dialect, "COUNT", Column(dialect, "id")), FunctionCall(dialect, "SUM", Column(dialect, "amount")), ], - from_=TableExpression(dialect, "sales"), + from_=Table(dialect, "sales"), group_by_having=GroupByHavingClause(dialect, group_by=[Column(dialect, "product_id")]), ) diff --git a/docs/zh_CN/postgres_specific_features/protocol_support.md b/docs/zh_CN/postgres_specific_features/protocol_support.md index 8d24edc5..2c9fb43c 100644 --- a/docs/zh_CN/postgres_specific_features/protocol_support.md +++ b/docs/zh_CN/postgres_specific_features/protocol_support.md @@ -33,14 +33,75 @@ PostgreSQL 方言实现了 `rhosocial.activerecord.backend.dialect.protocols` ### DDL 相关协议 +> DDL 协议已按语句拆分:一条语句一个协议。旧文档里的 `TableSupport`、 +> `ViewSupport`、`SchemaSupport`、`IndexSupport`、`SequenceSupport` 这些伞形名字 +> 已经不存在,请按下表逐条对应。 + | 协议 | 支持状态 | 版本要求 | 备注 | |------|---------|---------|------| -| **TableSupport** | ✅ 完整 | 所有版本 | CREATE TABLE、DROP TABLE、ALTER TABLE 所有功能 | -| **ViewSupport** | ✅ 完整 | 所有版本 | CREATE VIEW、DROP VIEW 所有选项 | +| **CreateTableSupport** | ✅ 完整 | 所有版本 | CREATE TABLE 所有功能 | +| **DropTableSupport** | ✅ 完整 | 所有版本 | DROP TABLE;IF EXISTS、CASCADE | +| **AlterTableSupport** | ✅ 完整 | 所有版本 | ALTER TABLE 及其动作 | +| **CreateViewSupport** | ✅ 完整 | 所有版本 | CREATE VIEW;OR REPLACE、TEMPORARY | +| **DropViewSupport** | ✅ 完整 | 所有版本 | DROP VIEW;IF EXISTS、CASCADE | +| **MaterializedViewSupport** | ✅ 完整 | ≥ 9.3 | 物化视图;CONCURRENTLY / IF NOT EXISTS ≥ 9.4 | | **TruncateSupport** | ✅ 完整 | 所有版本 | TRUNCATE TABLE;RESTART IDENTITY ≥ 8.4;CASCADE | -| **SchemaSupport** | ✅ 完整 | 所有版本 | CREATE SCHEMA、DROP SCHEMA 所有选项 | -| **IndexSupport** | ✅ 完整 | 所有版本 | CREATE INDEX、DROP INDEX 所有功能 | -| **SequenceSupport** | ✅ 完整 | 所有版本 | CREATE SEQUENCE、DROP SEQUENCE | +| **CreateSchemaSupport** | ✅ 完整 | 所有版本 | CREATE SCHEMA 所有选项 | +| **DropSchemaSupport** | ✅ 完整 | 所有版本 | DROP SCHEMA 所有选项 | +| **CreateIndexSupport** | ✅ 完整 | 所有版本 | CREATE INDEX;INCLUDE ≥ 11;NULLS NOT DISTINCT ≥ 15 | +| **DropIndexSupport** | ✅ 完整 | 所有版本 | DROP INDEX | +| **FulltextIndexSupport** | ✅ 完整 | ≥ 9.6 | 以 GIN + to_tsvector 实现全文索引 | +| **CreateSequenceSupport** | ✅ 完整 | 所有版本 | CREATE SEQUENCE 所有选项 | +| **DropSequenceSupport** | ✅ 完整 | 所有版本 | DROP SEQUENCE | +| **AlterSequenceSupport** | ✅ 完整 | 所有版本 | ALTER SEQUENCE | +| **CreateTriggerSupport** | ✅ 完整 | 所有版本 | CREATE TRIGGER;EXECUTE FUNCTION | +| **DropTriggerSupport** | ✅ 完整 | 所有版本 | DROP TRIGGER | +| **CreateTypeSupport** | ✅ 完整 | ≥ 9.6 | CREATE TYPE;COMPOSITE / RANGE / BASE / SHELL | +| **AlterTypeSupport** | ✅ 完整 | ≥ 9.6 | ALTER TYPE 动作 | +| **DropTypeSupport** | ✅ 完整 | ≥ 9.6 | DROP TYPE | +| **CreateDomainSupport** | ✅ 完整 | 所有版本 | CREATE DOMAIN | +| **AlterDomainSupport** | ✅ 完整 | 所有版本 | ALTER DOMAIN | +| **DropDomainSupport** | ✅ 完整 | 所有版本 | DROP DOMAIN | +| **CreateRoutineSupport** | ✅ 完整 | ≥ 9.6 | CREATE FUNCTION | +| **DropRoutineSupport** | ✅ 完整 | ≥ 9.6 | DROP FUNCTION | +| **CreateDatabaseSupport** | ✅ 完整 | 所有版本 | CREATE DATABASE 所有选项 | +| **DropDatabaseSupport** | ✅ 完整 | 所有版本 | DROP DATABASE;WITH (FORCE) ≥ 13 | +| **AlterDatabaseSupport** | ✅ 完整 | 所有版本 | ALTER DATABASE | +| **CommentSupport** | ✅ 完整 | 所有版本 | COMMENT ON | +| **ConstraintSupport** | ✅ 完整 | 所有版本 | 约束声明与变更 | +| **PartitionSupport** | ✅ 完整 | ≥ 10.0 | 声明式分区 | + +### 命名空间协议(与 DDL 分开) + +命名与 DDL 是两件事,因此也有两套协议:「引擎有没有 schema」由上面的 +`CreateSchemaSupport` / `DropSchemaSupport` 回答;「名字**能不能**被限定」由 +`NamespaceSupport` 回答。PostgreSQL 两者都回答「可以」——正因为答案相同,它们才 +不能合成一个开关:一个引擎可以提供 `CREATE SCHEMA` 却拒绝限定名字,读错开关就 +得不到任何线索。 + +| 协议 | 支持状态 | 备注 | +|------|---------|------| +| **NamespaceSupport** | ✅ 完整 | PostgreSQL 是两级命名空间:外层 database、内层 schema,用 `.` 连接 | +| **TableObjectSupport** | ✅ 完整 | 渲染 `Table` | +| **ViewObjectSupport** | ✅ 完整 | 渲染 `View` | +| **MaterializedViewObjectSupport** | ✅ 完整 | 渲染 `MaterializedView` | +| **ForeignTableObjectSupport** | ✅ 完整 | 渲染 `ForeignTable` | +| **IndexObjectSupport** | ✅ 完整 | 渲染 `Index` | +| **SequenceObjectSupport** | ✅ 完整 | 渲染 `Sequence` | +| **TriggerObjectSupport** | ✅ 完整 | 渲染 `Trigger` | +| **TypeObjectSupport** | ✅ 完整 | 渲染 `Type` | +| **RoutineObjectSupport** | ✅ 完整 | 渲染 `Function` / `Procedure` | + +`NamespaceSupport` 现在把两件事分成两个方法: + +- `validate_namespace(expr)` —— 校验。返回 `None` 或抛 + `UnsupportedFeatureError`。它在**渲染时**调用而不是构造时,因为构造时方言 + 可能还没定下来,对象的槽位也可能还没填完。 +- `format_qualified_name(expr)` —— 拼写。用 `self.separator`(类属性,默认 `"."`) + 把各级连接起来。 + +想在代码里提前判断,请用 `isinstance(dialect, NamespaceSupport)`:这些协议都是 +`runtime_checkable` 的,运行时检查仍然有效。 ## PostgreSQL 专属协议 @@ -66,15 +127,15 @@ PostgreSQL 方言实现了 `rhosocial.activerecord.backend.dialect.protocols` | 协议 | 描述 | 最低版本 | 文档 | |------|------|----------|------| | **PostgresPartitionSupport** | 高级分区特性 | PG 10+ | [分区文档](https://www.postgresql.org/docs/current/ddl-partitioning.html) | -| **PostgresPropertyGraphQueryMixin** | SQL/PGQ 兼容性门控 | 显式覆盖 | [SQL/PGQ 可用性](property_graph_query.md) | +| `PostgresPropertyGraphQueryMixin`(mixin,非协议) | SQL/PGQ 兼容性门控 | 显式覆盖 | [SQL/PGQ 可用性](property_graph_query.md) | | **PostgresPgPartmanSupport** | pg_partman 自动分区 | PG 10+ | [分区文档](partition.md) | -| **PostgresXMLMixin** | SQL/XML 标准表达式 | PG 8.3+ | [Dialect 表达式](dialect.md) | -| **PostgresCollationMixin** | 表达式级 COLLATE | 所有版本 | [Dialect 表达式](dialect.md) | +| **PostgresXMLSupport** | SQL/XML 标准表达式 | PG 8.3+ | [Dialect 表达式](dialect.md) | +| **PostgresCollationSupport** | 表达式级 COLLATE | 所有版本 | [Dialect 表达式](dialect.md) | | **PostgresIndexSupport** | 索引增强 | PG 10+ | [索引文档](https://www.postgresql.org/docs/current/indexes.html) | | **PostgresVacuumSupport** | VACUUM 改进 | PG 13+ | [VACUUM 文档](https://www.postgresql.org/docs/current/sql-vacuum.html) | | **PostgresQueryOptimizationSupport** | 查询优化特性 | PG 11+ | [查询文档](https://www.postgresql.org/docs/current/runtime-config-query.html) | | **PostgresDataTypeSupport** | 数据类型增强 | PG 11+ | [数据类型文档](https://www.postgresql.org/docs/current/datatype.html) | -| **PostgresSQLSyntaxSupport** | SQL 语法增强 | PG 12+ | [SQL 语法文档](https://www.postgresql.org/docs/current/sql-syntax.html) | +| **PostgresFeaturesSupport** | 生成列、CTE SEARCH/CYCLE、FETCH WITH TIES | 生成列 PG 12+ | [SQL 语法文档](https://www.postgresql.org/docs/current/sql-syntax.html) | | **PostgresLogicalReplicationSupport** | 逻辑复制特性 | PG 10+ | [复制文档](https://www.postgresql.org/docs/current/logical-replication.html) | | **PostgresMaterializedViewSupport** | 物化视图(CREATE/REFRESH/ALTER/DROP、IF NOT EXISTS、CONCURRENTLY、TABLESPACE、存储参数) | PG 9.3+(CONCURRENTLY / IF NOT EXISTS:9.4+) | [物化视图 DDL 文档](https://www.postgresql.org/docs/current/sql-creatematerializedview.html) · [指南](../../ddl/materialized_view.md) | | **PostgresTableSupport** | 表特定特性 | 所有版本 | [表文档](https://www.postgresql.org/docs/current/ddl.html) | @@ -159,10 +220,13 @@ PostgreSQL 索引特性超越标准 SQL: **官方文档**:https://www.postgresql.org/docs/current/datatype.html -### PostgresSQLSyntaxSupport +### PostgresFeaturesSupport **特性来源**:原生支持(无需扩展) +SQL 语法层面的增强在这里——它们不属于任何一条具体的 DDL/DML 协议,所以没有各自 +的伞形协议。 + | 特性 | 支持 | 版本 | 描述 | |------|------|------|------| | `supports_generated_columns()` | ✅ | ≥ 12.0 | GENERATED ALWAYS AS 列 | diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py index 7a8e2c81..b820730f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py @@ -15,13 +15,13 @@ WindowFunctionSupport, CTESupport, FilterClauseSupport, ReturningSupport, UpsertSupport, LateralJoinSupport, JoinSupport, JSONSupport, ExplainSupport, GraphSupport, - SetOperationSupport, ViewSupport, - TableSupport, TruncateSupport, GeneratedColumnSupport, - TriggerSupport, FunctionSupport, + SetOperationSupport, ViewObjectSupport, + TableObjectSupport, TruncateSupport, GeneratedColumnSupport, + TriggerObjectSupport, RoutineObjectSupport, AdvancedGroupingSupport, ArraySupport, ILIKESupport, - IndexSupport, LockingSupport, MergeSupport, + IndexObjectSupport, LockingSupport, MergeSupport, OrderedSetAggregationSupport, QualifyClauseSupport, - SchemaSupport, SequenceSupport, TemporalTableSupport, + NamespaceSupport, SequenceObjectSupport, TemporalTableSupport, ) from rhosocial.activerecord.backend.impl.postgres.protocols import ( PostgresExtensionInfo, @@ -96,10 +96,10 @@ ], "Transaction & Locking": [LockingSupport, TemporalTableSupport], "Query Analysis": [ExplainSupport, GraphSupport, QualifyClauseSupport], - "DDL - Table": [TableSupport, TruncateSupport, GeneratedColumnSupport], - "DDL - View": [ViewSupport, PostgresMaterializedViewSupport], - "DDL - Schema & Index": [SchemaSupport, IndexSupport], - "DDL - Sequence & Trigger": [SequenceSupport, TriggerSupport, FunctionSupport], + "DDL - Table": [TableObjectSupport, TruncateSupport, GeneratedColumnSupport], + "DDL - View": [ViewObjectSupport, PostgresMaterializedViewSupport], + "DDL - Schema & Index": [NamespaceSupport, IndexObjectSupport], + "DDL - Sequence & Trigger": [SequenceObjectSupport, TriggerObjectSupport, RoutineObjectSupport], "String Matching": [ILIKESupport], "PostgreSQL Native": [ PostgresPartitionSupport, PostgresIndexSupport, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 92d049e8..b6cec6af 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -13,6 +13,25 @@ from rhosocial.activerecord.backend.dialect.base import SQLDialectBase from rhosocial.activerecord.backend.dialect.mixins import ( + # Named objects. Each *NameMixin supplies one ``format__object`` and + # subclasses NamespaceMixin, so each has to precede it in the base list. + NamespaceMixin, + TableNameMixin, + ViewNameMixin, + MaterializedViewNameMixin, + ForeignTableNameMixin, + IndexNameMixin, + SequenceNameMixin, + TriggerNameMixin, + FunctionNameMixin, + ProcedureNameMixin, + TypeNameMixin, + DomainNameMixin, + SynonymNameMixin, + SchemaNameMixin, + DatabaseNameMixin, + PropertyGraphNameMixin, + RelationSourceMixin, SQLXMLMixin, CollationMixin, CTEMixin, @@ -53,6 +72,43 @@ DomainMixin, ) from rhosocial.activerecord.backend.dialect.protocols import ( + AlterDatabaseSupport, + AlterDomainSupport, + AlterSequenceSupport, + AlterTableSupport, + AlterTypeSupport, + CreateDatabaseSupport, + CreateDomainSupport, + CreateIndexSupport, + CreateRoutineSupport, + CreateSchemaSupport, + CreateSequenceSupport, + CreateTableSupport, + CreateTriggerSupport, + CreateTypeSupport, + CreateViewSupport, + DropDatabaseSupport, + DropDomainSupport, + DropIndexSupport, + DropRoutineSupport, + DropSchemaSupport, + DropSequenceSupport, + DropTableSupport, + DropTriggerSupport, + DropTypeSupport, + DropViewSupport, + FulltextIndexSupport, + MaterializedViewObjectSupport, + MaterializedViewSupport, + NamespaceSupport, + RoutineObjectSupport, + SequenceObjectSupport, + TableObjectSupport, + TriggerObjectSupport, + TypeObjectSupport, + IndexObjectSupport, + ViewObjectSupport, + ForeignTableObjectSupport, SQLXMLSupport, SQLXMLParsingSupport, SQLXMLSerializationSupport, @@ -78,9 +134,6 @@ LateralJoinSupport, WildcardSupport, JoinSupport, - ViewSupport, - SchemaSupport, - SequenceSupport, SetOperationSupport, TruncateSupport, ILIKESupport, @@ -88,10 +141,9 @@ TransactionControlSupport, SQLFunctionSupport, DDLTypeSupport, - UserDefinedTypeSupport, - DomainSupport, ) from .mixins import ( + PostgresObjectNameMixin, PostgresExtensionMixin, PostgresMaterializedViewMixin, PostgresTableMixin, @@ -126,6 +178,7 @@ PostgresJoinMixin, PostgresTruncateMixin, PostgresSchemaMixin, + PostgresNamespaceMixin, PostgresDatabaseMixin, PostgresSequenceMixin, PostgresTransactionMixin, @@ -237,6 +290,7 @@ PostgresJoinSupport, PostgresTruncateSupport, PostgresSchemaSupport, + PostgresNamespaceSupport, PostgresSequenceSupport, PostgresTransactionSupport, PostgresViewSupport, @@ -301,12 +355,28 @@ class PostgresDialect( SQLDialectBase, PostgresTypeMixin, PostgresDomainMixin, - PostgresTypeSupport, - PostgresDomainSupport, UserDefinedTypeMixin, DomainMixin, - UserDefinedTypeSupport, - DomainSupport, + # Named objects. Each *NameMixin subclasses NamespaceMixin, so each has to + # precede it in this list (C3: a subclass precedes its base) or the base's + # format_qualified_name would shadow the per-kind formatter. + TableNameMixin, + ViewNameMixin, + MaterializedViewNameMixin, + ForeignTableNameMixin, + IndexNameMixin, + SequenceNameMixin, + TriggerNameMixin, + FunctionNameMixin, + ProcedureNameMixin, + TypeNameMixin, + DomainNameMixin, + SynonymNameMixin, + SchemaNameMixin, + DatabaseNameMixin, + PropertyGraphNameMixin, + RelationSourceMixin, + PostgresObjectNameMixin, # PG-specific mixins (before global mixins to override) PostgresDateTimeMixin, PostgresDQLMixin, @@ -329,6 +399,14 @@ class PostgresDialect( PostgresJoinMixin, PostgresTruncateMixin, PostgresSchemaMixin, + # PostgreSQL's naming side: which namespace levels exist (a database outside, + # a schema inside) and how they are spelled. Placed before NamespaceMixin + # because it subclasses it (C3: a subclass precedes its base) -- otherwise + # core's always-False defaults and core's two-slot spelling would win. After + # the *NameMixin block for the same reason: those subclass NamespaceMixin too, + # and each supplies one format__object. + PostgresNamespaceMixin, + NamespaceMixin, PostgresDatabaseMixin, PostgresSequenceMixin, PostgresTransactionMixin, @@ -478,9 +556,48 @@ class PostgresDialect( LateralJoinSupport, WildcardSupport, JoinSupport, - ViewSupport, - SchemaSupport, - SequenceSupport, + # Type and domain DDL protocols. These derive TypeObjectSupport, so they + # precede the object protocols further down. + PostgresTypeSupport, + PostgresDomainSupport, + # These three declare the PostgreSQL-only additions to the per-statement + # protocols below, so they derive Create/Drop/AlterTableSupport, + # Create/DropIndexSupport + FulltextIndexSupport and + # Create/DropTriggerSupport. A subclass precedes its base (C3), so each + # has to come before the core protocols it extends. + PostgresTableSupport, + PostgresIndexSupport, + PostgresTriggerSupport, + # Per-statement DDL protocols. Core split each former umbrella protocol + # into one protocol per statement expression, so a dialect that implements + # CREATE/DROP TABLE declares those two rather than the old umbrella. + CreateTableSupport, + DropTableSupport, + AlterTableSupport, + CreateViewSupport, + DropViewSupport, + MaterializedViewSupport, + CreateIndexSupport, + DropIndexSupport, + FulltextIndexSupport, + CreateSequenceSupport, + DropSequenceSupport, + AlterSequenceSupport, + CreateTriggerSupport, + DropTriggerSupport, + CreateTypeSupport, + AlterTypeSupport, + DropTypeSupport, + CreateDomainSupport, + AlterDomainSupport, + DropDomainSupport, + CreateSchemaSupport, + DropSchemaSupport, + CreateDatabaseSupport, + DropDatabaseSupport, + AlterDatabaseSupport, + CreateRoutineSupport, + DropRoutineSupport, # Introspection protocol IntrospectionSupport, # Transaction control protocol @@ -488,7 +605,6 @@ class PostgresDialect( # PostgreSQL-specific protocols PostgresExtensionSupport, PostgresMaterializedViewSupport, - PostgresTableSupport, PostgresPgvectorSupport, PostgresPostGISSupport, PostgresPostgisRasterSupport, @@ -497,7 +613,6 @@ class PostgresDialect( PostgresHstoreSupport, # Native feature protocols PostgresPartitionSupport, - PostgresIndexSupport, PostgresVacuumSupport, PostgresCopySupport, PostgresQueryOptimizationSupport, @@ -518,6 +633,7 @@ class PostgresDialect( PostgresJoinSupport, PostgresTruncateSupport, PostgresSchemaSupport, + PostgresNamespaceSupport, PostgresSequenceSupport, PostgresTransactionSupport, PostgresViewSupport, @@ -550,7 +666,6 @@ class PostgresDialect( PostgresOrafceSupport, PostgresAddressStandardizerSupport, # DDL feature protocols - PostgresTriggerSupport, PostgresCommentSupport, PostgresConstraintSupport, PostgresPolicySupport, @@ -569,6 +684,22 @@ class PostgresDialect( PostgresRangeTypeSupport, PostgresJSONBEnhancedSupport, PostgresArrayEnhancedSupport, + # Type and domain DDL protocols. These derive TypeObjectSupport / + # NamespaceSupport, so they precede the object protocols below. + # Named objects. Each derives NamespaceSupport, and PostgresTableSupport, + # PostgresIndexSupport and PostgresTriggerSupport derive from three of + # them, so the whole block sits after the PostgreSQL-specific protocols + # that restate them. + ViewObjectSupport, + MaterializedViewObjectSupport, + TableObjectSupport, + ForeignTableObjectSupport, + IndexObjectSupport, + SequenceObjectSupport, + TriggerObjectSupport, + TypeObjectSupport, + RoutineObjectSupport, + NamespaceSupport, # DataType Support Protocol DDLTypeSupport, # New feature protocols diff --git a/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py b/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py index 3efb5e3a..4f12e918 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py @@ -16,6 +16,7 @@ from typing import Any, Dict, List, Optional +from rhosocial.activerecord.backend.expression.objects import Type from rhosocial.activerecord.backend.expression.statements.ddl_type import ( AlterTypeExpression, CreateTypeExpression, @@ -64,12 +65,11 @@ def create_type(self, enum_type: PostgresEnumType, if_not_exists: bool = False) """ expr = CreateTypeExpression( dialect=self._backend.dialect, - type_name=enum_type.name, + type=Type(self._backend.dialect, enum_type.name, schema_name=enum_type.schema), definition=PostgresEnumTypeDefinition( self._backend.dialect, enum_type.values, ), - schema_name=enum_type.schema, if_not_exists=if_not_exists, ) sql, params = expr.to_sql() @@ -86,8 +86,7 @@ def drop_type(self, enum_type: PostgresEnumType, if_exists: bool = False, cascad """ expr = PostgresDropTypeExpression( dialect=self._backend.dialect, - type_name=enum_type.name, - schema_name=enum_type.schema, + type=Type(self._backend.dialect, enum_type.name, schema_name=enum_type.schema), if_exists=if_exists, cascade=cascade, ) @@ -121,9 +120,8 @@ def add_value( ) expr = AlterTypeExpression( dialect=self._backend.dialect, - type_name=enum_type.name, + type=Type(self._backend.dialect, enum_type.name, schema_name=enum_type.schema), actions=[action], - schema_name=enum_type.schema, ) sql, params = expr.to_sql() self._backend.execute(sql, params) @@ -149,9 +147,8 @@ def rename_value(self, enum_type: PostgresEnumType, old_value: str, new_value: s ) expr = AlterTypeExpression( dialect=self._backend.dialect, - type_name=enum_type.name, + type=Type(self._backend.dialect, enum_type.name, schema_name=enum_type.schema), actions=[action], - schema_name=enum_type.schema, ) sql, params = expr.to_sql() self._backend.execute(sql, params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py index fb4b1dcd..8c01e886 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py @@ -18,10 +18,10 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, ValuesSource, WhereClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Column, Literal from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.expression.statements import ( @@ -62,7 +62,7 @@ def execute_expression(expression, options=None): def create_demo_tables(): users_table = CreateTableExpression( dialect=dialect, - table_name='quickstart_users', + table=Table(dialect, 'quickstart_users'), columns=[ ColumnDefinition( 'id', @@ -84,7 +84,7 @@ def create_demo_tables(): logs_table = CreateTableExpression( dialect=dialect, - table_name='quickstart_logs', + table=Table(dialect, 'quickstart_logs'), columns=[ ColumnDefinition( 'id', @@ -107,7 +107,7 @@ def create_demo_tables(): def seed_demo_data(): insert_users = InsertExpression( dialect=dialect, - into='quickstart_users', + into=Table(dialect, 'quickstart_users'), columns=['name', 'status'], source=ValuesSource( dialect, @@ -140,7 +140,7 @@ def seed_demo_data(): Column(dialect, 'name'), Column(dialect, 'status'), ], - from_=TableExpression(dialect, 'quickstart_users'), + from_=Table(dialect, 'quickstart_users'), where=WhereClause( dialect, condition=ComparisonPredicate( @@ -174,7 +174,7 @@ def seed_demo_data(): Column(dialect, 'name'), Column(dialect, 'status'), ], - from_=TableExpression(dialect, 'quickstart_users'), + from_=Table(dialect, 'quickstart_users'), where=WhereClause( dialect, condition=ComparisonPredicate( @@ -199,7 +199,7 @@ def seed_demo_data(): with backend.transaction(): insert_log = InsertExpression( dialect=dialect, - into='quickstart_logs', + into=Table(dialect, 'quickstart_logs'), columns=['message'], source=ValuesSource(dialect, [[Literal(dialect, 'quickstart transaction')]]), ) @@ -208,7 +208,7 @@ def seed_demo_data(): logs_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'message')], - from_=TableExpression(dialect, 'quickstart_logs'), + from_=Table(dialect, 'quickstart_logs'), ) result = execute_expression(logs_query, dql_options) print(f"Transaction result: {result.data}") @@ -220,7 +220,7 @@ def seed_demo_data(): invalid_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id')], - from_=TableExpression(dialect, 'nonexistent_table'), + from_=Table(dialect, 'nonexistent_table'), ) execute_expression(invalid_query, dql_options) except Exception as error: @@ -229,10 +229,10 @@ def seed_demo_data(): # ============================================================ # SECTION: Disconnect # ============================================================ -drop_logs = DropTableExpression(dialect=dialect, table_name='quickstart_logs', if_exists=True) +drop_logs = DropTableExpression(dialect=dialect, table=Table(dialect, 'quickstart_logs'), if_exists=True) execute_expression(drop_logs, ddl_options) -drop_users = DropTableExpression(dialect=dialect, table_name='quickstart_users', if_exists=True) +drop_users = DropTableExpression(dialect=dialect, table=Table(dialect, 'quickstart_users'), if_exists=True) execute_expression(drop_users, ddl_options) backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py index 2cf85a23..5a3b8846 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py @@ -38,7 +38,7 @@ drop_table = DropTableExpression( dialect=dialect, - table='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -47,7 +47,7 @@ create_table = CreateTableExpression( dialect=dialect, - table='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition( 'id', @@ -64,7 +64,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name'], source=ValuesSource( dialect, @@ -96,7 +96,7 @@ add_email_expr = AlterTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), actions=[add_email_action], ) @@ -121,7 +121,7 @@ add_age_expr = AlterTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), actions=[add_age_action], ) @@ -138,7 +138,7 @@ rename_expr = AlterTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), actions=[rename_action], ) @@ -148,7 +148,8 @@ print("Column renamed successfully") # Verify table structure -from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression import QueryExpression +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Column from rhosocial.activerecord.backend.expression.query_parts import OrderByClause @@ -159,7 +160,7 @@ Column(dialect, 'data_type'), Column(dialect, 'is_nullable'), ], - from_=TableExpression(dialect, 'columns', schema_name='information_schema'), + from_=Table(dialect, 'columns', schema_name='information_schema'), where=Column(dialect, 'table_name') == Literal(dialect, 'users'), order_by=OrderByClause(dialect, [Column(dialect, 'ordinal_position')]), ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py index ac10af4b..79e782c9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py @@ -33,7 +33,7 @@ drop_table = DropTableExpression( dialect=dialect, - table='products', + table=Table(dialect, 'products'), if_exists=True, cascade=True, ) @@ -42,7 +42,7 @@ create_table = CreateTableExpression( dialect=dialect, - table='products', + table=Table(dialect, 'products'), columns=[ ColumnDefinition( 'id', @@ -66,8 +66,8 @@ create_idx = CreateIndexExpression( dialect=dialect, - index_name='idx_category_price', - table_name='products', + index=Index(dialect, 'idx_category_price'), + table=Table(dialect, 'products'), columns=['category', 'price'], if_not_exists=True, ) @@ -83,13 +83,14 @@ print("Index created: idx_category_price") # Verify index creation -from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression import QueryExpression +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import Column, Literal verify_expr = QueryExpression( dialect=dialect, select=[Column(dialect, 'indexname')], - from_=TableExpression(dialect, 'pg_indexes'), + from_=Table(dialect, 'pg_indexes'), where=Column(dialect, 'indexname') == Literal(dialect, 'idx_category_price'), ) options = ExecutionOptions(stmt_type=StatementType.DQL) 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 2256fe52..bb364804 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 @@ -15,6 +15,7 @@ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, @@ -41,7 +42,7 @@ dialect = backend.dialect # Drop if exists for clean setup -drop = DropTableExpression(dialect=dialect, table='products', if_exists=True) +drop = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), if_exists=True) sql, params = drop.to_sql() backend.execute(sql, params) @@ -103,7 +104,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table='products', + table=Table(dialect, 'products'), columns=columns, indexes=indexes, if_not_exists=True, @@ -128,7 +129,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_table = DropTableExpression(dialect=dialect, table='products', if_exists=True) +drop_table = DropTableExpression(dialect=dialect, table=Table(dialect, '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/ddl/drop_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py index bc611ee0..91e0270b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py @@ -31,7 +31,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'INT'), ], @@ -45,10 +45,11 @@ # SECTION: DROP TABLE # ============================================================ from rhosocial.activerecord.backend.expression import DropTableExpression +from rhosocial.activerecord.backend.expression.objects import Table drop_expr = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), ) sql, params = drop_expr.to_sql() print(f"DROP TABLE SQL: {sql}") @@ -58,7 +59,7 @@ # Already deleted drop_expr_exists = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, ) sql, params = drop_expr_exists.to_sql() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py index cbd91864..79702910 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py @@ -30,7 +30,6 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, CreateMaterializedViewExpression, DropMaterializedViewExpression, RefreshMaterializedViewExpression, @@ -38,6 +37,7 @@ GroupByHavingClause, FunctionCall, ) +from rhosocial.activerecord.backend.expression.objects import Index, MaterializedView, Table from rhosocial.activerecord.backend.expression.core import Literal, Column, WildcardExpression from rhosocial.activerecord.backend.expression.query_parts import OrderByClause from rhosocial.activerecord.backend.expression.statements import ( @@ -76,20 +76,20 @@ ddl_options = ExecutionOptions(stmt_type=StatementType.DDL) _drop_mv = DropMaterializedViewExpression( - dialect=dialect, view_name='sales_summary', if_exists=True, cascade=True, + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), if_exists=True, cascade=True, ) backend.execute(*_drop_mv.to_sql()) _drop_mv = DropMaterializedViewExpression( - dialect=dialect, view_name='sales_daily', if_exists=True, cascade=True, + dialect=dialect, view=MaterializedView(dialect, 'sales_daily'), if_exists=True, cascade=True, ) backend.execute(*_drop_mv.to_sql()) _drop_mv = DropMaterializedViewExpression( - dialect=dialect, view_name='sales_product_summary', if_exists=True, cascade=True, + dialect=dialect, view=MaterializedView(dialect, 'sales_product_summary'), if_exists=True, cascade=True, ) backend.execute(*_drop_mv.to_sql()) drop_table = DropTableExpression( dialect=dialect, - table='sales', + table=Table(dialect, 'sales'), if_exists=True, cascade=True, ) @@ -97,7 +97,7 @@ create_table = CreateTableExpression( dialect=dialect, - table='sales', + table=Table(dialect, 'sales'), columns=[ ColumnDefinition( dialect, @@ -125,7 +125,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='sales', + into=Table(dialect, 'sales'), columns=['product_id', 'amount', 'sale_date'], source=ValuesSource( dialect, @@ -155,7 +155,7 @@ FunctionCall(dialect, 'SUM', Column(dialect, 'amount')), FunctionCall(dialect, 'AVG', Column(dialect, 'amount')), ], - from_=TableExpression(dialect, 'sales'), + from_=Table(dialect, 'sales'), group_by_having=GroupByHavingClause( dialect, group_by=[Column(dialect, 'product_id')], @@ -164,7 +164,7 @@ create_mv = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), query=summary_query, column_aliases=['product_id', 'total_sales', 'total_amount', 'avg_amount'], # Storage parameter names are validated against PostgresStorageParameter; @@ -184,7 +184,7 @@ verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'sales_summary'), + from_=Table(dialect, 'sales_summary'), order_by=OrderByClause(dialect, [Column(dialect, 'product_id')]), ) sql, params = verify_query.to_sql() @@ -207,12 +207,12 @@ Column(dialect, 'amount'), Column(dialect, 'sale_date'), ], - from_=TableExpression(dialect, 'sales'), + from_=Table(dialect, 'sales'), ) mv_tablespace = CreateMaterializedViewExpression( dialect=dialect, - view_name='sales_on_fast_storage', + view=MaterializedView(dialect, 'sales_on_fast_storage'), query=sales_query, tablespace='fast_ssd', ) @@ -231,7 +231,7 @@ Column(dialect, 'sale_date'), FunctionCall(dialect, 'SUM', Column(dialect, 'amount')), ], - from_=TableExpression(dialect, 'sales'), + from_=Table(dialect, 'sales'), group_by_having=GroupByHavingClause( dialect, group_by=[Column(dialect, 'sale_date')], @@ -240,7 +240,7 @@ create_mv_nodata = CreateMaterializedViewExpression( dialect=dialect, - view_name='sales_daily', + view=MaterializedView(dialect, 'sales_daily'), query=daily_query, column_aliases=['sale_date', 'daily_total'], with_data=False, @@ -253,7 +253,7 @@ nodata_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'sales_daily'), + from_=Table(dialect, 'sales_daily'), ) sql, params = nodata_query.to_sql() try: @@ -270,7 +270,7 @@ # Insert new data so refresh has an effect insert_new = InsertExpression( dialect=dialect, - into='sales', + into=Table(dialect, 'sales'), columns=['product_id', 'amount', 'sale_date'], source=ValuesSource( dialect, @@ -283,7 +283,7 @@ # Refresh sales_summary refresh_mv = RefreshMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), ) sql, params = refresh_mv.to_sql() print(f"\nREFRESH SQL: {sql}") @@ -304,8 +304,8 @@ # Create a unique index prerequisite create_unique_idx = CreateIndexExpression( dialect=dialect, - index_name='sales_summary_product_id_idx', - table_name='sales_summary', + index=Index(dialect, 'sales_summary_product_id_idx'), + table=Table(dialect, 'sales_summary'), columns=['product_id'], unique=True, ) @@ -315,7 +315,7 @@ # Insert more data insert_more = InsertExpression( dialect=dialect, - into='sales', + into=Table(dialect, 'sales'), columns=['product_id', 'amount', 'sale_date'], source=ValuesSource( dialect, @@ -327,7 +327,7 @@ refresh_concurrent = RefreshMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), concurrent=True, ) sql, params = refresh_concurrent.to_sql() @@ -348,7 +348,7 @@ refresh_with_data = RefreshMaterializedViewExpression( dialect=dialect, - view_name='sales_daily', + view=MaterializedView(dialect, 'sales_daily'), with_data=True, ) sql, params = refresh_with_data.to_sql() @@ -372,17 +372,16 @@ # Refresh with schema (public is PG's default schema) pg_refresh = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name='sales_summary', - schema='public', + view=MaterializedView(dialect, 'sales_summary', schema_name='public'), ) sql, params = pg_refresh.to_sql() print(f"\nSchema-qualified REFRESH SQL: {sql}") backend.execute(sql, params) -# Also supports backward-compatible aliases: name=, concurrently= +# Also supports the backward-compatible concurrently= alias pg_refresh_c = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), concurrently=True, ) sql, params = pg_refresh_c.to_sql() @@ -395,7 +394,7 @@ drop_mv1 = DropMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), if_exists=True, cascade=True, ) @@ -405,7 +404,7 @@ drop_mv2 = DropMaterializedViewExpression( dialect=dialect, - view_name='sales_daily', + view=MaterializedView(dialect, 'sales_daily'), if_exists=True, ) sql, params = drop_mv2.to_sql() @@ -419,7 +418,7 @@ drop_noexist = DropMaterializedViewExpression( dialect=dialect, - view_name='nonexistent_mv', + view=MaterializedView(dialect, 'nonexistent_mv'), ) sql, params = drop_noexist.to_sql() print(f"DROP without IF EXISTS SQL (not executed): {sql}") @@ -434,7 +433,7 @@ if_not_exists_mv = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), query=summary_query, column_aliases=['product_id', 'total_sales', 'total_amount', 'avg_amount'], if_not_exists=True, @@ -453,9 +452,8 @@ schema_mv = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name='Order Summary', + view=MaterializedView(dialect, 'Order Summary', schema_name='mv_reporting'), query=summary_query, - schema='mv_reporting', column_aliases=['product_id', 'total_sales', 'total_amount', 'avg_amount'], ) sql, params = schema_mv.to_sql() @@ -465,7 +463,7 @@ schema_verify = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression( + from_=Table( dialect, 'Order Summary', schema_name='mv_reporting' ), ) @@ -485,7 +483,7 @@ alter_mv = PostgresAlterMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), actions=[ PostgresSetMaterializedViewPropertiesAction( dialect, {PostgresStorageParameter.FILLFACTOR: 85} @@ -501,7 +499,7 @@ rename_mv = PostgresAlterMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view=MaterializedView(dialect, 'sales_summary'), actions=[PostgresRenameMaterializedViewAction(dialect, 'sales_summary_v2')], ) sql, params = rename_mv.to_sql() @@ -511,8 +509,7 @@ # Move the schema-qualified view into the public schema. set_schema_mv = PostgresAlterMaterializedViewExpression( dialect=dialect, - view_name='Order Summary', - schema='mv_reporting', + view=MaterializedView(dialect, 'Order Summary', schema_name='mv_reporting'), actions=[PostgresSetMaterializedViewSchemaAction(dialect, 'public')], ) sql, params = set_schema_mv.to_sql() @@ -522,7 +519,7 @@ # CURRENT_USER / CURRENT_ROLE / SESSION_USER are emitted verbatim. owner_mv = PostgresAlterMaterializedViewExpression( dialect=dialect, - view_name='sales_summary_v2', + view=MaterializedView(dialect, 'sales_summary_v2'), actions=[PostgresChangeMaterializedViewOwnerAction(dialect, 'CURRENT_USER')], ) sql, params = owner_mv.to_sql() @@ -552,8 +549,7 @@ drop_schema_mv = PostgresDropMaterializedViewExpression( dialect=dialect, - view_name='Order Summary', - schema='mv_reporting', + view=MaterializedView(dialect, 'Order Summary', schema_name='mv_reporting'), if_exists=True, cascade=True, ) @@ -565,13 +561,13 @@ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ drop_mv = PostgresDropMaterializedViewExpression( - dialect=dialect, view_name='sales_summary_v2', if_exists=True, cascade=True, + dialect=dialect, view=MaterializedView(dialect, 'sales_summary_v2'), if_exists=True, cascade=True, ) backend.execute(*drop_mv.to_sql()) backend.execute('DROP SCHEMA IF EXISTS mv_reporting CASCADE', options=ddl_options) drop_table = DropTableExpression( dialect=dialect, - table='sales', + table=Table(dialect, 'sales'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py index 2944e802..e83727eb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py @@ -19,8 +19,8 @@ InsertExpression, ValuesSource, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, WildcardExpression, Column from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType @@ -40,11 +40,11 @@ # Drop partitions first, then parent table for partition in ['orders_2024_q1', 'orders_2024_q2', 'orders_2024_q3', 'orders_2024_q4']: - drop = DropTableExpression(dialect=dialect, table_name=partition, if_exists=True, cascade=True) + drop = DropTableExpression(dialect=dialect, table=Table(dialect, partition), if_exists=True, cascade=True) sql, params = drop.to_sql() backend.execute(sql, params) -drop_parent = DropTableExpression(dialect=dialect, table_name='orders', if_exists=True, cascade=True) +drop_parent = DropTableExpression(dialect=dialect, table=Table(dialect, 'orders'), if_exists=True, cascade=True) sql, params = drop_parent.to_sql() backend.execute(sql, params) @@ -88,7 +88,7 @@ # 3. Insert data (PostgreSQL routes to correct partition automatically) insert_expr = InsertExpression( dialect=dialect, - into='orders', + into=Table(dialect, 'orders'), columns=['order_date', 'customer_name', 'amount'], source=ValuesSource(dialect, [ [Literal(dialect, '2024-02-15'), Literal(dialect, 'Alice'), Literal(dialect, 150.00)], @@ -105,7 +105,7 @@ query_all = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'orders'), + from_=Table(dialect, 'orders'), ) sql, params = query_all.to_sql() result = backend.execute(sql, params, options=dql_options) @@ -115,7 +115,7 @@ query_q1 = QueryExpression( dialect=dialect, select=[Column(dialect, 'customer_name'), Column(dialect, 'amount')], - from_=TableExpression(dialect, 'orders_2024_q1'), + from_=Table(dialect, 'orders_2024_q1'), ) sql, params = query_q1.to_sql() result = backend.execute(sql, params, options=dql_options) @@ -125,11 +125,11 @@ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ for partition in ['orders_2024_q1', 'orders_2024_q2', 'orders_2024_q3', 'orders_2024_q4']: - drop = DropTableExpression(dialect=dialect, table_name=partition, if_exists=True, cascade=True) + drop = DropTableExpression(dialect=dialect, table=Table(dialect, partition), if_exists=True, cascade=True) sql, params = drop.to_sql() backend.execute(sql, params) -drop_parent = DropTableExpression(dialect=dialect, table_name='orders', if_exists=True, cascade=True) +drop_parent = DropTableExpression(dialect=dialect, table=Table(dialect, 'orders'), if_exists=True, cascade=True) sql, params = drop_parent.to_sql() backend.execute(sql, params) backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py index e342af32..91fa14a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py @@ -17,11 +17,11 @@ CreateTableExpression, DropTableExpression, QueryExpression, - TableExpression, InsertExpression, ValuesSource, CreateIndexExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import Literal, WildcardExpression from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, @@ -42,7 +42,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -51,7 +51,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'SERIAL', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -76,8 +76,8 @@ # 1. CREATE UNIQUE INDEX on a single column create_email_idx = CreateIndexExpression( dialect=dialect, - index_name='idx_users_email', - table_name='users', + index=Index(dialect, 'idx_users_email'), + table=Table(dialect, 'users'), columns=['email'], unique=True, if_not_exists=True, @@ -89,8 +89,8 @@ # 2. CREATE UNIQUE INDEX on multiple columns (composite unique) create_composite_idx = CreateIndexExpression( dialect=dialect, - index_name='idx_users_username_email', - table_name='users', + index=Index(dialect, 'idx_users_username_email'), + table=Table(dialect, 'users'), columns=['username', 'email'], unique=True, if_not_exists=True, @@ -115,7 +115,7 @@ # Insert initial row insert_expr = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['email', 'username'], source=ValuesSource(dialect, [ [Literal(dialect, 'alice@example.com'), Literal(dialect, 'alice')], @@ -129,7 +129,7 @@ try: duplicate_insert = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['email', 'username'], source=ValuesSource(dialect, [ [Literal(dialect, 'alice@example.com'), Literal(dialect, 'alice2')], @@ -145,7 +145,7 @@ verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), ) sql, params = verify_query.to_sql() result = backend.execute(sql, params) @@ -156,7 +156,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py index fc75c2d4..d35c3665 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py @@ -41,7 +41,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'INT'), ColumnDefinition('name', 'VARCHAR(100)'), @@ -54,7 +54,7 @@ insert = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['id', 'name'], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 'Alice')], @@ -69,22 +69,20 @@ # SECTION: CREATE VIEW # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - CreateViewExpression, - DropViewExpression, + QueryExpression, CreateViewExpression, DropViewExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table, View from rhosocial.activerecord.backend.expression.core import WildcardExpression query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name')], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), ) view_expr = CreateViewExpression( dialect=dialect, - view_name='user_names', + view=View(dialect, 'user_names'), query=query, ) sql, params = view_expr.to_sql() @@ -97,7 +95,7 @@ verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'user_names'), + from_=Table(dialect, 'user_names'), ) options_dql = ExecutionOptions(stmt_type=StatementType.DQL) sql, params = verify_query.to_sql() @@ -109,7 +107,7 @@ # ============================================================ view_expr_replace = CreateViewExpression( dialect=dialect, - view_name='user_names', + view=View(dialect, 'user_names'), query=query, replace=True, ) @@ -122,7 +120,7 @@ # ============================================================ drop_view = DropViewExpression( dialect=dialect, - view_name='user_names', + view=View(dialect, 'user_names'), ) sql, params = drop_view.to_sql() print(f"DROP VIEW SQL: {sql}") @@ -131,7 +129,7 @@ # ============================================================ # SECTION: Teardown # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name='users', if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), 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/delete/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py index 8d5b9ccd..30fa82bc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py @@ -20,9 +20,9 @@ ValuesSource, DeleteExpression, ReturningClause, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, @@ -44,7 +44,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -53,7 +53,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'SERIAL', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -72,7 +72,7 @@ # Insert sample data insert_expr = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name', 'email'], source=ValuesSource(dialect, [ [Literal(dialect, 'Alice'), Literal(dialect, 'alice@example.com')], @@ -90,7 +90,7 @@ # 1. Simple DELETE with WHERE delete_expr = DeleteExpression( dialect=dialect, - table=TableExpression(dialect, 'users'), + tables=Table(dialect, 'users'), where=ComparisonPredicate( dialect, '=', @@ -112,7 +112,7 @@ # 2. DELETE with RETURNING clause delete_returning = DeleteExpression( dialect=dialect, - table=TableExpression(dialect, 'users'), + tables=Table(dialect, 'users'), where=ComparisonPredicate( dialect, '=', @@ -133,7 +133,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py index 6b980e2d..f2217d0f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py @@ -34,7 +34,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="customers", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'customers'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -43,6 +43,7 @@ # ============================================================ from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCreateExtensionExpression, + PostgresCreateIndexExpression, ) from rhosocial.activerecord.backend.expression import ( CreateTableExpression, @@ -52,9 +53,9 @@ CreateIndexExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -98,7 +99,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="customers", + table=Table(dialect, 'customers'), columns=columns, if_not_exists=True, ) @@ -117,7 +118,7 @@ ] insert_expr = InsertExpression( dialect=dialect, - into="customers", + into=Table(dialect, 'customers'), columns=["name", "email", "city"], source=ValuesSource(dialect, rows), ) @@ -128,10 +129,10 @@ # Example 3: Create bloom index on multiple columns # Bloom index is useful for equality searches on multiple columns - create_idx = CreateIndexExpression( + create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_customers_bloom", - table_name="customers", + index=Index(dialect, 'idx_customers_bloom'), + table=Table(dialect, 'customers'), columns=["name", "email", "city"], index_type="BLOOM", if_not_exists=True, @@ -146,7 +147,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "email"), Column(dialect, "city")], - from_=TableExpression(dialect, "customers"), + from_=Table(dialect, "customers"), where=ComparisonPredicate( Column(dialect, "city"), "=", @@ -162,10 +163,10 @@ # Example 5: Create bloom index with custom options # length=64 (signature size in bits), col1=2 (number of bits for first column) - create_idx_opts = CreateIndexExpression( + create_idx_opts = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_customers_bloom_opts", - table_name="customers", + index=Index(dialect, 'idx_customers_bloom_opts'), + table=Table(dialect, 'customers'), columns=["name", "email"], index_type="BLOOM", if_not_exists=True, @@ -184,7 +185,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="customers", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'customers'), 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/extensions/btree_gin.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py index 3d7a38d4..38f2e614 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py @@ -33,7 +33,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="employees", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'employees'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -42,6 +42,7 @@ # ============================================================ from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCreateExtensionExpression, + PostgresCreateIndexExpression, ) from rhosocial.activerecord.backend.expression import ( CreateTableExpression, @@ -50,10 +51,10 @@ ColumnConstraintType, InsertExpression, QueryExpression, - TableExpression, Column, CreateIndexExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.options import ExecutionOptions @@ -95,7 +96,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="employees", + table=Table(dialect, 'employees'), columns=columns, if_not_exists=True, ) @@ -114,7 +115,7 @@ ] insert_expr = InsertExpression( dialect=dialect, - into="employees", + into=Table(dialect, 'employees'), columns=["name", "department"], source=ValuesSource(dialect, rows), ) @@ -125,10 +126,10 @@ # Example 3: Create GIN index with btree_gin operator class # btree_gin allows GIN indexes to support equality checks on scalar types - create_idx = CreateIndexExpression( + create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_employees_name_gin", - table_name="employees", + index=Index(dialect, 'idx_employees_name_gin'), + table=Table(dialect, 'employees'), columns=["name"], index_type="GIN", if_not_exists=True, @@ -145,7 +146,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "department")], - from_=TableExpression(dialect, "employees"), + from_=Table(dialect, "employees"), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -160,7 +161,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="employees", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'employees'), 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/extensions/btree_gist.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py index bec5324b..74a69d83 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py @@ -33,7 +33,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="events", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'events'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -42,6 +42,7 @@ # ============================================================ from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCreateExtensionExpression, + PostgresCreateIndexExpression, ) from rhosocial.activerecord.backend.expression import ( CreateTableExpression, @@ -50,10 +51,10 @@ ColumnConstraintType, InsertExpression, QueryExpression, - TableExpression, Column, CreateIndexExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -96,7 +97,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="events", + table=Table(dialect, 'events'), columns=columns, if_not_exists=True, ) @@ -114,7 +115,7 @@ ] insert_expr = InsertExpression( dialect=dialect, - into="events", + into=Table(dialect, 'events'), columns=["name", "created_at"], source=ValuesSource(dialect, rows), ) @@ -126,10 +127,10 @@ # Example 3: Create GiST index with btree_gist operator class # btree_gist allows GiST indexes to support B-tree operations on scalar types, # which is useful for exclusion constraints and range queries - create_idx = CreateIndexExpression( + create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_events_created_at_gist", - table_name="events", + index=Index(dialect, 'idx_events_created_at_gist'), + table=Table(dialect, 'events'), columns=["created_at"], index_type="GIST", if_not_exists=True, @@ -147,7 +148,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "created_at")], - from_=TableExpression(dialect, "events"), + from_=Table(dialect, "events"), where=ComparisonPredicate( dialect, ">", @@ -168,7 +169,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="events", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'events'), 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/extensions/citext.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py index 93c9fcaa..1bd4d9af 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py @@ -32,7 +32,7 @@ # Clean up for demo using DropTableExpression from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="users", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -56,10 +56,9 @@ ValuesSource, ) from rhosocial.activerecord.backend.expression import ( - Column, - QueryExpression, - TableExpression, + Column, QueryExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table # Check if citext extension is available available = dialect.is_extension_available("citext") @@ -106,7 +105,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="users", + table=Table(dialect, 'users'), columns=columns, if_not_exists=True, ) @@ -122,7 +121,7 @@ # Example 2: Insert with case-insensitive data insert_expr = InsertExpression( dialect=dialect, - into="users", + into=Table(dialect, 'users'), columns=["email", "username"], source=ValuesSource( dialect, @@ -145,7 +144,7 @@ Column(dialect, "id"), Column(dialect, "email"), ], - from_=TableExpression(dialect, "users"), + from_=Table(dialect, "users"), where=Column(dialect, "email", table="users") == Literal(dialect, "test@example.com"), ) @@ -166,7 +165,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="users", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), 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/extensions/cube.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py index 8ee6f04f..b0af80fb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py @@ -34,7 +34,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="products", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -51,11 +51,11 @@ ColumnConstraintType, CreateIndexExpression, QueryExpression, - TableExpression, Column, OrderByClause, LimitOffsetClause, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import Literal, Subquery from rhosocial.activerecord.backend.expression.operators import ( BinaryExpression, @@ -117,7 +117,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="products", + table=Table(dialect, 'products'), columns=columns, if_not_exists=True, ) @@ -131,7 +131,7 @@ # Ranges: '(x1,x2),(y1,y2)' represents a hypercube insert_expr = InsertExpression( dialect=dialect, - into="products", + into=Table(dialect, 'products'), columns=["name", "feature_vector"], source=ValuesSource( dialect, @@ -158,7 +158,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "feature_vector")], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), where=BinaryExpression( dialect, "@>", Literal(dialect, "(0.1,0.2,0.0),(0.5,0.6,0.3)").cast("cube"), @@ -177,7 +177,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "feature_vector")], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), where=BinaryExpression( dialect, "<@", Column(dialect, "feature_vector"), @@ -229,7 +229,7 @@ Column(dialect, "feature_vector"), dist_for_select, ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), where=ComparisonPredicate( dialect, "<", dist_for_where, @@ -268,7 +268,7 @@ Column(dialect, "feature_vector"), nn_dist_for_select, ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), order_by=OrderByClause(dialect, expressions=[nn_dist_for_order]), limit_offset=LimitOffsetClause(dialect, limit=3), ) @@ -282,8 +282,8 @@ # Example 7: Create GiST index for fast cube queries create_idx = CreateIndexExpression( dialect=dialect, - index_name="idx_products_feature_vector", - table_name="products", + index=Index(dialect, 'idx_products_feature_vector'), + table=Table(dialect, 'products'), columns=["feature_vector"], index_type="GIST", if_not_exists=True, @@ -301,7 +301,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="products", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), 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/extensions/earthdistance.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py index db9d472e..d5be94e0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py @@ -33,7 +33,7 @@ # Clean up for demo using DropTableExpression from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="cities", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'cities'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -50,8 +50,8 @@ ColumnConstraintType, QueryExpression, Column, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import FunctionCall, Literal, Subquery from rhosocial.activerecord.backend.expression.operators import BinaryExpression from rhosocial.activerecord.backend.expression.query_parts import OrderByClause @@ -139,7 +139,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="cities", + table=Table(dialect, 'cities'), columns=columns, if_not_exists=True, ) @@ -151,7 +151,7 @@ # Example 2: Insert city coordinate data insert_expr = InsertExpression( dialect=dialect, - into="cities", + into=Table(dialect, 'cities'), columns=["name", "latitude", "longitude"], source=ValuesSource( dialect, @@ -230,7 +230,7 @@ Column(dialect, "name"), distance_km_aliased, ], - from_=TableExpression(dialect, "cities"), + from_=Table(dialect, "cities"), where=contains_pred, order_by=OrderByClause( dialect, @@ -273,7 +273,7 @@ Column(dialect, "name"), round_distance.as_("distance_km"), ], - from_=TableExpression(dialect, "cities"), + from_=Table(dialect, "cities"), where=within_radius_pred, order_by=OrderByClause( dialect, @@ -296,7 +296,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="cities", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'cities'), 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/extensions/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py index 8115c7a7..ee9b0f21 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py @@ -35,7 +35,7 @@ # Clean up using DropTableExpression from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="products", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -55,13 +55,10 @@ QueryExpression, UpdateExpression, ) -from rhosocial.activerecord.backend.expression.core import ( - Column, - Literal, - FunctionCall, - Subquery, - TableExpression, +from rhosocial.activerecord.backend.expression import ( + Column, Literal, FunctionCall, Subquery, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.operators import ( BinaryExpression, ) @@ -112,7 +109,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="products", + table=Table(dialect, 'products'), columns=columns, if_not_exists=True, ) @@ -124,7 +121,7 @@ # Use Literal with .cast("hstore") for type-safe hstore insertion insert_expr = InsertExpression( dialect=dialect, - into="products", + into=Table(dialect, 'products'), columns=["name", "attributes"], source=ValuesSource( dialect, @@ -164,7 +161,7 @@ Literal(dialect, "color"), )).as_("color"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), ) sql, params = query.to_sql() print("\n--- Get value by key (-> operator) ---") @@ -178,7 +175,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "attributes")], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), where=BinaryExpression( dialect, "?", Column(dialect, "attributes"), @@ -197,7 +194,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "attributes")], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), where=BinaryExpression( dialect, "@>", Column(dialect, "attributes"), @@ -222,7 +219,7 @@ Literal(dialect, "brand=>Acme").cast("hstore"), )).as_("with_brand"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), ) sql, params = query.to_sql() print("\n--- Concatenation operator (||) ---") @@ -241,7 +238,7 @@ Column(dialect, "attributes"), ).as_("keys"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), ) sql, params = query.to_sql() print("\n--- Get all keys (akeys function) ---") @@ -260,7 +257,7 @@ Column(dialect, "attributes"), ).as_("values"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), ) sql, params = query.to_sql() print("\n--- Get all values (avals function) ---") @@ -279,7 +276,7 @@ Column(dialect, "attributes"), ).as_("key"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), ) sql, params = query.to_sql() print("\n--- Get keys as set (skeys function) ---") @@ -299,7 +296,7 @@ Literal(dialect, "weight"), ).as_("without_weight"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), ) sql, params = query.to_sql() print("\n--- Delete key (delete function) ---") @@ -313,7 +310,7 @@ dml_opts = ExecutionOptions(stmt_type=StatementType.DML) update_expr = UpdateExpression( dialect=dialect, - table="products", + table=Table(dialect, 'products'), assignments={ "attributes": BinaryExpression( dialect, "||", @@ -349,7 +346,7 @@ Literal(dialect, "discount"), )).as_("discount"), ], - from_=TableExpression(dialect, "products"), + from_=Table(dialect, "products"), where=ComparisonPredicate( dialect, "=", BinaryExpression( @@ -371,7 +368,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="products", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), 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/extensions/hypopg.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py index 829f6301..12617bb5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py @@ -16,6 +16,7 @@ # ============================================================ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) @@ -35,7 +36,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="products", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -101,7 +102,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="products", + table=Table(dialect, 'products'), columns=columns, if_not_exists=True, ) @@ -120,7 +121,7 @@ ] insert_expr = InsertExpression( dialect=dialect, - into="products", + into=Table(dialect, 'products'), columns=["name", "category", "price"], source=ValuesSource(dialect, rows), ) @@ -191,7 +192,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="products", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'products'), 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/extensions/intarray.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py index 63dadc7c..799db067 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py @@ -34,7 +34,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="tags", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'tags'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -51,10 +51,10 @@ ColumnConstraintType, CreateIndexExpression, QueryExpression, - TableExpression, Column, OrderByClause, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.operators import ( BinaryExpression, @@ -114,7 +114,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="tags", + table=Table(dialect, 'tags'), columns=columns, if_not_exists=True, ) @@ -126,7 +126,7 @@ # Example 2: Insert integer array data insert_expr = InsertExpression( dialect=dialect, - into="tags", + into=Table(dialect, 'tags'), columns=["name", "tag_ids"], source=ValuesSource( dialect, @@ -152,7 +152,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "tag_ids")], - from_=TableExpression(dialect, "tags"), + from_=Table(dialect, "tags"), where=BinaryExpression( dialect, "@>", Column(dialect, "tag_ids"), @@ -171,7 +171,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "tag_ids")], - from_=TableExpression(dialect, "tags"), + from_=Table(dialect, "tags"), where=BinaryExpression( dialect, "&&", Column(dialect, "tag_ids"), @@ -192,7 +192,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), idx_func], - from_=TableExpression(dialect, "tags"), + from_=Table(dialect, "tags"), order_by=OrderByClause(dialect, expressions=[Column(dialect, "name")]), ) sql, params = query.to_sql() @@ -216,7 +216,7 @@ sort_func, unique_sorted_func, ], - from_=TableExpression(dialect, "tags"), + from_=Table(dialect, "tags"), where=Column(dialect, "id") == Literal(dialect, 4), ) sql, params = query.to_sql() @@ -229,8 +229,8 @@ # gist__int_ops operator class enables fast @>, &&, = queries create_idx = CreateIndexExpression( dialect=dialect, - index_name="idx_tags_tag_ids", - table_name="tags", + index=Index(dialect, 'idx_tags_tag_ids'), + table=Table(dialect, 'tags'), columns=["tag_ids"], index_type="GIST", if_not_exists=True, @@ -248,7 +248,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="tags", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'tags'), 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/extensions/ltree.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py index d3bace83..95fe69ef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py @@ -35,7 +35,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="categories", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'categories'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -52,10 +52,10 @@ ColumnConstraintType, CreateIndexExpression, QueryExpression, - TableExpression, Column, OrderByClause, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.operators import BinaryExpression from rhosocial.activerecord.backend.expression.statements.dml import ( @@ -116,7 +116,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="categories", + table=Table(dialect, 'categories'), columns=columns, if_not_exists=True, ) @@ -129,7 +129,7 @@ # ltree paths use dot-separated labels (only alphanumeric and underscore) insert_expr = InsertExpression( dialect=dialect, - into="categories", + into=Table(dialect, 'categories'), columns=["name", "path"], source=ValuesSource( dialect, @@ -159,7 +159,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "path")], - from_=TableExpression(dialect, "categories"), + from_=Table(dialect, "categories"), where=BinaryExpression( dialect, "@>", Literal(dialect, "electronics.computers"), @@ -179,7 +179,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "path")], - from_=TableExpression(dialect, "categories"), + from_=Table(dialect, "categories"), where=BinaryExpression( dialect, "<@", Column(dialect, "path"), @@ -200,7 +200,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "path")], - from_=TableExpression(dialect, "categories"), + from_=Table(dialect, "categories"), where=BinaryExpression( dialect, "~", Column(dialect, "path"), @@ -219,7 +219,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "path")], - from_=TableExpression(dialect, "categories"), + from_=Table(dialect, "categories"), where=BinaryExpression( dialect, "~", Column(dialect, "path"), @@ -237,8 +237,8 @@ # Example 7: Create GiST index for fast path queries create_idx = CreateIndexExpression( dialect=dialect, - index_name="idx_categories_path", - table_name="categories", + index=Index(dialect, 'idx_categories_path'), + table=Table(dialect, 'categories'), columns=["path"], index_type="GIST", if_not_exists=True, @@ -256,7 +256,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="categories", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'categories'), 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/extensions/orafce.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py index 1b823337..ab81fdcb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py @@ -33,7 +33,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="employees", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'employees'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -57,10 +57,9 @@ ValuesSource, ) from rhosocial.activerecord.backend.expression import ( - Column, - QueryExpression, - TableExpression, + Column, QueryExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType from rhosocial.activerecord.backend.impl.postgres.functions.orafce import ( @@ -110,7 +109,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="employees", + table=Table(dialect, 'employees'), columns=columns, if_not_exists=True, ) @@ -122,7 +121,7 @@ # Example 2: Insert data with NULL values insert_expr = InsertExpression( dialect=dialect, - into="employees", + into=Table(dialect, 'employees'), columns=["name", "hire_date", "bonus", "description"], source=ValuesSource( dialect, @@ -166,7 +165,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), add_months_expr], - from_=TableExpression(dialect, "employees"), + from_=Table(dialect, "employees"), order_by=[Column(dialect, "id")], ) sql, params = query.to_sql() @@ -184,7 +183,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), last_day_expr], - from_=TableExpression(dialect, "employees"), + from_=Table(dialect, "employees"), order_by=[Column(dialect, "id")], ) sql, params = query.to_sql() @@ -203,7 +202,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), nvl_expr], - from_=TableExpression(dialect, "employees"), + from_=Table(dialect, "employees"), order_by=[Column(dialect, "id")], ) sql, params = query.to_sql() @@ -226,7 +225,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), decode_expr], - from_=TableExpression(dialect, "employees"), + from_=Table(dialect, "employees"), order_by=[Column(dialect, "id")], ) sql, params = query.to_sql() @@ -249,7 +248,7 @@ Column(dialect, "description"), instr_expr, ], - from_=TableExpression(dialect, "employees"), + from_=Table(dialect, "employees"), order_by=[Column(dialect, "id")], ) sql, params = query.to_sql() @@ -265,7 +264,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="employees", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'employees'), 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/extensions/pg_partman.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py index 65ab4f7e..3f6a493f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py @@ -20,6 +20,7 @@ # ============================================================ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) @@ -39,7 +40,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="log_entries", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'log_entries'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -100,7 +101,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="log_entries", + table=Table(dialect, 'log_entries'), columns=columns, if_not_exists=True, ) @@ -153,7 +154,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="log_entries", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'log_entries'), 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/extensions/pg_stat_statements.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py index f7817f42..098cfa9f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py @@ -47,9 +47,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.query_parts import OrderByClause @@ -95,7 +95,7 @@ Column(dialect, "total_exec_time"), Column(dialect, "mean_exec_time"), ], - from_=TableExpression(dialect, "pg_stat_statements"), + from_=Table(dialect, "pg_stat_statements"), order_by=OrderByClause(dialect, [Column(dialect, "total_exec_time")], descending=True), ) sql, params = query.to_sql() @@ -140,7 +140,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="stats_demo", + table=Table(dialect, 'stats_demo'), columns=columns, if_not_exists=True, ) @@ -153,7 +153,7 @@ rows = [[Literal(dialect, f"item{i}"), Literal(dialect, i * 10)] for i in range(5)] insert_expr = InsertExpression( dialect=dialect, - into="stats_demo", + into=Table(dialect, 'stats_demo'), columns=["name", "value"], source=ValuesSource(dialect, rows), ) @@ -164,7 +164,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "value")], - from_=TableExpression(dialect, "stats_demo"), + from_=Table(dialect, "stats_demo"), where=ComparisonPredicate( Column(dialect, "value"), ">", @@ -185,7 +185,7 @@ Column(dialect, "query"), Column(dialect, "calls"), ], - from_=TableExpression(dialect, "pg_stat_statements"), + from_=Table(dialect, "pg_stat_statements"), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -195,7 +195,7 @@ print(f"Could not check statistics: {e}") # Cleanup demo table - drop_expr = DropTableExpression(dialect=dialect, table_name="stats_demo", if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'stats_demo'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py index 06c9eb2a..7cd95eb5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py @@ -36,6 +36,7 @@ # ============================================================ from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCreateExtensionExpression, + PostgresCreateIndexExpression, ) from rhosocial.activerecord.backend.expression import ( CreateTableExpression, @@ -46,12 +47,12 @@ DropTableExpression, QueryExpression, Column, - TableExpression, FunctionCall, BinaryExpression, OrderByClause, Literal, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements.dml import ( InsertExpression, ) @@ -64,7 +65,7 @@ # Clean up for demo drop_expr = DropTableExpression( dialect=dialect, - table_name="articles", + table=Table(dialect, 'articles'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -112,7 +113,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="articles", + table=Table(dialect, 'articles'), columns=columns, if_not_exists=True, ) @@ -124,7 +125,7 @@ # Example 2: Insert text data insert_expr = InsertExpression( dialect=dialect, - into="articles", + into=Table(dialect, 'articles'), columns=["title"], source=ValuesSource( dialect, @@ -165,7 +166,7 @@ Column(dialect, "title"), sim_select, ], - from_=TableExpression(dialect, "articles"), + from_=Table(dialect, "articles"), where=sim_where > Literal(dialect, 0.1), order_by=OrderByClause(dialect, [(sim_order, "DESC")]), ) @@ -183,7 +184,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, "title")], - from_=TableExpression(dialect, "articles"), + from_=Table(dialect, "articles"), where=BinaryExpression( dialect, "%", Column(dialect, "title"), Literal(dialect, "Postgres guide") ), @@ -214,7 +215,7 @@ sim_pg_select, sim_design_select, ], - from_=TableExpression(dialect, "articles"), + from_=Table(dialect, "articles"), order_by=OrderByClause(dialect, [(sim_pg_order, "DESC")]), ) sql, params = query.to_sql() @@ -226,10 +227,10 @@ # Example 6: Create GIN index with trigram operator class # GIN index with gin_trgm_ops accelerates %, similarity, LIKE queries - create_idx = CreateIndexExpression( + create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_articles_title_trgm", - table_name="articles", + index=Index(dialect, 'idx_articles_title_trgm'), + table=Table(dialect, 'articles'), columns=["title"], index_type="GIN", if_not_exists=True, @@ -251,7 +252,7 @@ # ============================================================ drop_expr = DropTableExpression( dialect=dialect, - table_name="articles", + table=Table(dialect, 'articles'), if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py index af6f2d19..44ccf4d9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py @@ -15,6 +15,7 @@ # ============================================================ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) @@ -34,7 +35,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="route_edges", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'route_edges'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -119,7 +120,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="route_edges", + table=Table(dialect, 'route_edges'), columns=columns, if_not_exists=True, ) @@ -133,7 +134,7 @@ # 1 --(cost=4)--> 3 insert_expr = InsertExpression( dialect=dialect, - into="route_edges", + into=Table(dialect, 'route_edges'), columns=["source", "target", "cost", "reverse_cost", "x1", "y1", "x2", "y2"], source=ValuesSource( dialect, @@ -204,7 +205,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="route_edges", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'route_edges'), 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/extensions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py index fc0f88c1..12a7be64 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py @@ -16,6 +16,7 @@ # ============================================================ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) @@ -35,7 +36,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table="documents", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'documents'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -112,7 +113,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table="documents", + table=Table(dialect, 'documents'), columns=columns, if_not_exists=True, ) @@ -125,7 +126,7 @@ # Use Literal with .cast("vector") for type-safe vector insertion. insert_expr = InsertExpression( dialect=dialect, - into="documents", + into=Table(dialect, 'documents'), columns=["content", "embedding"], source=ValuesSource( dialect, @@ -220,7 +221,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table="documents", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, '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/extensions/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py index ece4456b..708f7809 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py @@ -34,7 +34,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="locations", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'locations'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -63,10 +63,9 @@ ValuesSource, ) from rhosocial.activerecord.backend.expression import ( - Column, - QueryExpression, - TableExpression, + Column, QueryExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType @@ -117,7 +116,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="locations", + table=Table(dialect, 'locations'), columns=columns, if_not_exists=True, ) @@ -130,7 +129,7 @@ # Use FunctionCall to compose ST_SetSRID(ST_MakePoint(lng, lat), 4326) for WGS84 points insert_expr = InsertExpression( dialect=dialect, - into="locations", + into=Table(dialect, 'locations'), columns=["name", "geom"], source=ValuesSource( dialect, @@ -188,7 +187,7 @@ QueryExpression( dialect=dialect, select=[Column(dialect, "geom")], - from_=TableExpression(dialect, "locations"), + from_=Table(dialect, "locations"), where=ComparisonPredicate( dialect, "=", Column(dialect, "name"), @@ -202,7 +201,7 @@ QueryExpression( dialect=dialect, select=[Column(dialect, "geom")], - from_=TableExpression(dialect, "locations"), + from_=Table(dialect, "locations"), where=ComparisonPredicate( dialect, "=", Column(dialect, "name"), @@ -237,7 +236,7 @@ dialect, "ST_AsText", Column(dialect, "geom"), ).as_("point"), ], - from_=TableExpression(dialect, "locations"), + from_=Table(dialect, "locations"), where=FunctionCall( dialect, "ST_DWithin", Column(dialect, "geom"), @@ -263,8 +262,8 @@ # Example 5: Create GIST spatial index create_idx = CreateIndexExpression( dialect=dialect, - index_name="idx_locations_geom", - table_name="locations", + index=Index(dialect, 'idx_locations_geom'), + table=Table(dialect, 'locations'), columns=["geom"], index_type="GIST", if_not_exists=True, @@ -282,7 +281,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="locations", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'locations'), 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/extensions/postgis_raster.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py index 3026e3e0..e6ca9bea 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py @@ -34,7 +34,7 @@ # Clean up for demo from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name="terrain_data", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'terrain_data'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -51,9 +51,9 @@ ColumnConstraintType, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.options import ExecutionOptions @@ -112,7 +112,7 @@ create_expr = CreateTableExpression( dialect=dialect, - table_name="terrain_data", + table=Table(dialect, 'terrain_data'), columns=columns, if_not_exists=True, ) @@ -126,7 +126,7 @@ hex_wkb = "01000001000000000000000000000000000000000000F03F000000000000F03F000000000000000000000000000000000000000000000000E6100000010001004400010101" insert_expr = InsertExpression( dialect=dialect, - into="terrain_data", + into=Table(dialect, 'terrain_data'), columns=["name", "rast"], source=ValuesSource( dialect, @@ -148,7 +148,7 @@ summary_query = QueryExpression( dialect=dialect, select=[st_summary(dialect, Column(dialect, "rast")).as_("summary")], - from_=TableExpression(dialect, "terrain_data"), + from_=Table(dialect, "terrain_data"), ) sql, params = summary_query.to_sql() print("\n--- ST_Summary query ---") @@ -168,7 +168,7 @@ Literal(dialect, 1), ).as_("pixel_value"), ], - from_=TableExpression(dialect, "terrain_data"), + from_=Table(dialect, "terrain_data"), ) sql, params = value_query.to_sql() print("\n--- ST_Value query (pixel at 1,1) ---") @@ -184,7 +184,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name="terrain_data", if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'terrain_data'), 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/extensions/tablefunc.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py index c02d94e3..ffeac890 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py @@ -36,7 +36,7 @@ from rhosocial.activerecord.backend.expression import DropTableExpression for table_name in ["monthly_sales", "org_tree"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table_name), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -63,6 +63,7 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.objects import Table # Check if tablefunc extension is available available = dialect.is_extension_available("tablefunc") @@ -102,7 +103,7 @@ ] create_expr = CreateTableExpression( dialect=dialect, - table_name="monthly_sales", + table=Table(dialect, 'monthly_sales'), columns=columns, if_not_exists=True, ) @@ -113,7 +114,7 @@ # Insert sales data insert_expr = InsertExpression( dialect=dialect, - into="monthly_sales", + into=Table(dialect, 'monthly_sales'), columns=["month", "category", "amount"], source=ValuesSource( dialect, @@ -146,7 +147,7 @@ ] create_tree = CreateTableExpression( dialect=dialect, - table_name="org_tree", + table=Table(dialect, 'org_tree'), columns=tree_columns, if_not_exists=True, ) @@ -157,7 +158,7 @@ # Insert hierarchical data (CEO -> VP Engineering, VP Sales -> Engineers) insert_tree = InsertExpression( dialect=dialect, - into="org_tree", + into=Table(dialect, 'org_tree'), columns=["name", "parent_id"], source=ValuesSource( dialect, @@ -281,7 +282,7 @@ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ for table_name in ["monthly_sales", "org_tree"]: - drop_expr = DropTableExpression(dialect=dialect, table_name=table_name, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table_name), 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/insert/batch.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py index 42eaac9e..7a09a886 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py @@ -33,7 +33,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='logs', + table=Table(dialect, 'logs'), if_exists=True, cascade=True, ) @@ -42,7 +42,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='logs', + table=Table(dialect, 'logs'), columns=[ ColumnDefinition( 'id', @@ -63,17 +63,15 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - InsertExpression, - ValuesSource, - QueryExpression, - TableExpression, + InsertExpression, ValuesSource, QueryExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, WildcardExpression, Column from rhosocial.activerecord.backend.expression.query_parts import OrderByClause insert = InsertExpression( dialect=dialect, - into='logs', + into=Table(dialect, 'logs'), columns=['level', 'message'], source=ValuesSource( dialect, @@ -100,7 +98,7 @@ verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'logs'), + from_=Table(dialect, 'logs'), order_by=OrderByClause(dialect, [Column(dialect, 'id')]), ) options = ExecutionOptions(stmt_type=StatementType.DQL) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py index 09c25c26..90197247 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py @@ -31,7 +31,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'SERIAL'), ColumnDefinition('name', 'VARCHAR(100)'), @@ -45,12 +45,13 @@ # ============================================================ # SECTION: Single INSERT # ============================================================ -from rhosocial.activerecord.backend.expression import InsertExpression, ValuesSource, QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression import InsertExpression, ValuesSource, QueryExpression +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, WildcardExpression insert_expr = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name'], source=ValuesSource(dialect, [ [Literal(dialect, 'Alice')], @@ -64,7 +65,7 @@ verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), ) sql, params = verify_query.to_sql() result = backend.execute(sql, params) @@ -75,7 +76,7 @@ # ============================================================ from rhosocial.activerecord.backend.expression import DropTableExpression -drop_expr = DropTableExpression(dialect=dialect, table_name='users', if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), 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/insert/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py index 9e3378e8..9549207b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py @@ -19,8 +19,8 @@ InsertExpression, ValuesSource, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.expression.statements import ( @@ -47,7 +47,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -56,7 +56,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition( 'id', @@ -87,7 +87,7 @@ insert_nothing = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['username', 'email', 'login_count'], source=ValuesSource(dialect, [ [Literal(dialect, 'alice'), Literal(dialect, 'alice@example.com'), Literal(dialect, 1)], @@ -105,7 +105,7 @@ # Try to insert again - will be ignored insert_nothing2 = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['username', 'email', 'login_count'], source=ValuesSource(dialect, [ [Literal(dialect, 'alice'), Literal(dialect, 'different@example.com'), Literal(dialect, 2)], @@ -122,7 +122,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'username'), Column(dialect, 'email')], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), where=ComparisonPredicate(dialect, '=', Column(dialect, 'username'), Literal(dialect, 'alice')), ) sql, params = query.to_sql() @@ -136,7 +136,7 @@ insert_update = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['username', 'email', 'login_count'], source=ValuesSource(dialect, [ [Literal(dialect, 'bob'), Literal(dialect, 'bob@example.com'), Literal(dialect, 1)], @@ -157,7 +157,7 @@ # Insert again - will update insert_update2 = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['username', 'email', 'login_count'], source=ValuesSource(dialect, [ [Literal(dialect, 'bob'), Literal(dialect, 'bob_new@example.com'), Literal(dialect, 2)], @@ -180,7 +180,7 @@ Column(dialect, 'id'), Column(dialect, 'username'), Column(dialect, 'email'), Column(dialect, 'login_count'), ], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), where=ComparisonPredicate(dialect, '=', Column(dialect, 'username'), Literal(dialect, 'bob')), ) sql, params = query.to_sql() @@ -192,7 +192,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py index 92240083..65c285b5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py @@ -19,9 +19,9 @@ InsertExpression, ValuesSource, ReturningClause, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, @@ -42,7 +42,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -51,7 +51,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'SERIAL', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -74,7 +74,7 @@ # 1. INSERT with RETURNING id insert_expr = InsertExpression( dialect=dialect, - into=TableExpression(dialect, 'users'), + into=Table(dialect, 'users'), source=ValuesSource(dialect, [[Literal(dialect, 'Alice'), Literal(dialect, 'alice@example.com')]]), columns=['name', 'email'], returning=ReturningClause(dialect, [Column(dialect, 'id')]), @@ -94,7 +94,7 @@ # 2. INSERT with multiple RETURNING columns insert_multi = InsertExpression( dialect=dialect, - into=TableExpression(dialect, 'users'), + into=Table(dialect, 'users'), source=ValuesSource(dialect, [[Literal(dialect, 'Bob'), Literal(dialect, 'bob@example.com')]]), columns=['name', 'email'], returning=ReturningClause(dialect, [Column(dialect, 'id'), Column(dialect, 'name')]), @@ -107,7 +107,7 @@ # 3. Batch INSERT with RETURNING batch_insert = InsertExpression( dialect=dialect, - into=TableExpression(dialect, 'users'), + into=Table(dialect, 'users'), source=ValuesSource(dialect, [ [Literal(dialect, 'Charlie'), Literal(dialect, 'charlie@example.com')], [Literal(dialect, 'Diana'), Literal(dialect, 'diana@example.com')], @@ -125,7 +125,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py index ea29824e..d8859c4f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py @@ -22,8 +22,8 @@ Column, Literal, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table # noqa: E402 def get_order(dialect, order_id: int): @@ -31,7 +31,7 @@ def get_order(dialect, order_id: int): return QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "status"), Column(dialect, "user_id")], - from_=TableExpression(dialect, "orders"), + from_=Table(dialect, "orders"), where=Column(dialect, "id") == Literal(dialect, order_id), ) @@ -41,7 +41,7 @@ def check_inventory(dialect, order_id: int): return QueryExpression( dialect, select=[Column(dialect, "available")], - from_=TableExpression(dialect, "inventory"), + from_=Table(dialect, "inventory"), where=Column(dialect, "order_id") == Literal(dialect, order_id), ) @@ -51,7 +51,7 @@ def reserve_inventory(dialect, order_id: int): return QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "available")], - from_=TableExpression(dialect, "inventory"), + from_=Table(dialect, "inventory"), where=Column(dialect, "order_id") == Literal(dialect, order_id), ) @@ -61,7 +61,7 @@ def send_notification(dialect, user_id: int, type: str): return QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "notifications"), + from_=Table(dialect, "notifications"), where=Column(dialect, "user_id") == Literal(dialect, user_id), ) @@ -71,7 +71,7 @@ def process_payment(dialect, order_id: int, amount: float): return QueryExpression( dialect, select=[Column(dialect, "status"), Column(dialect, "transaction_id")], - from_=TableExpression(dialect, "payments"), + from_=Table(dialect, "payments"), where=Column(dialect, "order_id") == Literal(dialect, order_id), ) @@ -81,7 +81,7 @@ def release_inventory(dialect, order_id: int): return QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "inventory"), + from_=Table(dialect, "inventory"), where=Column(dialect, "order_id") == Literal(dialect, order_id), ) @@ -91,7 +91,7 @@ def create_order_record(dialect, order_id: int, user_id: int, amount: float): return QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "created_at")], - from_=TableExpression(dialect, "order_records"), + from_=Table(dialect, "order_records"), where=Column(dialect, "order_id") == Literal(dialect, order_id), ) @@ -101,7 +101,7 @@ def confirm_inventory(dialect, order_id: int): return QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "inventory"), + from_=Table(dialect, "inventory"), where=Column(dialect, "order_id") == Literal(dialect, order_id), ) 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 09dae565..438d33d2 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 @@ -6,6 +6,7 @@ These are the building blocks used by NamedMigration up()/down() methods. """ +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements.ddl_table import ( CreateTableExpression, ColumnDefinition, @@ -23,7 +24,7 @@ def create_users_table(dialect): """CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(255), email VARCHAR(255)).""" return CreateTableExpression( dialect, - table="users", + table=Table(dialect, 'users'), columns=[ ColumnDefinition( dialect, @@ -39,14 +40,14 @@ def create_users_table(dialect): def drop_users_table(dialect): """DROP TABLE IF EXISTS users.""" - return DropTableExpression(dialect, table="users", if_exists=True) + return DropTableExpression(dialect, table=Table(dialect, 'users'), if_exists=True) def create_posts_table(dialect): """CREATE TABLE posts (id SERIAL PRIMARY KEY, title VARCHAR(255), user_id INTEGER).""" return CreateTableExpression( dialect, - table="posts", + table=Table(dialect, 'posts'), columns=[ ColumnDefinition( dialect, @@ -62,7 +63,7 @@ def create_posts_table(dialect): def drop_posts_table(dialect): """DROP TABLE IF EXISTS posts.""" - return DropTableExpression(dialect, table="posts", if_exists=True) + return DropTableExpression(dialect, table=Table(dialect, 'posts'), if_exists=True) def create_custom_table(dialect, table_name: str = "custom_table"): @@ -73,7 +74,7 @@ def create_custom_table(dialect, table_name: str = "custom_table"): """ return CreateTableExpression( dialect, - table=table_name, + table=Table(dialect, table_name), columns=[ ColumnDefinition( dialect, @@ -88,4 +89,4 @@ def create_custom_table(dialect, table_name: str = "custom_table"): def drop_custom_table(dialect, table_name: str = "custom_table"): """DROP TABLE IF EXISTS .""" - return DropTableExpression(dialect, table=table_name, if_exists=True) \ No newline at end of file + return DropTableExpression(dialect, table=Table(dialect, table_name), if_exists=True) \ No newline at end of file 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 236dc523..c8f0b33d 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 @@ -39,7 +39,6 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, CTEExpression, WithQueryExpression, SetOperationExpression, @@ -48,6 +47,8 @@ ColumnConstraint, ColumnConstraintType, ) +from rhosocial.activerecord.backend.expression.sources import NamedRelationRef +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import BetweenPredicate from rhosocial.activerecord.backend.expression.query_parts import ( @@ -71,7 +72,7 @@ ColumnDefinition('name', 'VARCHAR(100)'), ColumnDefinition('city', 'VARCHAR(50)'), ] -backend.execute(*CreateTableExpression(dialect, 'users', users_cols, if_not_exists=True).to_sql()) +backend.execute(*CreateTableExpression(dialect, Table(dialect, 'users'), users_cols, if_not_exists=True).to_sql()) follows_cols = [ ColumnDefinition('id', 'INTEGER', constraints=[ @@ -79,7 +80,7 @@ ColumnDefinition('follower_id', 'INTEGER'), ColumnDefinition('followed_id', 'INTEGER'), ] -backend.execute(*CreateTableExpression(dialect, 'follows', follows_cols, if_not_exists=True).to_sql()) +backend.execute(*CreateTableExpression(dialect, Table(dialect, 'follows'), follows_cols, if_not_exists=True).to_sql()) users_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 'Alice'), Literal(dialect, 'NYC')], @@ -106,7 +107,7 @@ ColumnDefinition('account_holder', 'VARCHAR(100)'), ColumnDefinition('account_type', 'VARCHAR(20)'), ] -backend.execute(*CreateTableExpression(dialect, 'accounts', accounts_cols, if_not_exists=True).to_sql()) +backend.execute(*CreateTableExpression(dialect, Table(dialect, 'accounts'), accounts_cols, if_not_exists=True).to_sql()) txn_cols = [ ColumnDefinition('id', 'INTEGER', constraints=[ @@ -116,7 +117,7 @@ ColumnDefinition('amount', 'NUMERIC(12,2)'), ColumnDefinition('ts', 'VARCHAR(20)'), ] -backend.execute(*CreateTableExpression(dialect, 'transactions', txn_cols, if_not_exists=True).to_sql()) +backend.execute(*CreateTableExpression(dialect, Table(dialect, 'transactions'), txn_cols, if_not_exists=True).to_sql()) accounts_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 'Alice Smith'), Literal(dialect, 'checking')], @@ -155,19 +156,19 @@ Column(dialect, 'name'), Literal(dialect, 0).as_('depth'), ], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), where=WhereClause(dialect, condition=Column(dialect, 'name') == Literal(dialect, 'Alice')), ) # --- Recursive query: follow edges one hop, depth+1, bound depth<4 --- recursive_join = JoinClause( dialect=dialect, - left_table=TableExpression(dialect, 'traversal', alias='t'), - right_table=TableExpression(dialect, 'follows', alias='f'), + left_table=NamedRelationRef(dialect, Table(dialect, 'traversal'), alias='t'), + right_table=NamedRelationRef(dialect, Table(dialect, 'follows'), alias='f'), join_type='INNER JOIN', condition=Column(dialect, 'id', table='t') == Column(dialect, 'follower_id', table='f'), ).inner_join( - right_table=TableExpression(dialect, 'users', alias='u'), + right_table=NamedRelationRef(dialect, Table(dialect, 'users'), alias='u'), condition=Column(dialect, 'followed_id', table='f') == Column(dialect, 'id', table='u'), ) @@ -206,7 +207,7 @@ Column(dialect, 'name'), Column(dialect, 'depth'), ], - from_=TableExpression(dialect, 'traversal'), + from_=Table(dialect, 'traversal'), where=WhereClause(dialect, condition=BetweenPredicate( dialect, Column(dialect, 'depth'), Literal(dialect, 1), Literal(dialect, 3), )), @@ -241,8 +242,8 @@ # --- Base query: direct sources sending to target account ACC-001 --- aml_base_join = JoinClause( dialect=dialect, - left_table=TableExpression(dialect, 'transactions', alias='tx'), - right_table=TableExpression(dialect, 'accounts', alias='a'), + left_table=NamedRelationRef(dialect, Table(dialect, 'transactions'), alias='tx'), + right_table=NamedRelationRef(dialect, Table(dialect, 'accounts'), alias='a'), join_type='INNER JOIN', condition=Column(dialect, 'source_account_id', table='tx') == Column(dialect, 'id', table='a'), ) @@ -263,12 +264,12 @@ # --- Recursive query: trace upstream along the transaction chain --- aml_recursive_join = JoinClause( dialect=dialect, - left_table=TableExpression(dialect, 'fund_trace', alias='tr'), - right_table=TableExpression(dialect, 'transactions', alias='tx'), + left_table=NamedRelationRef(dialect, Table(dialect, 'fund_trace'), alias='tr'), + right_table=NamedRelationRef(dialect, Table(dialect, 'transactions'), alias='tx'), join_type='INNER JOIN', condition=Column(dialect, 'target_account_id', table='tx') == Column(dialect, 'id', table='tr'), ).inner_join( - right_table=TableExpression(dialect, 'accounts', alias='a'), + right_table=NamedRelationRef(dialect, Table(dialect, 'accounts'), alias='a'), condition=Column(dialect, 'source_account_id', table='tx') == Column(dialect, 'id', table='a'), ) @@ -311,7 +312,7 @@ Column(dialect, 'amount'), Column(dialect, 'depth'), ], - from_=TableExpression(dialect, 'fund_trace'), + from_=Table(dialect, 'fund_trace'), where=WhereClause(dialect, condition=Column(dialect, 'depth') >= Literal(dialect, 1)), order_by=OrderByClause(dialect, [Column(dialect, 'depth'), Column(dialect, 'id')]), ) @@ -340,7 +341,7 @@ FunctionCall(dialect, 'COUNT', Column(dialect, 'id'), alias='account_count'), FunctionCall(dialect, 'SUM', Column(dialect, 'amount'), alias='total_amount'), ], - from_=TableExpression(dialect, 'fund_trace'), + from_=Table(dialect, 'fund_trace'), order_by=OrderByClause(dialect, [Column(dialect, 'depth')]), ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py index abcd4eac..3d896f5a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='sales', + table=Table(dialect, 'sales'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='sales', + table=Table(dialect, 'sales'), columns=[ ColumnDefinition( 'id', @@ -65,7 +65,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='sales', + into=Table(dialect, 'sales'), columns=['product', 'quantity', 'price', 'region'], source=ValuesSource( dialect, @@ -84,11 +84,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, - GroupByHavingClause, + QueryExpression, Column, GroupByHavingClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -99,7 +97,7 @@ FunctionCall(dialect, 'SUM', Column(dialect, 'quantity')).as_('total_qty'), FunctionCall(dialect, 'AVG', Column(dialect, 'price')).as_('avg_price'), ], - from_=TableExpression(dialect, 'sales'), + from_=Table(dialect, 'sales'), group_by_having=GroupByHavingClause( dialect, group_by=[Column(dialect, 'product')], diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py index 30f7c1a2..565d4d7b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='orders', + table=Table(dialect, 'orders'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='orders', + table=Table(dialect, 'orders'), columns=[ ColumnDefinition( 'id', @@ -63,7 +63,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='orders', + into=Table(dialect, 'orders'), columns=['customer_name', 'tags'], source=ValuesSource( dialect, @@ -80,10 +80,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, + QueryExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.functions.array import ( array_length, array_to_string, @@ -103,7 +102,7 @@ Column(dialect, 'customer_name'), Column(dialect, 'tags'), ], - from_=TableExpression(dialect, 'orders'), + from_=Table(dialect, 'orders'), ) sql, params = query.to_sql() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py index de626b8b..8efaac94 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition( 'id', @@ -70,7 +70,7 @@ insert = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name', 'age', 'status'], source=ValuesSource( dialect, @@ -88,13 +88,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, - WhereClause, - OrderByClause, - LimitOffsetClause, + QueryExpression, Column, WhereClause, OrderByClause, LimitOffsetClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -105,7 +101,7 @@ Column(dialect, 'name'), Column(dialect, 'age'), ], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), where=WhereClause( dialect, condition=ComparisonPredicate( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py index 31505509..d7623637 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py @@ -31,10 +31,10 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, CTEExpression, WithQueryExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate, IsNullPredicate from rhosocial.activerecord.backend.expression.statements import ( @@ -48,13 +48,13 @@ dql_options = ExecutionOptions(stmt_type=StatementType.DQL) # Drop table first for clean setup -drop = DropTableExpression(dialect=dialect, table_name='employees', if_exists=True, cascade=True) +drop = DropTableExpression(dialect=dialect, table=Table(dialect, 'employees'), if_exists=True, cascade=True) sql, params = drop.to_sql() backend.execute(sql, params) create_table = CreateTableExpression( dialect=dialect, - table_name='employees', + table=Table(dialect, 'employees'), columns=[ ColumnDefinition('id', 'SERIAL', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -70,7 +70,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='employees', + into=Table(dialect, 'employees'), columns=['name', 'manager_id'], source=ValuesSource(dialect, [ [Literal(dialect, 'CEO'), Literal(dialect, None)], @@ -94,7 +94,7 @@ query=QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name')], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=ComparisonPredicate(dialect, '>', Column(dialect, 'id'), Literal(dialect, 2)), ), ) @@ -105,7 +105,7 @@ main_query=QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name')], - from_=TableExpression(dialect, 'high_earners'), + from_=Table(dialect, 'high_earners'), ), ) sql, params = cte_query.to_sql() @@ -127,7 +127,7 @@ Column(dialect, 'manager_id'), Literal(dialect, 1), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=IsNullPredicate(dialect, Column(dialect, 'manager_id')), ) @@ -144,7 +144,7 @@ main_query=QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name'), Column(dialect, 'manager_id')], - from_=TableExpression(dialect, 'org_chart'), + from_=Table(dialect, 'org_chart'), ), recursive=True, ) @@ -166,7 +166,7 @@ query=QueryExpression( dialect=dialect, select=[Column(dialect, 'manager_id'), Column(dialect, 'name')], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), ), materialized=False, # NOT MATERIALIZED - allows inlining ) @@ -177,7 +177,7 @@ main_query=QueryExpression( dialect=dialect, select=[Column(dialect, 'manager_id'), Column(dialect, 'name')], - from_=TableExpression(dialect, 'dept_stats'), + from_=Table(dialect, 'dept_stats'), ), ) sql, params = materialized_query.to_sql() @@ -186,7 +186,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_table = DropTableExpression(dialect=dialect, table_name='employees', if_exists=True, cascade=True) +drop_table = DropTableExpression(dialect=dialect, table=Table(dialect, 'employees'), if_exists=True, cascade=True) sql, params = drop_table.to_sql() backend.execute(sql, params) backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py index 7565f68c..74e0a458 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py @@ -39,7 +39,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, ) sql, params = drop_table.to_sql() @@ -47,7 +47,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'INT'), ColumnDefinition('name', 'VARCHAR(100)'), @@ -60,7 +60,7 @@ insert = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['id', 'name'], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 'Alice')], @@ -76,15 +76,14 @@ # SECTION: SELECT DISTINCT # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - SelectModifier, + QueryExpression, SelectModifier, ) +from rhosocial.activerecord.backend.expression.objects import Table query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name')], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), select_modifier=SelectModifier.DISTINCT, ) sql, params = query.to_sql() @@ -98,7 +97,7 @@ # ============================================================ # SECTION: Teardown # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name='users', if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), 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/query/explain.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py index a945175a..7703985b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py @@ -19,11 +19,11 @@ InsertExpression, ValuesSource, QueryExpression, - TableExpression, ExplainExpression, CreateIndexExpression, DropTableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, @@ -46,7 +46,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -55,7 +55,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'SERIAL', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -73,8 +73,8 @@ create_index = CreateIndexExpression( dialect=dialect, - index_name='idx_users_email', - table_name='users', + index=Index(dialect, 'idx_users_email'), + table=Table(dialect, 'users'), columns=['email'], if_not_exists=True, ) @@ -89,7 +89,7 @@ for name, email in users: insert_expr = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name', 'email'], source=ValuesSource(dialect, [[Literal(dialect, name), Literal(dialect, email)]]), ) @@ -104,7 +104,7 @@ query1 = QueryExpression( dialect=dialect, select=[Column(dialect, '*')], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'name'), Literal(dialect, 'Alice'), ), @@ -125,7 +125,7 @@ query2 = QueryExpression( dialect=dialect, select=[Column(dialect, '*')], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'email'), Literal(dialect, 'alice@example.com'), ), @@ -173,7 +173,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py index 947fb974..ce66cd1e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py @@ -37,7 +37,7 @@ for table in ['orders', 'customers']: drop_table = DropTableExpression( dialect=dialect, - table_name=table, + table=Table(dialect, table), if_exists=True, cascade=True, ) @@ -46,7 +46,7 @@ create_customers = CreateTableExpression( dialect=dialect, - table_name='customers', + table=Table(dialect, 'customers'), columns=[ ColumnDefinition( 'id', @@ -68,7 +68,7 @@ create_orders = CreateTableExpression( dialect=dialect, - table_name='orders', + table=Table(dialect, 'orders'), columns=[ ColumnDefinition( 'id', @@ -87,7 +87,7 @@ insert_customers = InsertExpression( dialect=dialect, - into='customers', + into=Table(dialect, 'customers'), columns=['name', 'email'], source=ValuesSource( dialect, @@ -101,7 +101,7 @@ insert_orders = InsertExpression( dialect=dialect, - into='orders', + into=Table(dialect, 'orders'), columns=['customer_id', 'total', 'status'], source=ValuesSource( dialect, @@ -118,16 +118,15 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, - WhereClause, + QueryExpression, Column, WhereClause, ) +from rhosocial.activerecord.backend.expression.sources import NamedRelationRef +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate -customers = TableExpression(dialect, 'customers', alias='c') -orders = TableExpression(dialect, 'orders', alias='o') +customers = NamedRelationRef(dialect, Table(dialect, 'customers'), alias='c') +orders = NamedRelationRef(dialect, Table(dialect, 'orders'), alias='o') query = QueryExpression( dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py index c6159fab..ddafbfcc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py @@ -14,11 +14,11 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, Column, OrderByClause, LimitOffsetClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import ( Literal, WildcardExpression, @@ -45,7 +45,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='articles', + table=Table(dialect, 'articles'), if_exists=True, cascade=True, ) @@ -54,7 +54,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='articles', + table=Table(dialect, 'articles'), columns=[ ColumnDefinition( 'id', @@ -79,7 +79,7 @@ insert = InsertExpression( dialect=dialect, - into='articles', + into=Table(dialect, 'articles'), columns=['title', 'author', 'published_at'], source=ValuesSource( dialect, @@ -118,7 +118,7 @@ Column(dialect, 'author'), Column(dialect, 'published_at'), ], - from_=TableExpression(dialect, 'articles'), + from_=Table(dialect, 'articles'), order_by=OrderByClause( dialect, expressions=[(Column(dialect, 'id'), 'ASC')], @@ -135,7 +135,7 @@ count_query = QueryExpression( dialect=dialect, select=[FunctionCall(dialect, 'COUNT', WildcardExpression(dialect)).as_('total')], - from_=TableExpression(dialect, 'articles'), + from_=Table(dialect, 'articles'), ) count_sql, count_params = count_query.to_sql() @@ -174,7 +174,7 @@ Column(dialect, 'author'), Column(dialect, 'published_at'), ], - from_=TableExpression(dialect, 'articles'), + from_=Table(dialect, 'articles'), order_by=OrderByClause( dialect, expressions=[(Column(dialect, 'id'), 'ASC')], diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py index f9cc7b2a..4a8152a6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py @@ -20,9 +20,9 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, WhereClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import ( LikePredicate, @@ -52,7 +52,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='employees', + table=Table(dialect, 'employees'), if_exists=True, cascade=True, ) @@ -61,7 +61,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='employees', + table=Table(dialect, 'employees'), columns=[ ColumnDefinition( 'id', @@ -87,7 +87,7 @@ insert = InsertExpression( dialect=dialect, - into='employees', + into=Table(dialect, 'employees'), columns=['name', 'department', 'salary', 'manager_id'], source=ValuesSource( dialect, @@ -146,7 +146,7 @@ Column(dialect, 'name'), Column(dialect, 'department'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=LikePredicate( @@ -169,7 +169,7 @@ Column(dialect, 'name'), Column(dialect, 'department'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=InPredicate( @@ -191,7 +191,7 @@ Column(dialect, 'name'), Column(dialect, 'salary'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=BetweenPredicate( @@ -214,7 +214,7 @@ Column(dialect, 'name'), Column(dialect, 'manager_id'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=IsNullPredicate( @@ -235,7 +235,7 @@ Column(dialect, 'name'), Column(dialect, 'manager_id'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=IsNullPredicate( @@ -258,7 +258,7 @@ Column(dialect, 'department'), Column(dialect, 'salary'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=LogicalPredicate( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py index 262a0a35..1b7e74c8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py @@ -37,7 +37,7 @@ for table in ['employees', 'departments']: drop_table = DropTableExpression( dialect=dialect, - table_name=table, + table=Table(dialect, table), if_exists=True, cascade=True, ) @@ -46,7 +46,7 @@ create_departments = CreateTableExpression( dialect=dialect, - table_name='departments', + table=Table(dialect, 'departments'), columns=[ ColumnDefinition( 'id', @@ -64,7 +64,7 @@ create_employees = CreateTableExpression( dialect=dialect, - table_name='employees', + table=Table(dialect, 'employees'), columns=[ ColumnDefinition( 'id', @@ -83,7 +83,7 @@ insert_departments = InsertExpression( dialect=dialect, - into='departments', + into=Table(dialect, 'departments'), columns=['name', 'budget'], source=ValuesSource( dialect, @@ -97,7 +97,7 @@ insert_employees = InsertExpression( dialect=dialect, - into='employees', + into=Table(dialect, 'employees'), columns=['name', 'salary', 'department_id'], source=ValuesSource( dialect, @@ -115,19 +115,16 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, - WhereClause, - Subquery, + QueryExpression, Column, WhereClause, Subquery, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate subquery_query = QueryExpression( dialect=dialect, select=[FunctionCall(dialect, 'AVG', Column(dialect, 'salary'))], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), ) sql, params = subquery_query.to_sql() subquery = Subquery(dialect, sql, params) @@ -138,7 +135,7 @@ Column(dialect, 'name'), Column(dialect, 'salary'), ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), where=WhereClause( dialect, condition=ComparisonPredicate( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py index 8e59959a..cd3520de 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='articles', + table=Table(dialect, 'articles'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='articles', + table=Table(dialect, 'articles'), columns=[ ColumnDefinition( 'id', @@ -63,7 +63,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='articles', + into=Table(dialect, 'articles'), columns=['title', 'content'], source=ValuesSource( dialect, @@ -82,10 +82,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, + QueryExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -104,7 +103,7 @@ Column(dialect, 'id'), Column(dialect, 'title'), ], - from_=TableExpression(dialect, 'articles'), + from_=Table(dialect, 'articles'), where=match_pred, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py index 24a234f7..845f1629 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py @@ -39,7 +39,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, ) sql, params = drop_table.to_sql() @@ -47,7 +47,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'INT'), ColumnDefinition('name', 'VARCHAR(100)'), @@ -60,7 +60,7 @@ insert = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['id', 'name'], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 'Alice')], @@ -75,15 +75,14 @@ # SECTION: UNION # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - SetOperationExpression, + QueryExpression, SetOperationExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table query1 = QueryExpression( dialect=dialect, select=[Column(dialect, 'name')], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), ) query2 = QueryExpression( @@ -123,7 +122,7 @@ # ============================================================ # SECTION: Teardown # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name='users', if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), 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/query/window.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py index 1f97712c..e3dc2eaf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='sales_data', + table=Table(dialect, 'sales_data'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='sales_data', + table=Table(dialect, 'sales_data'), columns=[ ColumnDefinition( 'id', @@ -64,7 +64,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='sales_data', + into=Table(dialect, 'sales_data'), columns=['salesperson', 'region', 'amount'], source=ValuesSource( dialect, @@ -83,12 +83,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, - WindowSpecification, - OrderByClause, + QueryExpression, Column, WindowSpecification, OrderByClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.advanced_functions import WindowFunctionCall query = QueryExpression( @@ -118,7 +115,7 @@ alias='region_total', ), ], - from_=TableExpression(dialect, 'sales_data'), + from_=Table(dialect, 'sales_data'), ) sql, params = query.to_sql() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py index 9b1731cb..90032096 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py @@ -25,8 +25,8 @@ # ============================================================ from rhosocial.activerecord.backend.expression import ( QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Column from rhosocial.activerecord.backend.expression.advanced_functions import ( WindowFunctionCall, @@ -101,7 +101,7 @@ Column(dialect, 'salary'), row_number, ], - from_=TableExpression(dialect, 'employees'), + from_=Table(dialect, 'employees'), ) sql, params = query.to_sql() 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 b2ba8fb5..8153e338 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 @@ -37,8 +37,8 @@ backend.introspect_and_adapt() dialect = backend.dialect -drop_v1 = DropTableExpression(dialect, "scores_v1", if_exists=True, cascade=True) -drop_v2 = DropTableExpression(dialect, "scores_v2", if_exists=True, cascade=True) +drop_v1 = DropTableExpression(dialect, Table(dialect, 'scores_v1'), if_exists=True, cascade=True) +drop_v2 = DropTableExpression(dialect, Table(dialect, 'scores_v2'), if_exists=True, cascade=True) sql, params = drop_v1.to_sql() backend.execute(sql, params) sql, params = drop_v2.to_sql() @@ -46,7 +46,7 @@ # One-dimensional array column scores_v1_table = CreateTableExpression( - dialect=dialect, table="scores_v1", columns=[ + dialect=dialect, table=Table(dialect, 'scores_v1'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "values", ArrayType(dialect, element_type=IntegerType(dialect), dimensions=1)), @@ -57,7 +57,7 @@ # Two-dimensional array column scores_v2_table = CreateTableExpression( - dialect=dialect, table="scores_v2", columns=[ + dialect=dialect, table=Table(dialect, 'scores_v2'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "values", ArrayType(dialect, element_type=IntegerType(dialect), dimensions=2)), @@ -96,6 +96,7 @@ ) from rhosocial.activerecord.backend.expression.types import IntegerType, ArrayType # noqa: E402 from datetime import datetime, timezone # noqa: E402 +from rhosocial.activerecord.backend.expression.objects import Table db_info = DatabaseInfo(name="test", version="16", version_tuple=(16, 0, 0), vendor="postgresql") 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 48e98a9b..88d86824 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 @@ -14,6 +14,7 @@ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, @@ -40,12 +41,12 @@ backend.introspect_and_adapt() dialect = backend.dialect -drop_demo = DropTableExpression(dialect, "demo", if_exists=True, cascade=True) +drop_demo = DropTableExpression(dialect, Table(dialect, 'demo'), if_exists=True, cascade=True) sql, params = drop_demo.to_sql() backend.execute(sql, params) demo_table = CreateTableExpression( - dialect=dialect, table="demo", columns=[ + dialect=dialect, table=Table(dialect, 'demo'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), @@ -70,14 +71,14 @@ # Add a column between name and email — shifts ordinal positions # but PostgresSchemaDiffer ignores ordinal_position. -add_age = AlterTableExpression(dialect, "demo", [ +add_age = AlterTableExpression(dialect, Table(dialect, 'demo'), [ AddColumn(dialect, ColumnDefinition(dialect, "age", IntegerType(dialect))) ]) sql, params = add_age.to_sql() backend.execute(sql, params) # Now drop it — column set is identical -drop_age = AlterTableExpression(dialect, "demo", [ +drop_age = AlterTableExpression(dialect, Table(dialect, 'demo'), [ DropColumn(dialect, "age") ]) sql, params = drop_age.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 55b64319..28e128ed 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 @@ -14,6 +14,7 @@ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, @@ -40,12 +41,12 @@ backend.introspect_and_adapt() dialect = backend.dialect -drop_demo = DropTableExpression(dialect, "demo", if_exists=True, cascade=True) +drop_demo = DropTableExpression(dialect, Table(dialect, 'demo'), if_exists=True, cascade=True) sql, params = drop_demo.to_sql() backend.execute(sql, params) demo_table = CreateTableExpression( - dialect=dialect, table="demo", columns=[ + dialect=dialect, table=Table(dialect, 'demo'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=200)), @@ -68,16 +69,16 @@ builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) # Snapshot with BTREE index -idx_btree = CreateIndexExpression(dialect, "idx_name", "demo", ["name"], index_type="BTREE") +idx_btree = CreateIndexExpression(dialect, Index(dialect, 'idx_name'), Table(dialect, 'demo'), ["name"], index_type="BTREE") sql, params = idx_btree.to_sql() backend.execute(sql, params) snap_btree = builder.build(schema="public") # Drop BTREE, create GIN index on array column — different index type -drop_idx = DropIndexExpression(dialect, "idx_name", "demo", if_exists=True) +drop_idx = DropIndexExpression(dialect, Index(dialect, 'idx_name'), "demo", if_exists=True) sql, params = drop_idx.to_sql() backend.execute(sql, params) -idx_gin = CreateIndexExpression(dialect, "idx_tags", "demo", ["tags"], index_type="GIN") +idx_gin = CreateIndexExpression(dialect, Index(dialect, 'idx_tags'), Table(dialect, 'demo'), ["tags"], index_type="GIN") sql, params = idx_gin.to_sql() backend.execute(sql, params) snap_gin = builder.build(schema="public") 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 25654c6a..6414b36a 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 @@ -20,6 +20,7 @@ from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression.statements import IdentityClause +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, @@ -43,8 +44,8 @@ backend.introspect_and_adapt() dialect = backend.dialect -drop_pg9 = DropTableExpression(dialect, "pg9_style", if_exists=True, cascade=True) -drop_pg10 = DropTableExpression(dialect, "pg10_style", if_exists=True, cascade=True) +drop_pg9 = DropTableExpression(dialect, Table(dialect, 'pg9_style'), if_exists=True, cascade=True) +drop_pg10 = DropTableExpression(dialect, Table(dialect, 'pg10_style'), if_exists=True, cascade=True) sql, params = drop_pg9.to_sql() backend.execute(sql, params) sql, params = drop_pg10.to_sql() @@ -52,7 +53,7 @@ # PostgreSQL 9-compatible: SERIAL shorthand pg9_table = CreateTableExpression( - dialect=dialect, table="pg9_style", columns=[ + dialect=dialect, table=Table(dialect, 'pg9_style'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), @@ -66,7 +67,7 @@ # in the PostgreSQL dialect's format_column_definition try: pg10_table = CreateTableExpression( - dialect=dialect, table="pg10_style", columns=[ + dialect=dialect, table=Table(dialect, 'pg10_style'), columns=[ ColumnDefinition(dialect, "id", IntegerType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)], identity_clause=IdentityClause(dialect, generation="ALWAYS")), 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 86413613..191f1489 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 @@ -14,6 +14,7 @@ import os from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, @@ -40,12 +41,12 @@ backend.introspect_and_adapt() dialect = backend.dialect -drop_users = DropTableExpression(dialect, "users", if_exists=True, cascade=True) +drop_users = DropTableExpression(dialect, Table(dialect, 'users'), if_exists=True, cascade=True) sql, params = drop_users.to_sql() backend.execute(sql, params) users_table = CreateTableExpression( - dialect=dialect, table="users", columns=[ + dialect=dialect, table=Table(dialect, 'users'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), @@ -74,7 +75,7 @@ snapshot_loaded = SchemaSnapshot.from_dict(json.loads(snapshot_json)) # Modify the database -add_phone = AlterTableExpression(dialect, "users", [ +add_phone = AlterTableExpression(dialect, Table(dialect, 'users'), [ AddColumn(dialect, ColumnDefinition(dialect, "phone", VarCharType(dialect, length=20))) ]) sql, params = add_phone.to_sql() 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 55f3dcb3..45913bc9 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 @@ -20,6 +20,7 @@ from rhosocial.activerecord.backend.expression.types import ( IntegerType, VarCharType, DecimalType, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.expression.types import ( PostgresSerialType, ) @@ -36,8 +37,8 @@ backend.introspect_and_adapt() dialect = backend.dialect -drop_users = DropTableExpression(dialect, "users", if_exists=True, cascade=True) -drop_orders = DropTableExpression(dialect, "orders", if_exists=True, cascade=True) +drop_users = DropTableExpression(dialect, Table(dialect, 'users'), if_exists=True, cascade=True) +drop_orders = DropTableExpression(dialect, Table(dialect, 'orders'), if_exists=True, cascade=True) sql, params = drop_users.to_sql() backend.execute(sql, params) sql, params = drop_orders.to_sql() @@ -57,7 +58,7 @@ snapshot_before = builder.build(schema="public") users_table = CreateTableExpression( - dialect=dialect, table="users", columns=[ + dialect=dialect, table=Table(dialect, 'users'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=100), @@ -68,7 +69,7 @@ backend.execute(sql, params) orders_table = CreateTableExpression( - dialect=dialect, table="orders", columns=[ + dialect=dialect, table=Table(dialect, 'orders'), columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "user_id", IntegerType(dialect)), @@ -76,9 +77,10 @@ ], table_constraints=[ TableConstraint( + dialect, constraint_type=TableConstraintType.FOREIGN_KEY, columns=["user_id"], - foreign_key_table="users", + foreign_key_table=Table(dialect, "users"), foreign_key_columns=["id"], ) ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py index 157695db..35677780 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py @@ -24,7 +24,6 @@ Literal, OrderByClause, QueryExpression, - TableExpression, ValuesSource, XMLStandaloneOption, XMLTableColumn, @@ -41,6 +40,7 @@ xmlserialize, xmltable, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect @@ -96,7 +96,7 @@ def select_from_examples(dialect: PostgresDialect, expr) -> SQLQueryAndParams: sql, params = QueryExpression( dialect, [expr], - from_=TableExpression(dialect, EXAMPLE_TABLE), + from_=Table(dialect, EXAMPLE_TABLE), ).to_sql() return sql, tuple(params) @@ -241,12 +241,12 @@ def execute_expression(backend: PostgresBackend, expr) -> None: def prepare_example_data(backend: PostgresBackend, dialect: PostgresDialect) -> None: - execute_expression(backend, DropTableExpression(dialect, EXAMPLE_TABLE, if_exists=True)) + execute_expression(backend, DropTableExpression(dialect, Table(dialect, EXAMPLE_TABLE), if_exists=True)) execute_expression( backend, CreateTableExpression( dialect, - EXAMPLE_TABLE, + Table(dialect, EXAMPLE_TABLE), [ ColumnDefinition( "id", @@ -265,7 +265,7 @@ def prepare_example_data(backend: PostgresBackend, dialect: PostgresDialect) -> backend, InsertExpression( dialect, - EXAMPLE_TABLE, + Table(dialect, EXAMPLE_TABLE), ValuesSource( dialect, [ @@ -279,7 +279,7 @@ def prepare_example_data(backend: PostgresBackend, dialect: PostgresDialect) -> def cleanup_example_data(backend: PostgresBackend, dialect: PostgresDialect) -> None: - execute_expression(backend, DropTableExpression(dialect, EXAMPLE_TABLE, if_exists=True)) + execute_expression(backend, DropTableExpression(dialect, Table(dialect, EXAMPLE_TABLE), if_exists=True)) def run_select_examples(dialect: PostgresDialect, examples: Iterable[SQLXMLExample]) -> None: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py index 7dd699ee..17b1da97 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py @@ -19,8 +19,8 @@ InsertExpression, ValuesSource, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.query_parts import OrderByClause from rhosocial.activerecord.backend.expression.statements import ( @@ -44,13 +44,13 @@ dql_options = ExecutionOptions(stmt_type=StatementType.DQL) -drop_table = DropTableExpression(dialect=dialect, table_name='accounts', if_exists=True, cascade=True) +drop_table = DropTableExpression(dialect=dialect, table=Table(dialect, 'accounts'), if_exists=True, cascade=True) sql, params = drop_table.to_sql() backend.execute(sql, params) create_table = CreateTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), columns=[ ColumnDefinition('id', 'SERIAL', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -68,7 +68,7 @@ # Insert initial data insert = InsertExpression( dialect=dialect, - into=TableExpression(dialect, 'accounts'), + into=Table(dialect, 'accounts'), source=ValuesSource(dialect, [ [Literal(dialect, 'Alice'), Literal(dialect, 1000.00)], [Literal(dialect, 'Bob'), Literal(dialect, 500.00)], @@ -91,7 +91,7 @@ update = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 1500.00)}, where=ComparisonPredicate( dialect, '=', @@ -107,7 +107,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'name'), @@ -124,7 +124,7 @@ with backend.transaction(): update = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 0)}, where=ComparisonPredicate( dialect, '=', @@ -143,7 +143,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'name'), @@ -160,7 +160,7 @@ # First operation update = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 2000.00)}, where=ComparisonPredicate( dialect, '=', @@ -178,7 +178,7 @@ # Second operation (within savepoint) update = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 9999.00)}, where=ComparisonPredicate( dialect, '=', @@ -197,7 +197,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), order_by=OrderByClause(dialect, [Column(dialect, 'id')]), ) sql, params = query.to_sql() @@ -207,7 +207,7 @@ # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ -drop_table = DropTableExpression(dialect=dialect, table_name='accounts', if_exists=True) +drop_table = DropTableExpression(dialect=dialect, table=Table(dialect, 'accounts'), 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/transaction/exclusive.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py index 1e720f7a..45761b37 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py @@ -20,9 +20,9 @@ InsertExpression, ValuesSource, QueryExpression, - TableExpression, UpdateExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.expression.query_parts import OrderByClause @@ -51,7 +51,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), if_exists=True, cascade=True, ) @@ -60,7 +60,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), columns=[ ColumnDefinition( 'id', @@ -82,7 +82,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='accounts', + into=Table(dialect, 'accounts'), columns=['name', 'balance'], source=ValuesSource(dialect, [ [Literal(dialect, 'Alice'), Literal(dialect, 1000)], @@ -106,7 +106,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'name'), @@ -120,7 +120,7 @@ # Update within the transaction update_expr = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 1100)}, where=ComparisonPredicate( dialect, '=', @@ -135,7 +135,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'name'), @@ -157,7 +157,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate( dialect, '=', Column(dialect, 'name'), @@ -187,7 +187,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), order_by=OrderByClause(dialect, [Column(dialect, 'id')]), ) sql, params = query.to_sql() @@ -197,7 +197,7 @@ # Update Bob's balance update_expr = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 600)}, where=ComparisonPredicate( dialect, '=', @@ -218,7 +218,7 @@ with backend.transaction_manager.transaction(isolation_level=IsolationLevel.SERIALIZABLE): update_expr = UpdateExpression( dialect=dialect, - table=TableExpression(dialect, 'accounts'), + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 1200)}, where=ComparisonPredicate( dialect, '=', @@ -234,7 +234,7 @@ query = QueryExpression( dialect=dialect, select=[Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), order_by=OrderByClause(dialect, [Column(dialect, 'id')]), ) sql, params = query.to_sql() @@ -246,7 +246,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py index 33b46164..c7aaeb5d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py @@ -20,9 +20,9 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, UpdateExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.expression.query_parts import ForUpdateClause @@ -50,7 +50,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), if_exists=True, cascade=True, ) @@ -59,7 +59,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), columns=[ ColumnDefinition( 'id', @@ -81,7 +81,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='accounts', + into=Table(dialect, 'accounts'), columns=['name', 'balance'], source=ValuesSource( dialect, @@ -104,7 +104,7 @@ lock_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate(dialect, '=', Column(dialect, 'name'), Literal(dialect, 'Alice')), for_update=ForUpdateClause(dialect), ) @@ -115,7 +115,7 @@ # Update the balance update_expr = UpdateExpression( dialect=dialect, - table='accounts', + table=Table(dialect, 'accounts'), assignments={'balance': Literal(dialect, 900)}, where=ComparisonPredicate(dialect, '=', Column(dialect, 'name'), Literal(dialect, 'Alice')), ) @@ -133,7 +133,7 @@ lock_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate(dialect, '>', Column(dialect, 'balance'), Literal(dialect, 500)), for_update=ForUpdateClause(dialect), ) @@ -149,7 +149,7 @@ skip_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), for_update=ForUpdateClause(dialect, skip_locked=True), ) sql, params = skip_query.to_sql() @@ -166,7 +166,7 @@ nowait_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), where=ComparisonPredicate(dialect, '=', Column(dialect, 'name'), Literal(dialect, 'Alice')), for_update=ForUpdateClause(dialect, nowait=True), ) @@ -195,7 +195,7 @@ share_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name'), Column(dialect, 'balance')], - from_=TableExpression(dialect, 'accounts'), + from_=Table(dialect, 'accounts'), for_update=ForUpdateClause(dialect, strength=LockStrength.SHARE), ) sql, params = share_query.to_sql() @@ -207,7 +207,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='accounts', + table=Table(dialect, 'accounts'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py index 7aefe90a..861555be 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition( 'id', @@ -63,7 +63,7 @@ insert_expr = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name', 'scores'], source=ValuesSource( dialect, @@ -80,10 +80,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, + QueryExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.functions.array import ( array_length, array_to_string, @@ -103,7 +102,7 @@ Column(dialect, 'name'), Column(dialect, 'scores'), ], - from_=TableExpression(dialect, 'users'), + from_=Table(dialect, 'users'), ) sql, params = query.to_sql() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py index 9479fad4..f4736b4e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py @@ -36,7 +36,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='documents', + table=Table(dialect, 'documents'), if_exists=True, cascade=True, ) @@ -45,7 +45,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='documents', + table=Table(dialect, 'documents'), columns=[ ColumnDefinition( 'id', @@ -63,7 +63,7 @@ import json insert_expr = InsertExpression( dialect=dialect, - into='documents', + into=Table(dialect, 'documents'), columns=['data'], source=ValuesSource( dialect, @@ -79,10 +79,9 @@ # SECTION: Business Logic (the pattern to learn) # ============================================================ from rhosocial.activerecord.backend.expression import ( - QueryExpression, - TableExpression, - Column, + QueryExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.functions.json import ( jsonb_path_query, json_path_root, @@ -103,7 +102,7 @@ Column(dialect, 'id'), Column(dialect, 'data'), ], - from_=TableExpression(dialect, 'documents'), + from_=Table(dialect, 'documents'), ) sql, params = query.to_sql() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py index 7ce054a5..aff5408e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py @@ -19,8 +19,8 @@ InsertExpression, ValuesSource, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.core import Literal, Column from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, @@ -54,7 +54,7 @@ # Clean up for table in ['events_v13', 'events_legacy']: - drop = DropTableExpression(dialect=dialect, table_name=table, if_exists=True, cascade=True) + drop = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True, cascade=True) sql, params = drop.to_sql() backend.execute(sql, params) @@ -68,7 +68,7 @@ # PostgreSQL < 13: uuid_generate_v4() (requires uuid-ossp extension) create_table = CreateTableExpression( dialect=dialect, - table_name='events', + table=Table(dialect, 'events'), columns=[ ColumnDefinition('id', 'UUID', constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), @@ -98,7 +98,7 @@ # 2. INSERT with auto-generated UUID (using DEFAULT) insert_default = InsertExpression( dialect=dialect, - into='events', + into=Table(dialect, 'events'), columns=['name'], source=ValuesSource(dialect, [ [Literal(dialect, 'user_created')], @@ -112,7 +112,7 @@ # 3. INSERT with explicit UUID value insert_explicit = InsertExpression( dialect=dialect, - into='events', + into=Table(dialect, 'events'), columns=['id', 'name'], source=ValuesSource(dialect, [ [Literal(dialect, '550e8400-e29b-41d4-a716-446655440000'), Literal(dialect, 'explicit_uuid')], @@ -126,7 +126,7 @@ verify_query = QueryExpression( dialect=dialect, select=[Column(dialect, 'id'), Column(dialect, 'name')], - from_=TableExpression(dialect, 'events'), + from_=Table(dialect, 'events'), ) sql, params = verify_query.to_sql() result = backend.execute(sql, params, options=dql_options) @@ -137,7 +137,7 @@ # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='events', + table=Table(dialect, 'events'), if_exists=True, cascade=True, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py index 73f56aef..c1561400 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py @@ -27,6 +27,7 @@ backend.connect() dialect = backend.dialect +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, InsertExpression, @@ -42,7 +43,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), if_exists=True, ) sql, params = drop_table.to_sql() @@ -50,7 +51,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table=Table(dialect, 'users'), columns=[ ColumnDefinition('id', 'SERIAL'), ColumnDefinition('name', 'VARCHAR(100)'), @@ -64,7 +65,7 @@ insert = InsertExpression( dialect=dialect, - into='users', + into=Table(dialect, 'users'), columns=['name', 'age'], source=ValuesSource(dialect, [ [Literal(dialect, 'Alice'), Literal(dialect, 25)], @@ -80,7 +81,7 @@ # ============================================================ update_expr = UpdateExpression( dialect=dialect, - table='users', + table=Table(dialect, 'users'), assignments={'age': Literal(dialect, 26)}, where=ComparisonPredicate(dialect, '=', Column(dialect, 'name'), Literal(dialect, 'Alice')), ) @@ -97,7 +98,7 @@ # ============================================================ update_all = UpdateExpression( dialect=dialect, - table='users', + table=Table(dialect, 'users'), assignments={'age': Literal(dialect, 99)}, ) sql, params = update_all.to_sql() @@ -108,7 +109,7 @@ # ============================================================ # SECTION: Teardown # ============================================================ -drop_expr = DropTableExpression(dialect=dialect, table_name='users', if_exists=True) +drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, 'users'), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) backend.disconnect() 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 b8a029bf..aa810387 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py @@ -9,9 +9,13 @@ - COMMENT: PostgreSQL 7.2+ """ -from typing import Optional, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.expression.objects import SchemaObject +from rhosocial.activerecord.backend.expression.statements.ddl_comment import ( + CommentObjectType, +) if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase @@ -29,30 +33,33 @@ class PostgresCommentExpression(BaseExpression): Attributes: object_type: Object type: 'TABLE', 'COLUMN', 'INDEX', 'VIEW', 'SCHEMA', 'FUNCTION', 'TRIGGER', etc. - object_name: Name of the object to comment on. - For COLUMN, format as 'table.column'. + object: The schema object being commented on. Its own namespace is + rendered by the dialect, so a qualified name needs no + assembly here. comment: Comment text (None to remove existing comment). - schema: Schema name for the object. + column: Name of the column, when the target is a column of ``object``. Example: + >>> from rhosocial.activerecord.backend.expression.objects import Table >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Comment on a table >>> comment = PostgresCommentExpression( ... dialect=dialect, ... object_type="TABLE", - ... object_name="users", + ... object=Table(dialect, "users"), ... comment="User accounts table", ... ) >>> sql, params = comment.to_sql() >>> sql - "COMMENT ON TABLE users IS 'User accounts table'" + 'COMMENT ON TABLE "users" IS ?' >>> # Comment on a column >>> comment = PostgresCommentExpression( ... dialect=dialect, ... object_type="COLUMN", - ... object_name="users.email", + ... object=Table(dialect, "users"), + ... column="email", ... comment="User email address", ... ) @@ -60,7 +67,7 @@ class PostgresCommentExpression(BaseExpression): >>> comment = PostgresCommentExpression( ... dialect=dialect, ... object_type="INDEX", - ... object_name="users_email_idx", + ... object=Table(dialect, "users"), ... comment=None, ... ) @@ -69,16 +76,22 @@ class PostgresCommentExpression(BaseExpression): def __init__( self, dialect: "SQLDialectBase", - object_type: str, - object_name: str, - comment: Optional[str], - schema: Optional[str] = None, + object_type: Union["CommentObjectType", str], + object: "SchemaObject", + comment: Optional[str] = None, + column: Optional[str] = None, ): super().__init__(dialect) + if not isinstance(object, SchemaObject): + raise TypeError( + f"object must be a SchemaObject, got {type(object).__name__}" + ) + if column is not None and (not isinstance(column, str) or not column.strip()): + raise ValueError("column must be a non-empty string or None") self.object_type = object_type - self.object_name = object_name + self.object = object self.comment = comment - self.schema = schema + self.column = column @property def format_method(self) -> str: 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 841fe26d..bc7bf21c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/domain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/domain.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Sequence, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression.bases import BaseExpression, SQLPredicate +from rhosocial.activerecord.backend.expression.objects import Domain from rhosocial.activerecord.backend.expression.statements.ddl_domain import ( AddDomainCheckAction, AlterDomainExpression, @@ -200,6 +201,7 @@ def __init__( resolved_schema = schema if schema is not None else schema_name if schema is not None and schema_name is not None and schema != schema_name: raise ValueError("schema and schema_name must match when both are provided") + domain = Domain(dialect, name, schema_name=resolved_schema) constraint_items: List[Any] if constraints is not None and checks is not None: raise ValueError("constraints and checks are mutually exclusive") @@ -245,7 +247,7 @@ def __init__( normalized_default = _LegacySqlExpression(dialect, default, "default") super().__init__( dialect, - name, + domain, normalized_type, default=normalized_default, nullability=nullability, @@ -270,7 +272,7 @@ def get_params(self) -> Dict[str, Any]: @property def name(self) -> str: - return self.domain_name + return self.domain.name class PostgresAlterDomainExpression(AlterDomainExpression): @@ -314,7 +316,7 @@ def __init__( actions = [RenameDomainAction(dialect, new_name)] else: actions = [_UnsupportedDomainAction(dialect, action)] - super().__init__(dialect, name, actions) + super().__init__(dialect, Domain(dialect, name, schema_name=resolved_schema), actions) self.schema = resolved_schema self.schema_name = resolved_schema self.action = action @@ -323,7 +325,7 @@ def __init__( @property def name(self) -> str: - return self.domain_name + return self.domain.name class PostgresDropDomainExpression(DropDomainExpression): @@ -341,7 +343,7 @@ def __init__( resolved_schema = schema if schema is not None else schema_name if schema is not None and schema_name is not None and schema != schema_name: raise ValueError("schema and schema_name must match when both are provided") - super().__init__(dialect, name) + super().__init__(dialect, Domain(dialect, name, schema_name=resolved_schema)) self.schema = resolved_schema self.schema_name = resolved_schema self.if_exists = if_exists @@ -350,4 +352,4 @@ def __init__( @property def name(self) -> str: - return self.domain_name + return self.domain.name diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index_definition.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index_definition.py index 0bb21df3..7dfe456d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index_definition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index_definition.py @@ -16,6 +16,7 @@ from typing import Any, Dict, List, Optional from rhosocial.activerecord.backend.expression.bases import SQLPredicate +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements.ddl_index import ( CreateIndexExpression, DropIndexExpression, @@ -37,8 +38,8 @@ class PostgresCreateIndexExpression(CreateIndexExpression): def __init__( self, dialect: Any, - index_name: str, - table_name: str, + index: Index, + table: Table, columns: List[Any], unique: bool = False, if_not_exists: bool = False, @@ -54,8 +55,8 @@ def __init__( ): super().__init__( dialect, - index_name=index_name, - table_name=table_name, + index=index, + table=table, columns=columns, unique=unique, if_not_exists=if_not_exists, 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 5aae391d..8e8b32e9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py @@ -20,6 +20,7 @@ from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.expression.objects import MaterializedView from rhosocial.activerecord.backend.expression.statements.ddl_view import ( CreateMaterializedViewExpression, DropMaterializedViewExpression, @@ -56,8 +57,9 @@ def _validate_name(value: str, field_name: str) -> None: class PostgresCreateMaterializedViewExpression(CreateMaterializedViewExpression): """PostgreSQL CREATE MATERIALIZED VIEW with schema and IF NOT EXISTS support. - Extends the generic expression with the two PostgreSQL-specific options: - ``IF NOT EXISTS`` (PG 9.4+) and schema qualification. + Extends the generic expression with the PostgreSQL-specific ``IF NOT EXISTS`` + (PG 9.4+). The view is a :class:`MaterializedView` object, so its schema + lives on the object rather than beside it. ``storage_options`` keys are validated against :class:`~....storage_parameters.PostgresStorageParameter`; pass @@ -65,6 +67,7 @@ class PostgresCreateMaterializedViewExpression(CreateMaterializedViewExpression) a table access method (``USING method``) or namespaced under ``toast.``. Example: + >>> from rhosocial.activerecord.backend.expression.objects import MaterializedView >>> from rhosocial.activerecord.backend.impl.postgres import ( ... PostgresDialect, ... PostgresStorageParameter, @@ -72,9 +75,8 @@ class PostgresCreateMaterializedViewExpression(CreateMaterializedViewExpression) >>> dialect = PostgresDialect(version=(15, 0, 0)) >>> create = PostgresCreateMaterializedViewExpression( ... dialect=dialect, - ... view_name="monthly_sales_summary", + ... view=MaterializedView(dialect, "monthly_sales_summary", schema_name="reporting"), ... query=sales_query, - ... schema="reporting", ... if_not_exists=True, ... storage_options={PostgresStorageParameter.FILLFACTOR: 70}, ... ) @@ -87,45 +89,42 @@ class PostgresCreateMaterializedViewExpression(CreateMaterializedViewExpression) def __init__( self, dialect: "SQLDialectBase", - view_name: str, + view: MaterializedView, query: Any, column_aliases: Optional[List[str]] = None, tablespace: Optional[str] = None, with_data: bool = True, storage_options: Optional[Dict[Any, Any]] = None, - schema: Optional[str] = None, if_not_exists: bool = False, allow_unlisted_storage_parameters: bool = False, ): super().__init__( dialect, - view_name=view_name, + view=view, query=query, column_aliases=column_aliases, tablespace=tablespace, with_data=with_data, storage_options=storage_options, ) - _validate_name(view_name, "view_name") - if schema is not None: - _validate_name(schema, "schema") if storage_options: validate_storage_parameters( storage_options.keys(), "storage_options", allow_unlisted=allow_unlisted_storage_parameters, ) - self.schema = schema self.if_not_exists = if_not_exists self.allow_unlisted_storage_parameters = allow_unlisted_storage_parameters class PostgresDropMaterializedViewExpression(DropMaterializedViewExpression): - """PostgreSQL DROP MATERIALIZED VIEW with schema qualification. + """PostgreSQL DROP MATERIALIZED VIEW. Example: + >>> from rhosocial.activerecord.backend.expression.objects import MaterializedView >>> drop = PostgresDropMaterializedViewExpression( - ... dialect=dialect, view_name="monthly_sales_summary", schema="reporting", + ... dialect=dialect, + ... view=MaterializedView(dialect, "monthly_sales_summary", schema_name="reporting"), ... ) >>> drop.to_sql()[0] 'DROP MATERIALIZED VIEW "reporting"."monthly_sales_summary"' @@ -134,21 +133,16 @@ class PostgresDropMaterializedViewExpression(DropMaterializedViewExpression): def __init__( self, dialect: "SQLDialectBase", - view_name: str, + view: MaterializedView, if_exists: bool = False, cascade: bool = False, - schema: Optional[str] = None, ): super().__init__( dialect, - view_name=view_name, + view=view, if_exists=if_exists, cascade=cascade, ) - _validate_name(view_name, "view_name") - if schema is not None: - _validate_name(schema, "schema") - self.schema = schema class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpression): @@ -157,40 +151,41 @@ class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpressio Replaces the contents of a materialized view by recalculating its query. The data is replaced atomically without affecting concurrent queries. - Extends the generic RefreshMaterializedViewExpression with PostgreSQL-specific - schema support and backward-compatible aliases. + Extends the generic expression with the backward-compatible aliases + ``name`` and ``concurrently``. Attributes: - view_name: Name of the materialized view to refresh (inherited). - schema: Schema name for the materialized view (PostgreSQL-specific). + view: The materialized view being refreshed (inherited); carries its + own schema. concurrent: Whether to refresh concurrently (inherited). with_data: Whether to repopulate data (inherited). - name: Alias for view_name (backward compatibility). - concurrently: Alias for concurrent (backward compatibility). + name: Alias for the view's name. + concurrently: Alias for concurrent. Example: + >>> from rhosocial.activerecord.backend.expression.objects import MaterializedView >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Regular refresh >>> refresh = PostgresRefreshMaterializedViewExpression( ... dialect=dialect, - ... name="monthly_sales_summary", + ... view=MaterializedView(dialect, "monthly_sales_summary"), ... ) >>> sql, params = refresh.to_sql() >>> sql - "REFRESH MATERIALIZED VIEW monthly_sales_summary" + 'REFRESH MATERIALIZED VIEW "monthly_sales_summary"' >>> # Concurrent refresh (PG 9.4+, requires unique index) >>> refresh = PostgresRefreshMaterializedViewExpression( ... dialect=dialect, - ... name="monthly_sales_summary", + ... view=MaterializedView(dialect, "monthly_sales_summary"), ... concurrently=True, ... ) >>> # Refresh without data (create empty, PG 9.4+) >>> refresh = PostgresRefreshMaterializedViewExpression( ... dialect=dialect, - ... name="monthly_sales_summary", + ... view=MaterializedView(dialect, "monthly_sales_summary"), ... with_data=False, ... ) @@ -199,26 +194,26 @@ class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpressio def __init__( self, dialect: "SQLDialectBase", - name: str, - schema: Optional[str] = None, + view: MaterializedView, concurrently: bool = False, with_data: Optional[bool] = None, ): super().__init__( dialect, - view_name=name, + view=view, concurrent=concurrently, with_data=with_data, ) - _validate_name(name, "name") - if schema is not None: - _validate_name(schema, "schema") - self.schema = schema @property def name(self) -> str: - """Alias for view_name (backward compatibility).""" - return self.view_name + """Alias for the view's unqualified name.""" + return self.view.name + + @property + def schema(self) -> Optional[str]: + """The view's schema, read off the object.""" + return self.view.schema_name @property def concurrently(self) -> bool: @@ -355,7 +350,7 @@ class PostgresAlterMaterializedViewExpression(BaseExpression): >>> dialect = PostgresDialect(version=(15, 0, 0)) >>> alter = PostgresAlterMaterializedViewExpression( ... dialect=dialect, - ... view_name="sales_summary", + ... view=MaterializedView(dialect, "sales_summary"), ... actions=[ ... PostgresRenameMaterializedViewAction(dialect, "sales_summary_v2"), ... PostgresSetMaterializedViewPropertiesAction(dialect, {"fillfactor": 90}), @@ -369,14 +364,10 @@ class PostgresAlterMaterializedViewExpression(BaseExpression): def __init__( self, dialect: "SQLDialectBase", - view_name: str, + view: MaterializedView, actions: Sequence[MaterializedViewAlterAction], - schema: Optional[str] = None, ) -> None: super().__init__(dialect) - _validate_name(view_name, "view_name") - if schema is not None: - _validate_name(schema, "schema") action_list = list(actions or []) if not action_list: raise ValueError("actions must contain at least one MaterializedViewAlterAction") @@ -386,8 +377,7 @@ def __init__( "actions must contain MaterializedViewAlterAction instances, " f"got {type(action).__name__}" ) - self.view_name = view_name - self.schema = schema + self.view = view self.actions = action_list @property 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 25709b1f..9232182b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py @@ -9,6 +9,7 @@ from typing import Any, Dict, Mapping, Optional, Sequence, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.expression.objects import Type from rhosocial.activerecord.backend.expression.statements.ddl_type import ( AlterTypeExpression, CreateTypeExpression, @@ -492,19 +493,13 @@ class PostgresDropTypeExpression(DropTypeExpression): def __init__( self, dialect: "SQLDialectBase", - type_name: str, + type: Type, *, - schema_name: Optional[str] = None, if_exists: bool = False, cascade: bool = False, restrict: bool = False, ) -> None: - super().__init__( - dialect, - type_name, - schema_name=schema_name, - if_exists=if_exists, - ) + super().__init__(dialect, type, if_exists=if_exists) self.cascade = cascade self.restrict = restrict @@ -521,9 +516,8 @@ def __init__( definition = PostgresEnumTypeDefinition(dialect, values) super().__init__( dialect, - name, + Type(dialect, name, schema_name=schema), definition, - schema_name=schema, if_not_exists=if_not_exists, ) self.name = name @@ -543,8 +537,7 @@ def __init__( ) -> None: super().__init__( dialect, - name, - schema_name=schema, + Type(dialect, name, schema_name=schema), if_exists=if_exists, cascade=cascade, restrict=restrict, @@ -579,7 +572,9 @@ def __init__( after=after, if_not_exists=if_not_exists, ) - super().__init__(dialect, type_name, [action], schema_name=schema) + super().__init__( + dialect, Type(dialect, type_name, schema_name=schema), [action] + ) self.type_name = type_name self.new_value = new_value self.schema = schema @@ -613,9 +608,8 @@ def __init__( raise ValueError("New value cannot be empty") super().__init__( dialect, - type_name, + Type(dialect, type_name, schema_name=schema), [PostgresRenameEnumValueAction(dialect, old_value, new_value)], - schema_name=schema, ) self.type_name = type_name self.old_value = old_value @@ -649,9 +643,8 @@ def __init__( ) super().__init__( dialect, - name, + Type(dialect, name, schema_name=schema), definition, - schema_name=schema, if_not_exists=if_not_exists, ) self.name = name diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/orafce.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/orafce.py index 398b6443..efd3f39f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/orafce.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/orafce.py @@ -15,8 +15,10 @@ CREATE EXTENSION IF NOT EXISTS orafce; Note: orafce functions are installed in the "oracle" schema, so all function -calls are schema-qualified (e.g., oracle.ADD_MONTHS) to ensure they resolve -correctly regardless of the current search_path setting. +calls are schema-qualified to ensure they resolve correctly regardless of the +current search_path setting. That qualification is produced by the dialect +(:meth:`PostgresOrafceMixin.orafce_function_name`); these factories pass only +the routine's own name. Supported functions: - Date functions: ADD_MONTHS, LAST_DAY, MONTHS_BETWEEN, NEXT_DAY @@ -36,6 +38,7 @@ if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase + from ..mixins.extensions.orafce import PostgresOrafceMixin def _convert_to_expression( @@ -87,7 +90,8 @@ def add_months( >>> add_months(dialect, '2024-01-31', 1) """ return core.FunctionCall( - dialect, "oracle.ADD_MONTHS", + dialect, + dialect.orafce_function_name("ADD_MONTHS"), _convert_to_expression(dialect, date_expr), _convert_to_expression(dialect, months), ) @@ -114,7 +118,8 @@ def last_day( >>> last_day(dialect, '2024-01-01') """ return core.FunctionCall( - dialect, "oracle.LAST_DAY", + dialect, + dialect.orafce_function_name("LAST_DAY"), _convert_to_expression(dialect, date_expr), ) @@ -144,7 +149,8 @@ def months_between( >>> months_between(dialect, '2024-06-20', '2024-01-10') """ return core.FunctionCall( - dialect, "oracle.MONTHS_BETWEEN", + dialect, + dialect.orafce_function_name("MONTHS_BETWEEN"), _convert_to_expression(dialect, date1), _convert_to_expression(dialect, date2), ) @@ -173,7 +179,8 @@ def next_day( >>> next_day(dialect, '2024-06-15', 'FRIDAY') """ return core.FunctionCall( - dialect, "oracle.NEXT_DAY", + dialect, + dialect.orafce_function_name("NEXT_DAY"), _convert_to_expression(dialect, date_expr), _convert_to_expression(dialect, day), ) @@ -204,7 +211,8 @@ def nvl( >>> nvl(dialect, 'discount', '0') """ return core.FunctionCall( - dialect, "oracle.NVL", + dialect, + dialect.orafce_function_name("NVL"), _convert_to_expression(dialect, expr1), _convert_to_expression(dialect, expr2), ) @@ -235,7 +243,8 @@ def nvl2( >>> nvl2(dialect, 'email', "'has email'", "'no email'") """ return core.FunctionCall( - dialect, "oracle.NVL2", + dialect, + dialect.orafce_function_name("NVL2"), _convert_to_expression(dialect, expr1), _convert_to_expression(dialect, expr2), _convert_to_expression(dialect, expr3), @@ -279,7 +288,11 @@ def decode( args.append(_convert_to_expression(dialect, m)) if default is not None: args.append(_convert_to_expression(dialect, default)) - return core.FunctionCall(dialect, "oracle.DECODE", *args) + return core.FunctionCall( + dialect, + dialect.orafce_function_name("DECODE"), + *args, + ) # ============== Numeric Functions ============== @@ -313,7 +326,11 @@ def orafce_trunc( args = [_convert_to_expression(dialect, value)] if format is not None: args.append(_convert_to_expression(dialect, format)) - return core.FunctionCall(dialect, "oracle.TRUNC", *args) + return core.FunctionCall( + dialect, + dialect.orafce_function_name("TRUNC"), + *args, + ) def orafce_round( @@ -345,7 +362,11 @@ def orafce_round( args = [_convert_to_expression(dialect, value)] if format is not None: args.append(_convert_to_expression(dialect, format)) - return core.FunctionCall(dialect, "oracle.ROUND", *args) + return core.FunctionCall( + dialect, + dialect.orafce_function_name("ROUND"), + *args, + ) # ============== String Functions ============== @@ -378,7 +399,8 @@ def instr( >>> instr(dialect, 'description', "'the'", position=1, occurrence=2) """ return core.FunctionCall( - dialect, "oracle.INSTR", + dialect, + dialect.orafce_function_name("INSTR"), _convert_to_expression(dialect, string_expr), _convert_to_expression(dialect, substring_expr), _convert_to_expression(dialect, position), @@ -419,7 +441,11 @@ def substr( ] if length is not None: args.append(_convert_to_expression(dialect, length)) - return core.FunctionCall(dialect, "oracle.SUBSTR", *args) + return core.FunctionCall( + dialect, + dialect.orafce_function_name("SUBSTR"), + *args, + ) __all__ = [ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_repack.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_repack.py index d84fb131..d68bbb5d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_repack.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_repack.py @@ -86,7 +86,10 @@ def repack_version( >>> repack_version(dialect) # Generates: repack.repack_version() """ - return core.FunctionCall(dialect, "repack.repack_version") + return core.FunctionCall( + dialect, + dialect.pg_repack_function_name("repack_version"), + ) __all__ = [ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py index e46172dd..61e3870c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/vector_search.py @@ -23,8 +23,8 @@ Column, Literal, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.core import ( BaseExpression, WildcardExpression, @@ -146,7 +146,7 @@ def vector_search( return QueryExpression( dialect=dialect, select=select, - from_=TableExpression(dialect, table), + from_=Table(dialect, table), where=where, order_by=OrderByClause(dialect, [distance_expr]), limit_offset=LimitOffsetClause(dialect, limit=top_k), @@ -207,8 +207,8 @@ def create_vector_index( return PostgresCreateIndexExpression( dialect=dialect, - index_name=idx_name, - table_name=table_name, + index=Index(dialect, idx_name), + table=Table(dialect, table_name), columns=[column_name], index_type=index_type_l.upper(), if_not_exists=if_not_exists, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py index 4a9fac6b..54aa8647 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py @@ -11,6 +11,7 @@ - extensions: Extension mixins (pgvector, postgis, hstore, etc.) """ +from .object_name import PostgresObjectNameMixin from .extension import PostgresExtensionMixin from .materialized_view import PostgresMaterializedViewMixin from .ddl_table import PostgresTableMixin @@ -112,6 +113,11 @@ from .truncate import PostgresTruncateMixin from .ddl_database import PostgresDatabaseMixin from .schema import PostgresSchemaMixin +from .namespace import PostgresNamespaceMixin + +# Deprecated alias of PostgresNamespaceMixin; kept importable so an out-of-tree +# dialect that inherited the old name keeps working. +from .catalog import PostgresCatalogMixin from .sequence import PostgresSequenceMixin from .transaction import PostgresTransactionMixin from .ddl_view import PostgresViewMixin @@ -219,6 +225,8 @@ "PostgresJoinMixin", "PostgresTruncateMixin", "PostgresSchemaMixin", + "PostgresNamespaceMixin", + "PostgresCatalogMixin", # deprecated alias of PostgresNamespaceMixin "PostgresDatabaseMixin", "PostgresSequenceMixin", "PostgresTransactionMixin", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/catalog.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/catalog.py new file mode 100644 index 00000000..f34e2841 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/catalog.py @@ -0,0 +1,24 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/catalog.py +"""Deprecated import path for the PostgreSQL naming-side mixin. + +The naming-side answers used to be split: ``PostgresCatalogMixin`` held the two +outer-namespace switches and ``PostgresSchemaMixin`` held ``supports_schema_ +qualification`` next to its DDL siblings. That split is now +:class:`~...mixins.namespace.PostgresNamespaceMixin`, which holds all of them +plus PostgreSQL's own ``format_qualified_name``. + +This module keeps the old name importable as an alias rather than deleting it, so +an out-of-tree dialect that inherited ``PostgresCatalogMixin`` keeps working. +""" + +from .namespace import PostgresNamespaceMixin + +#: Alias of :class:`~...mixins.namespace.PostgresNamespaceMixin`. +#: +#: One class under two names, not two classes: a subclass here would put a second +#: definition of the same switches into ``PostgresDialect``'s MRO and the loser +#: would be whichever one happened to be listed second -- which is the failure +#: mode that produced Snowflake's duplicate ``supports_schema``. +PostgresCatalogMixin = PostgresNamespaceMixin + +__all__ = ["PostgresCatalogMixin"] \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py index bbd40943..268894a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py @@ -1,6 +1,8 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/collation.py """PostgreSQL collation feature support implementation.""" +from rhosocial.activerecord.backend.expression.objects import SchemaObject + class PostgresCollationMixin: """PostgreSQL collation override implementation. @@ -22,6 +24,6 @@ def validate_collation_name(self, expr) -> str: 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) + return self._format_schema_object_name( + SchemaObject(self, expr.collation_name, schema_name=schema) + ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/cluster.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/cluster.py index a56e64a7..96fdc4f3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/cluster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/cluster.py @@ -10,6 +10,7 @@ from typing import List, Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Table if TYPE_CHECKING: from ...expression.ddl.cluster import PostgresClusterExpression @@ -31,13 +32,14 @@ def supports_cluster(self) -> bool: def _format_cluster_table_ref( self, schema: Optional[str], table_name: str ) -> str: - """Format ``table_name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return ( - f"{self.format_identifier(schema)}." - f"{self.format_identifier(table_name)}" - ) - return self.format_identifier(table_name) + """Render the table a CLUSTER statement targets. + + The name is a :class:`Table` schema object rendered by the dialect. + ``CLUSTER`` with no table at all (cluster every table) is handled by + the caller, which never reaches this helper. + """ + sql, _ = self.format_table_object(Table(self, table_name, schema_name=schema)) + return sql # ------------------------------------------------------------------ # # Statement diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/collation.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/collation.py index abee6a37..b5c212a3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/collation.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/collation.py @@ -13,6 +13,7 @@ from typing import List, Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import SchemaObject if TYPE_CHECKING: from ...expression.ddl.collation import ( @@ -35,13 +36,16 @@ def supports_collation_ddl(self) -> bool: # Helpers # ------------------------------------------------------------------ # def _format_collation_ref(self, schema: Optional[str], name: str) -> str: - """Format ``name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return ( - f"{self.format_identifier(schema)}." - f"{self.format_identifier(name)}" - ) - return self.format_identifier(name) + """Render a collation name, optionally schema-qualified. + + A collation has no dedicated kind in the schema-object tree, so it is + carried by the base :class:`SchemaObject`; what the dialect needs + from it -- the three slots and their quoting -- is exactly what the + base provides. + """ + return self._format_schema_object_name( + SchemaObject(self, name, schema_name=schema) + ) # ------------------------------------------------------------------ # # CREATE COLLATION diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/comment.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/comment.py index 9e1627b8..f409ae15 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/comment.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/comment.py @@ -19,16 +19,18 @@ def supports_comment_on(self) -> bool: def format_comment_statement(self, expr) -> Tuple[str, tuple]: """Format COMMENT ON statement (PostgreSQL-specific). - Accepts any comment expression carrying ``object_type``, - ``object_name`` and ``comment``. The comment text is bound - through a parameter placeholder (``IS ?`` / params); ``comment=None`` - renders ``IS NULL`` (clears the comment). Dotted names - (``schema.table`` / ``table.column``) are quoted segment-by-segment - so the rendered target stays a valid qualified reference. + Accepts a :class:`~rhosocial.activerecord.backend.expression.objects.SchemaObject` + holder carrying ``object_type``, the ``object`` being annotated, + ``comment`` and an optional ``column``. The object renders itself, so a + qualified name comes out of the namespace machinery rather than being + assembled here; a ``column`` is appended after it when the target is a + column of that object. The comment text is bound through a parameter + placeholder (``IS ?`` / params); ``comment=None`` renders ``IS NULL`` + (clears the comment). Args: - expr: PostgresCommentExpression or CommentOnExpression carrying - ``object_type``, ``object_name`` and ``comment``. + expr: ``CommentOnExpression`` carrying ``object_type``, ``object``, + ``comment`` and an optional ``column``. Returns: Tuple of (SQL string, parameters) @@ -38,16 +40,17 @@ def format_comment_statement(self, expr) -> Tuple[str, tuple]: else: comment_value = self.get_parameter_placeholder() - object_sql = ".".join( - self.format_identifier(part) for part in str(expr.object_name).split(".") - ) - parts = ["COMMENT ON", expr.object_type, object_sql] + object_sql, object_params = expr.object.to_sql() + if expr.column: + object_sql = f"{object_sql}.{self.format_identifier(expr.column)}" + + object_type = getattr(expr.object_type, "value", expr.object_type) + parts = ["COMMENT ON", object_type, object_sql] parts.append("IS") parts.append(comment_value) sql = " ".join(parts) if expr.comment is None: - return sql, () - else: - return sql, (expr.comment,) + return sql, object_params + return sql, tuple(object_params) + (expr.comment,) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py index 4de201ef..9b945403 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py @@ -7,6 +7,7 @@ from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.dialect.mixins.ddl_domain import DomainMixin +from rhosocial.activerecord.backend.expression.objects import Domain from rhosocial.activerecord.backend.expression.statements.ddl_domain import ( AddDomainCheckAction, AlterDomainExpression, @@ -142,6 +143,17 @@ def format_create_domain_statement( self, expr: CreateDomainExpression, ) -> Tuple[str, tuple]: + """Render ``CREATE DOMAIN``, with the PostgreSQL-only options. + + Raises: + TypeError: ``expr.domain`` is not a :class:`Domain`. Another object + kind would have had its own name rendered as the domain's. + """ + if not isinstance(expr.domain, Domain): + raise TypeError( + f"CreateDomainExpression.domain must be a Domain, " + f"got {type(expr.domain).__name__}" + ) if not self.supports_domains() or not self.supports_create_domain(): raise UnsupportedFeatureError( self.name, @@ -153,10 +165,7 @@ def format_create_domain_statement( raise ValueError("DOMAIN data types must render without bind parameters") parts = [ "CREATE DOMAIN", - self._format_domain_identifier( - expr.domain_name, - getattr(expr, "schema_name", None), - ), + expr.domain.to_sql()[0], "AS", type_sql, ] @@ -202,6 +211,16 @@ def format_alter_domain_statement( self, expr: AlterDomainExpression, ) -> Tuple[str, tuple]: + """Render ``ALTER DOMAIN``. + + Raises: + TypeError: ``expr.domain`` is not a :class:`Domain`. + """ + if not isinstance(expr.domain, Domain): + raise TypeError( + f"AlterDomainExpression.domain must be a Domain, " + f"got {type(expr.domain).__name__}" + ) if not self.supports_domains() or not self.supports_alter_domain(): raise UnsupportedFeatureError( self.name, @@ -223,12 +242,8 @@ def format_alter_domain_statement( action_sql, params = action.to_sql() action_parts.append(action_sql) action_params.extend(params) - domain_ref = self._format_domain_identifier( - expr.domain_name, - getattr(expr, "schema_name", None), - ) return ( - f"ALTER DOMAIN {domain_ref} {', '.join(action_parts)}", + f"ALTER DOMAIN {expr.domain.to_sql()[0]} {', '.join(action_parts)}", tuple(action_params), ) @@ -242,6 +257,16 @@ def format_drop_domain_statement( self, expr: DropDomainExpression, ) -> Tuple[str, tuple]: + """Render ``DROP DOMAIN``, with the PostgreSQL-only options. + + Raises: + TypeError: ``expr.domain`` is not a :class:`Domain`. + """ + if not isinstance(expr.domain, Domain): + raise TypeError( + f"DropDomainExpression.domain must be a Domain, " + f"got {type(expr.domain).__name__}" + ) if not self.supports_domains() or not self.supports_drop_domain(): raise UnsupportedFeatureError( self.name, @@ -261,12 +286,7 @@ def format_drop_domain_statement( parts = ["DROP DOMAIN"] if if_exists: parts.append("IF EXISTS") - parts.append( - self._format_domain_identifier( - expr.domain_name, - getattr(expr, "schema_name", None), - ) - ) + parts.append(expr.domain.to_sql()[0]) if behavior: parts.append(behavior) return " ".join(parts), () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/foreign_table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/foreign_table.py index 8cecf8ac..2e6ad43b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/foreign_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/foreign_table.py @@ -10,6 +10,7 @@ from typing import List, Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import ForeignTable if TYPE_CHECKING: from ...expression.ddl.foreign_table import ( @@ -34,13 +35,14 @@ def supports_foreign_table_ddl(self) -> bool: def _format_foreign_table_ref( self, schema: Optional[str], name: str ) -> str: - """Format ``name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return ( - f"{self.format_identifier(schema)}." - f"{self.format_identifier(name)}" - ) - return self.format_identifier(name) + """Render the foreign table a FOREIGN TABLE statement names. + + The name is a :class:`ForeignTable` schema object rendered by the + dialect, so the qualification and quoting follow the shared rules + instead of a second copy of them. + """ + sql, _ = self.format_foreign_table_object(ForeignTable(self, name, schema_name=schema)) + return sql # ------------------------------------------------------------------ # # CREATE FOREIGN TABLE 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 afcc4d99..4986f471 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py @@ -8,6 +8,7 @@ from typing import Any, Dict, Optional, Tuple, List, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression.bases import ToSQLProtocol +from rhosocial.activerecord.backend.expression.objects import Index, Table if TYPE_CHECKING: # pragma: no cover from ...expression.ddl import ( @@ -141,7 +142,7 @@ def supports_streaming_btree_index_build(self) -> bool: return self.version >= (18, 0, 0) # ========================================================================= - # IndexSupport Protocol Overrides (all supported by PostgreSQL) + # IndexObjectSupport Protocol Overrides (all supported by PostgreSQL) # ========================================================================= def supports_create_index(self) -> bool: @@ -291,14 +292,32 @@ def format_create_fulltext_index_statement(self, expr) -> Tuple[str, tuple]: Returns: Tuple of (SQL string, parameters tuple) + + Raises: + TypeError: ``expr.index`` is not an :class:`Index`, or ``expr.table`` + is not a :class:`Table`. PostgreSQL spells a full-text index as a + GIN index, but the two objects still have to be an index and a + table -- without the check, a statement handed a ``View`` here + rendered ``CREATE INDEX ... ON USING GIN``, which is a + well-formed statement naming the wrong kind of object. """ + if not isinstance(expr.index, Index): + raise TypeError( + f"CreateFulltextIndexExpression.index must be an Index, " + f"got {type(expr.index).__name__}" + ) + if not isinstance(expr.table, Table): + raise TypeError( + f"CreateFulltextIndexExpression.table must be a Table, " + f"got {type(expr.table).__name__}" + ) config = self._FT_DEFAULT_CONFIG parts = ["CREATE INDEX"] if expr.if_not_exists: parts.append("IF NOT EXISTS") - parts.append(self.format_identifier(expr.index_name)) + parts.append(expr.index.to_sql()[0]) parts.append("ON") - parts.append(self.format_identifier(expr.table_name)) + parts.append(expr.table.to_sql()[0]) parts.append("USING GIN") if len(expr.columns) == 1: @@ -321,11 +340,27 @@ def format_drop_fulltext_index_statement(self, expr) -> Tuple[str, tuple]: Returns: Tuple of (SQL string, parameters tuple) + + Raises: + TypeError: ``expr.index`` is not an :class:`Index`. """ + if not isinstance(expr.index, Index): + raise TypeError( + f"DropFulltextIndexExpression.index must be an Index, " + f"got {type(expr.index).__name__}" + ) parts = ["DROP INDEX"] if expr.if_exists: parts.append("IF EXISTS") - parts.append(self.format_identifier(expr.index_name)) + parts.append(expr.index.to_sql()[0]) + # ``expr.table`` is deliberately not rendered -- PostgreSQL's DROP INDEX + # names the index alone, and a GIN full-text index is no exception. It is + # still checked, because a caller who set it meant something by it. + if expr.table is not None and not isinstance(expr.table, Table): + raise TypeError( + f"DropFulltextIndexExpression.table must be a Table, " + f"got {type(expr.table).__name__}" + ) return " ".join(parts), () def format_create_index_statement(self, expr: "CreateIndexExpression") -> Tuple[str, tuple]: @@ -337,7 +372,23 @@ def format_create_index_statement(self, expr: "CreateIndexExpression") -> Tuple[ - INCLUDE clause with version checks - NULLS NOT DISTINCT via the typed ``nulls_not_distinct`` field - WITH storage parameters via the typed ``with_options`` field + + Raises: + TypeError: ``expr.index`` is not an :class:`Index`, or ``expr.table`` + is not a :class:`Table`. Either would have had its own name + rendered through its own ``format_method``, producing a + well-formed CREATE INDEX naming the wrong kind of object. """ + if not isinstance(expr.index, Index): + raise TypeError( + f"CreateIndexExpression.index must be an Index, " + f"got {type(expr.index).__name__}" + ) + if not isinstance(expr.table, Table): + raise TypeError( + f"CreateIndexExpression.table must be a Table, " + f"got {type(expr.table).__name__}" + ) all_params = [] parts = ["CREATE"] @@ -353,9 +404,9 @@ def format_create_index_statement(self, expr: "CreateIndexExpression") -> Tuple[ if expr.if_not_exists: parts.append("IF NOT EXISTS") - parts.append(self.format_identifier(expr.index_name)) + parts.append(expr.index.to_sql()[0]) parts.append("ON") - parts.append(self.format_identifier(expr.table_name)) + parts.append(expr.table.to_sql()[0]) if expr.index_type: parts.append(f"USING {expr.index_type}") @@ -421,7 +472,16 @@ def format_drop_index_statement(self, expr: "DropIndexExpression") -> Tuple[str, Extends the generic format_drop_index_statement to support: - CONCURRENTLY clause (PG 18+) via the typed ``concurrent`` field + + Raises: + TypeError: ``expr.index`` is not an :class:`Index`. Another object + kind would have had its own name rendered as the index's. """ + if not isinstance(expr.index, Index): + raise TypeError( + f"DropIndexExpression.index must be an Index, " + f"got {type(expr.index).__name__}" + ) parts = ["DROP INDEX"] if getattr(expr, "concurrent", False): @@ -432,9 +492,17 @@ def format_drop_index_statement(self, expr: "DropIndexExpression") -> Tuple[str, if expr.if_exists: parts.append("IF EXISTS") - parts.append(self.format_identifier(expr.index_name)) + parts.append(expr.index.to_sql()[0]) - # table_name is ignored for PostgreSQL (unlike MySQL) + # ``expr.table`` is optional and deliberately ignored for PostgreSQL: + # ``DROP INDEX`` names the index alone, and no ON clause exists. It is + # still checked, because a caller who set it means something by it -- but + # the check is on kind, not on presence, so leaving it unset is fine. + if expr.table is not None and not isinstance(expr.table, Table): + raise TypeError( + f"DropIndexExpression.table must be a Table, " + f"got {type(expr.table).__name__}" + ) return " ".join(parts), () def format_alter_index_statement( @@ -563,12 +631,23 @@ def format_reindex_statement(self, expr: "PostgresReindexExpression") -> Tuple[s parts.append(target_type) - # Add schema qualifier for INDEX and TABLE - if target_type in ("INDEX", "TABLE") and expr.schema: - parts.append(f"{self.format_identifier(expr.schema)}.") - - # Add name (format as identifier) - parts.append(self.format_identifier(expr.name)) + # INDEX and TABLE take a schema qualifier; the other target types + # (SCHEMA, DATABASE, SYSTEM) are not objects inside a schema and + # must not be qualified with one. + if target_type == "INDEX": + parts.append( + self.format_index_object( + Index(self, expr.name, schema_name=expr.schema) + )[0] + ) + elif target_type == "TABLE": + parts.append( + self.format_table_object( + Table(self, expr.name, schema_name=expr.schema) + )[0] + ) + else: + parts.append(self.format_identifier(expr.name)) # TABLESPACE option (PG 14+) if expr.tablespace: @@ -624,18 +703,17 @@ def format_create_index_pg_statement( if if_not_exists: parts.append("IF NOT EXISTS") - # Index name with optional schema - if schema: - parts.append(f"{self.format_identifier(schema)}.{self.format_identifier(index_name)}") - else: - parts.append(self.format_identifier(index_name)) + # Index name and its target table, each through the shared renderer so + # the two agree on qualification and quoting. + parts.append( + self.format_index_object(Index(self, index_name, schema_name=schema))[0] + ) # ON table parts.append("ON") - if schema: - parts.append(f"{self.format_identifier(schema)}.{self.format_identifier(table_name)}") - else: - parts.append(self.format_identifier(table_name)) + parts.append( + self.format_table_object(Table(self, table_name, schema_name=schema))[0] + ) # Index type index_type = index_type.lower() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/partition.py index 2d2e9edf..e455cf4c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/partition.py @@ -12,6 +12,7 @@ from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.expression.bases import BaseExpression from rhosocial.activerecord.backend.expression.core import Literal +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import PartitionClause if TYPE_CHECKING: @@ -30,6 +31,19 @@ class PostgresPartitionMixin: All features are native, using version number for detection. """ + def _format_partition_table_ref( + self, schema: Optional[str], table_name: str + ) -> str: + """Render a partition or its parent table. + + Both sides of every partition statement -- CREATE ... PARTITION OF, + ATTACH/DETACH PARTITION -- are ordinary tables living in a schema, so + both go through the shared schema-object renderer and cannot drift + apart from each other or from the rest of the dialect. + """ + sql, _ = self.format_table_object(Table(self, table_name, schema_name=schema)) + return sql + def supports_table_partitioning(self) -> bool: """Check if declarative table partitioning is supported. @@ -456,23 +470,15 @@ def format_create_partition_statement( parts = ["CREATE TABLE"] if expr.if_not_exists: parts.append("IF NOT EXISTS") - if expr.schema: - child_name = ( - f"{self.format_identifier(expr.schema)}." - f"{self.format_identifier(expr.partition_name)}" - ) - else: - child_name = self.format_identifier(expr.partition_name) + child_name = self._format_partition_table_ref( + expr.schema, expr.partition_name + ) parts.append(child_name) parent_schema = expr.parent_schema if expr.parent_schema is not None else expr.schema - if parent_schema: - parent_name = ( - f"{self.format_identifier(parent_schema)}." - f"{self.format_identifier(expr.parent_table)}" - ) - else: - parent_name = self.format_identifier(expr.parent_table) + parent_name = self._format_partition_table_ref( + parent_schema, expr.parent_table + ) parts.append(f"PARTITION OF {parent_name}") params: List[Any] = [] @@ -581,22 +587,17 @@ def format_detach_partition_statement( parts = ["ALTER TABLE"] parent_schema = expr.parent_schema if expr.parent_schema is not None else expr.schema - if parent_schema: - parts.append( - f"{self.format_identifier(parent_schema)}." - f"{self.format_identifier(expr.parent_table)}" - ) - else: - parts.append(self.format_identifier(expr.parent_table)) + parts.append( + self._format_partition_table_ref(parent_schema, expr.parent_table) + ) if expr.concurrently and not self.supports_concurrent_detach(): raise ValueError("DETACH CONCURRENTLY requires PostgreSQL 14+") parts.append("DETACH PARTITION") - if expr.schema: - parts.append(f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.partition_name)}") - else: - parts.append(self.format_identifier(expr.partition_name)) + parts.append( + self._format_partition_table_ref(expr.schema, expr.partition_name) + ) if expr.concurrently: parts.append("CONCURRENTLY") @@ -646,20 +647,12 @@ def format_attach_partition_statement( parts = ["ALTER TABLE"] parent_schema = expr.parent_schema if expr.parent_schema is not None else expr.schema - if parent_schema: - parent_name = ( - f"{self.format_identifier(parent_schema)}." - f"{self.format_identifier(expr.parent_table)}" - ) - else: - parent_name = self.format_identifier(expr.parent_table) - if expr.schema: - partition_name = ( - f"{self.format_identifier(expr.schema)}." - f"{self.format_identifier(expr.partition_name)}" - ) - else: - partition_name = self.format_identifier(expr.partition_name) + parent_name = self._format_partition_table_ref( + parent_schema, expr.parent_table + ) + partition_name = self._format_partition_table_ref( + expr.schema, expr.partition_name + ) parts.extend((parent_name, "ATTACH PARTITION", partition_name)) params: List[Any] = [] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/policy.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/policy.py index efedabf2..d05d694b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/policy.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/policy.py @@ -16,6 +16,7 @@ from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.expression.bases import SQLPredicate +from rhosocial.activerecord.backend.expression.objects import Table from ...expression.ddl.policy import ( AlterPolicyMode, @@ -89,10 +90,14 @@ def supports_current_role_keyword(self) -> bool: # Shared helpers # ------------------------------------------------------------------ # def _format_table_ref(self, schema: Optional[str], table_name: str) -> str: - """Format ``table_name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return f"{self.format_identifier(schema)}.{self.format_identifier(table_name)}" - return self.format_identifier(table_name) + """Render the table a POLICY is attached to. + + The name is a :class:`Table` schema object rendered by the dialect, so + the policy statements quote and qualify it exactly as every other + table reference does. + """ + sql, _ = self.format_table_object(Table(self, table_name, schema_name=schema)) + return sql def _format_role_list(self, roles: List[str]) -> Tuple[str, tuple]: """Format the ``TO role [, ...]`` clause. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/rls_config.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/rls_config.py index d606a713..88e4b150 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/rls_config.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/rls_config.py @@ -13,6 +13,7 @@ from typing import Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Table if TYPE_CHECKING: from ...expression.ddl.rls_config import ( @@ -45,13 +46,13 @@ def supports_rls_enable_always(self) -> bool: def _format_rls_table_ref( self, schema: Optional[str], table_name: str ) -> str: - """Format ``table_name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return ( - f"{self.format_identifier(schema)}." - f"{self.format_identifier(table_name)}" - ) - return self.format_identifier(table_name) + """Render the table whose row-level security is being toggled. + + The name is a :class:`Table` schema object rendered by the dialect, so + both RLS forms quote and qualify the table identically. + """ + sql, _ = self.format_table_object(Table(self, table_name, schema_name=schema)) + return sql # ------------------------------------------------------------------ # # ENABLE / DISABLE diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py index a70b1df3..e42a5b69 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py @@ -13,6 +13,8 @@ from typing import List, Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.dialect.mixins.function import FunctionMixin +from rhosocial.activerecord.backend.expression.objects import Function if TYPE_CHECKING: from ...expression.ddl.routine import ( @@ -21,10 +23,31 @@ PostgresDropAggregateExpression, PostgresDropFunctionExpression, ) + from rhosocial.activerecord.backend.expression.statements import ( + CreateFunctionExpression, + DropFunctionExpression, + ) class PostgresRoutineMixin: - """PostgreSQL FUNCTION / AGGREGATE DDL implementation.""" + """PostgreSQL FUNCTION / AGGREGATE DDL implementation. + + Two spellings of each statement live in this backend and they are not the + same thing. ``PostgresCreateFunctionExpression`` and + ``PostgresDropFunctionExpression`` are the PostgreSQL-owned ones, with the + richer option set (``SECURITY``, ``COST``, ``ROWS``, ``STRICT``) rendered by + :meth:`format_create_function_ddl_statement` below. Core's own + ``CreateFunctionExpression`` / ``DropFunctionExpression`` carry the portable + SQL/PSM subset, and are rendered by delegating to core's ``FunctionMixin``. + + Both are needed: :class:`PostgresDialect` declares ``CreateRoutineSupport`` + and ``DropRoutineSupport`` in its base list, and a ``runtime_checkable`` + protocol is satisfied by its own ``...`` bodies. With no implementation + anywhere, ``CreateFunctionExpression(...).to_sql()`` called the stub and + returned ``None``, and ``supports_function()`` returned ``None`` too -- both + read as falsy to every caller, and neither raised. The switches and the two + delegating formatters below are what make that declaration true. + """ # ------------------------------------------------------------------ # # Capability switches @@ -37,17 +60,118 @@ def supports_aggregate_ddl(self) -> bool: """CREATE/DROP AGGREGATE require PostgreSQL 9.6+.""" return self.version >= (9, 6, 0) + def supports_function(self) -> bool: + """PostgreSQL has user-defined functions. + + Unconditional. ``CREATE FUNCTION`` predates every version this backend + targets; :meth:`supports_function_ddl` gates the *tracked* form, not the + statement, and reporting ``False`` here would make + ``isinstance(dialect, CreateRoutineSupport)``-style checks pass while + every function statement raised. + """ + return True + + def supports_create_function(self) -> bool: + """Whether CREATE FUNCTION is supported. + + Tied to :meth:`supports_function_ddl` so the two cannot disagree: the + core renderer asks this before emitting anything. + """ + return self.supports_function_ddl() + + def supports_drop_function(self) -> bool: + """Whether DROP FUNCTION is supported. Same gate as CREATE.""" + return self.supports_function_ddl() + + def supports_function_or_replace(self) -> bool: + """Whether CREATE OR REPLACE FUNCTION is supported.""" + return self.supports_function_ddl() + + def supports_function_parameters(self) -> bool: + """Whether a named parameter list is supported.""" + return self.supports_function_ddl() + + def supports_drop_function_if_exists(self) -> bool: + """Whether DROP FUNCTION IF EXISTS is supported.""" + return self.supports_function_ddl() + + def supports_drop_function_cascade(self) -> bool: + """Whether DROP FUNCTION CASCADE is supported.""" + return self.supports_function_ddl() + # ------------------------------------------------------------------ # - # Helpers + # Core's portable CREATE/DROP FUNCTION # ------------------------------------------------------------------ # - def _format_routine_ref(self, schema: Optional[str], name: str) -> str: - """Format ``name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return ( - f"{self.format_identifier(schema)}." - f"{self.format_identifier(name)}" + def format_create_function_statement( + self, expr: "CreateFunctionExpression" + ) -> Tuple[str, tuple]: + """Render core's ``CreateFunctionExpression``. + + Delegates to core's renderer instead of restating it, so the object-kind + check and the parameter validation stay in one place -- core is where + that logic lives, and a second copy here would be free to drift. + + Args: + expr: Core's :class:`CreateFunctionExpression`, holding a + :class:`~...expression.objects.Function` rather than a bare name. + + Returns: + A ``(sql, params)`` tuple; ``params`` is empty, because a function + body is DDL and carries no bind parameters. + + Raises: + TypeError: ``expr.function`` is not a ``Function``. + UnsupportedFeatureError: The configured PostgreSQL version predates + 9.6, so this backend does not claim CREATE FUNCTION. + """ + if not isinstance(expr.function, Function): + raise TypeError( + f"CreateFunctionExpression.function must be a Function, " + f"got {type(expr.function).__name__}" + ) + return FunctionMixin.format_create_function_statement(self, expr) + + def format_drop_function_statement( + self, expr: "DropFunctionExpression" + ) -> Tuple[str, tuple]: + """Render core's ``DropFunctionExpression``. + + Delegates to core's renderer for the reason given on + :meth:`format_create_function_statement`. + + Raises: + TypeError: ``expr.function`` is not a ``Function``. + UnsupportedFeatureError: The configured PostgreSQL version predates + 9.6, so this backend does not claim DROP FUNCTION. + """ + if not isinstance(expr.function, Function): + raise TypeError( + f"DropFunctionExpression.function must be a Function, " + f"got {type(expr.function).__name__}" ) - return self.format_identifier(name) + return FunctionMixin.format_drop_function_statement(self, expr) + + # ------------------------------------------------------------------ # + # Helpers + # ------------------------------------------------------------------ # + def _format_routine_ref( + self, + schema: Optional[str], + name: str, + ) -> str: + """Render the routine a CREATE/DROP statement names. + + Args: + schema: The routine's schema, or ``None`` for the default one. + name: The routine's unqualified name. + + Returns: + The quoted, optionally schema-qualified identifier. + """ + sql, _ = self.format_function_object( + Function(self, name, schema_name=schema) + ) + return sql # ------------------------------------------------------------------ # # CREATE FUNCTION diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/table_settings.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/table_settings.py index 29c4277d..802edca0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/table_settings.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/table_settings.py @@ -12,6 +12,7 @@ from typing import Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Table from ...expression.ddl.table_settings import LoggingMode if TYPE_CHECKING: @@ -40,13 +41,14 @@ def supports_table_set_access_method(self) -> bool: def _format_settings_table_ref( self, schema: Optional[str], table_name: str ) -> str: - """Format ``table_name`` (optionally schema-qualified) as identifier(s).""" - if schema: - return ( - f"{self.format_identifier(schema)}." - f"{self.format_identifier(table_name)}" - ) - return self.format_identifier(table_name) + """Render the table a table-level setting is applied to. + + The name is a :class:`Table` schema object rendered by the dialect. + Both settings (``LOGGED``/``UNLOGGED`` and ``ACCESS METHOD``) go + through it, so they cannot drift apart. + """ + sql, _ = self.format_table_object(Table(self, table_name, schema_name=schema)) + return sql # ------------------------------------------------------------------ # # Statement diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py index fd2f2172..dacab5aa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py @@ -1,6 +1,8 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/trigger.py from typing import Tuple +from rhosocial.activerecord.backend.expression.objects import Table, Trigger + class PostgresTriggerMixin: """PostgreSQL trigger DDL implementation. @@ -49,15 +51,16 @@ def format_create_trigger_statement(self, expr) -> Tuple[str, tuple]: Supported expression attributes: - ``expr.if_not_exists`` — add ``IF NOT EXISTS`` (PG 9.5+). - - ``expr.trigger_name`` — trigger name (identifier). + - ``expr.trigger`` — the trigger being created, which renders its own name. - ``expr.timing`` — ``BEFORE``, ``AFTER``, or ``INSTEAD OF``. - ``expr.events`` — list of event types (``INSERT``, ``UPDATE``, ``DELETE``, ``TRUNCATE``). - ``expr.update_columns`` — column list for ``UPDATE OF``. - - ``expr.table_name`` — target table (identifier). + - ``expr.table`` — the target table, which renders its own name. - ``expr.referencing`` — ``REFERENCING`` clause string (PG 10+). - ``expr.level`` — ``FOR EACH ROW`` or ``FOR EACH STATEMENT``. - ``expr.condition`` — ``WHEN`` predicate expression. - - ``expr.function_name`` — function to execute. + - ``expr.function`` — the function to execute, which renders its own + name. Args: expr: Expression instance with trigger attributes @@ -71,7 +74,7 @@ def format_create_trigger_statement(self, expr) -> Tuple[str, tuple]: if expr.if_not_exists and self.supports_trigger_if_not_exists(): parts.append("IF NOT EXISTS") - parts.append(self.format_identifier(expr.trigger_name)) + parts.append(expr.trigger.to_sql()[0]) parts.append(expr.timing.value) @@ -83,7 +86,7 @@ def format_create_trigger_statement(self, expr) -> Tuple[str, tuple]: parts.append(events_str) parts.append("ON") - parts.append(self.format_identifier(expr.table_name)) + parts.append(expr.table.to_sql()[0]) if expr.referencing and self.supports_trigger_referencing(): parts.append(expr.referencing) @@ -98,7 +101,7 @@ def format_create_trigger_statement(self, expr) -> Tuple[str, tuple]: all_params.extend(cond_params) parts.append("EXECUTE FUNCTION") - parts.append(f"{self.format_identifier(expr.function_name)}()") + parts.append(f"{expr.function.to_sql()[0]}()") return " ".join(parts), tuple(all_params) @@ -106,8 +109,8 @@ def format_drop_trigger_statement(self, expr) -> Tuple[str, tuple]: """Format DROP TRIGGER statement (PostgreSQL syntax). - ``expr.if_exists`` — add ``IF EXISTS``. - - ``expr.trigger_name`` — trigger name (identifier). - - ``expr.table_name`` — optional ``ON table_name`` clause. + - ``expr.trigger`` — the trigger being dropped, which renders its own name. + - ``expr.table`` — optional ``ON `` clause. Args: expr: Expression instance with trigger attributes @@ -115,16 +118,31 @@ def format_drop_trigger_statement(self, expr) -> Tuple[str, tuple]: Returns: Tuple of (SQL string, empty params tuple) + Raises: + TypeError: ``expr.trigger`` is not a :class:`Trigger`, or + ``expr.table`` is set and is not a :class:`Table`. The table slot + is optional -- PostgreSQL's ``DROP TRIGGER`` can name the trigger + alone -- so its check is on kind, not on presence. """ + if not isinstance(expr.trigger, Trigger): + raise TypeError( + f"DropTriggerExpression.trigger must be a Trigger, " + f"got {type(expr.trigger).__name__}" + ) + if expr.table is not None and not isinstance(expr.table, Table): + raise TypeError( + f"DropTriggerExpression.table must be a Table, " + f"got {type(expr.table).__name__}" + ) parts = ["DROP TRIGGER"] if expr.if_exists: parts.append("IF EXISTS") - parts.append(self.format_identifier(expr.trigger_name)) + parts.append(expr.trigger.to_sql()[0]) - if expr.table_name: + if expr.table is not None: parts.append("ON") - parts.append(self.format_identifier(expr.table_name)) + parts.append(expr.table.to_sql()[0]) return " ".join(parts), () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py index 79a3c2ba..fac6e713 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py @@ -14,6 +14,7 @@ TypeAlterAction, TypeDefinition, ) +from rhosocial.activerecord.backend.expression.objects import SchemaObject, Type from rhosocial.activerecord.backend.expression.types import DataType from ...expression.ddl.type import ( PostgresAddEnumValueAction, @@ -82,6 +83,13 @@ def _as_dialect(self) -> "SQLDialectBase": ) def _format_qualified_identifier(self, name: str, schema: Optional[str] = None) -> str: + """Render a dotted identifier, each part quoted. + + For the names that are types this is the type's own renderer; this + remains for the ones that are not types -- a COLLATE target, a SET + SCHEMA value -- which are bare schema-resident identifiers rather than + a catalogue kind the object tree models. + """ value = f"{schema}.{name}" if schema is not None else name parts = value.split(".") if not parts or any(not part.strip() for part in parts): @@ -260,9 +268,8 @@ def format_create_type_enum_statement( dialect = self._as_dialect() expr = CreateTypeExpression( dialect, - name, + Type(dialect, name, schema_name=schema), PostgresEnumTypeDefinition(dialect, values), - schema_name=schema, ) return self.format_create_type_statement(expr) @@ -270,6 +277,17 @@ def format_create_type_statement( self, expr: CreateTypeExpression, ) -> Tuple[str, tuple]: + """Render ``CREATE TYPE`` for a PostgreSQL type definition. + + Raises: + TypeError: ``expr.type`` is not a :class:`Type`. Another object kind + would have had its own name rendered as the type's. + """ + if not isinstance(expr.type, Type): + raise TypeError( + f"CreateTypeExpression.type must be a Type, " + f"got {type(expr.type).__name__}" + ) if not self.supports_type_objects() or not self.supports_create_type(): raise UnsupportedFeatureError(self.name, "CREATE TYPE") if expr.if_not_exists and not self.supports_create_type_if_not_exists(): @@ -288,7 +306,7 @@ def format_create_type_statement( parts.append("TYPE") if expr.if_not_exists: parts.append("IF NOT EXISTS") - parts.append(self._format_qualified_identifier(expr.type_name, expr.schema_name)) + parts.append(expr.type.to_sql()[0]) if definition_sql: parts.append(definition_sql) return " ".join(parts), tuple(definition_params) @@ -311,6 +329,16 @@ def format_alter_type_statement( self, expr: AlterTypeExpression, ) -> Tuple[str, tuple]: + """Render ``ALTER TYPE`` for the PostgreSQL type-alter actions. + + Raises: + TypeError: ``expr.type`` is not a :class:`Type`. + """ + if not isinstance(expr.type, Type): + raise TypeError( + f"AlterTypeExpression.type must be a Type, " + f"got {type(expr.type).__name__}" + ) if not self.supports_type_objects() or not self.supports_alter_type(): raise UnsupportedFeatureError(self.name, "ALTER TYPE") if expr.if_exists and not self.supports_alter_type_if_exists(): @@ -341,7 +369,7 @@ def format_alter_type_statement( parts = ["ALTER TYPE"] if expr.if_exists: parts.append("IF EXISTS") - parts.append(self._format_qualified_identifier(expr.type_name, expr.schema_name)) + parts.append(expr.type.to_sql()[0]) parts.append(", ".join(action_parts)) return " ".join(parts), tuple(action_params) @@ -368,14 +396,18 @@ def format_drop_type_statement( resolved_schema = schema if schema is not None else schema_name expr = PostgresDropTypeExpression( self._as_dialect(), - expr, - schema_name=resolved_schema, + Type(self._as_dialect(), expr, schema_name=resolved_schema), if_exists=if_exists, cascade=cascade, restrict=restrict, ) if not self.supports_type_objects() or not self.supports_drop_type(): raise UnsupportedFeatureError(self.name, "DROP TYPE") + if not isinstance(expr.type, Type): + raise TypeError( + f"{type(expr).__name__}.type must be a Type, " + f"got {type(expr.type).__name__}" + ) if expr.if_exists and not self.supports_drop_type_if_exists(): raise UnsupportedFeatureError(self.name, "DROP TYPE IF EXISTS") behavior = self._format_type_behavior( @@ -389,7 +421,7 @@ def format_drop_type_statement( parts = ["DROP TYPE"] if expr.if_exists: parts.append("IF EXISTS") - parts.append(self._format_qualified_identifier(expr.type_name, expr.schema_name)) + parts.append(expr.type.to_sql()[0]) if behavior: parts.append(behavior) return " ".join(parts), () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_database.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_database.py index aebd8a25..c94ac1e2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_database.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_database.py @@ -5,6 +5,7 @@ from typing import Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Database if TYPE_CHECKING: from rhosocial.activerecord.backend.expression.statements.ddl_database import ( @@ -72,8 +73,24 @@ def supports_database_force_drop(self) -> bool: def format_create_database_statement( self, expr: CreateDatabaseExpression ) -> Tuple[str, tuple]: + """Render ``CREATE DATABASE``, with the PostgreSQL-only options. + + Overrides core's version to add ENCODING / LC_COLLATE / TEMPLATE / + OWNER / CONNECTION LIMIT / TABLESPACE. Overriding means overriding the + kind check too: this body is the one that runs, so the check core's body + carries is not reached. Without it, a statement handed a ``Table`` renders + ``CREATE DATABASE users`` -- well-formed, and naming a table. + + Raises: + TypeError: ``expr.database`` is not a :class:`Database`. + """ + if not isinstance(expr.database, Database): + raise TypeError( + f"CreateDatabaseExpression.database must be a Database, " + f"got {type(expr.database).__name__}" + ) parts = ["CREATE DATABASE"] - parts.append(self.format_identifier(expr.database_name)) + parts.append(expr.database.to_sql()[0]) if expr.encoding: parts.append(f"ENCODING = '{expr.encoding}'") if expr.collation: @@ -91,10 +108,20 @@ def format_create_database_statement( def format_drop_database_statement( self, expr: DropDatabaseExpression ) -> Tuple[str, tuple]: + """Render ``DROP DATABASE``, with the PostgreSQL-only options. + + Raises: + TypeError: ``expr.database`` is not a :class:`Database`. + """ + if not isinstance(expr.database, Database): + raise TypeError( + f"DropDatabaseExpression.database must be a Database, " + f"got {type(expr.database).__name__}" + ) parts = ["DROP DATABASE"] if expr.if_exists and self.supports_database_if_exists(): parts.append("IF EXISTS") - parts.append(self.format_identifier(expr.database_name)) + parts.append(expr.database.to_sql()[0]) if expr.force and self.supports_database_force_drop(): parts.append("WITH (FORCE)") return " ".join(parts), () @@ -102,9 +129,19 @@ def format_drop_database_statement( def format_alter_database_statement( self, expr: AlterDatabaseExpression ) -> Tuple[str, tuple]: + """Render ``ALTER DATABASE``. + + Raises: + TypeError: ``expr.database`` is not a :class:`Database`. + """ + if not isinstance(expr.database, Database): + raise TypeError( + f"AlterDatabaseExpression.database must be a Database, " + f"got {type(expr.database).__name__}" + ) from rhosocial.activerecord.backend.expression.statements.ddl_database import AlterDatabaseAction parts = ["ALTER DATABASE"] - parts.append(self.format_identifier(expr.database_name)) + parts.append(expr.database.to_sql()[0]) if expr.action == AlterDatabaseAction.RENAME_TO: parts.append(f"RENAME TO {self.format_identifier(expr.target)}") elif expr.action == AlterDatabaseAction.OWNER_TO: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py index 9d4bf39c..c92260b6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py @@ -2,6 +2,9 @@ import re from typing import Any, List, Tuple +from rhosocial.activerecord.backend.expression.objects import Table + + class PostgresTableMixin: """PostgreSQL table extended features implementation.""" @@ -119,11 +122,28 @@ def format_create_table_like_statement(self, expr) -> Tuple[str, tuple]: * ``dict`` with ``"including"`` / ``"excluding"`` lists; * ``list`` of feature strings (treated as ``INCLUDING``); * ``list`` of ``(action, feature)`` tuples (``ACTION FEATURE``). + + Raises: + TypeError: ``expr.table`` is not a :class:`Table`, or + ``expr.like_table`` is not a :class:`Table`. Two ``Table`` slots + that read alike are exactly where a wrong kind hides: both would + render their own name and the statement would still be + well-formed. """ from rhosocial.activerecord.backend.dialect.exceptions import ( UnsupportedFeatureError, ) + if not isinstance(expr.table, Table): + raise TypeError( + f"CreateTableLikeExpression.table must be a Table, " + f"got {type(expr.table).__name__}" + ) + if not isinstance(expr.like_table, Table): + raise TypeError( + f"CreateTableLikeExpression.like_table must be a Table, " + f"got {type(expr.like_table).__name__}" + ) if not self.supports_create_table_like(): raise UnsupportedFeatureError(self.name, "CREATE TABLE ... (LIKE ...)") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py index 3428f2bd..a47c3407 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py @@ -3,6 +3,8 @@ from typing import Tuple, TYPE_CHECKING +from rhosocial.activerecord.backend.expression.objects import View + if TYPE_CHECKING: from ....expression.statements.ddl_view import CreateViewExpression @@ -37,7 +39,7 @@ def format_create_view_statement(self, expr: "CreateViewExpression") -> Tuple[st - ``expr.temporary`` — add ``TEMPORARY``. - ``expr.replace`` — add ``OR REPLACE``. - - ``expr.view_name`` — view name (identifier). + - ``expr.view`` — the view being created, which renders its own name. - ``expr.column_aliases`` — optional list of column aliases. - ``expr.query`` — source SELECT expression. - ``expr.options.check_option`` — ``WITH {LOCAL|CASCADED} CHECK OPTION``. @@ -48,7 +50,17 @@ def format_create_view_statement(self, expr: "CreateViewExpression") -> Tuple[st Returns: Tuple of (SQL string, params tuple) + Raises: + TypeError: ``expr.view`` is not a :class:`~...expression.objects.View`. + Another object kind would have had its own name rendered as the + view's, because ``to_sql`` dispatches on the object's own + ``format_method``. """ + if not isinstance(expr.view, View): + raise TypeError( + f"CreateViewExpression.view must be a View, " + f"got {type(expr.view).__name__}" + ) parts = ["CREATE"] if expr.temporary: @@ -58,7 +70,7 @@ def format_create_view_statement(self, expr: "CreateViewExpression") -> Tuple[st parts.append("OR REPLACE") parts.append("VIEW") - parts.append(self.format_identifier(expr.view_name)) + parts.append(expr.view.to_sql()[0]) if expr.column_aliases: cols = ", ".join(self.format_identifier(c) for c in expr.column_aliases) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py index bfba0e91..cc53596a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/extended_statistics.py @@ -7,6 +7,8 @@ from typing import Tuple, TYPE_CHECKING +from rhosocial.activerecord.backend.expression.objects import SchemaObject, Table + if TYPE_CHECKING: from ...expression.ddl import PostgresCreateStatisticsExpression, PostgresDropStatisticsExpression @@ -57,12 +59,12 @@ def format_create_statistics_statement(self, expr: "PostgresCreateStatisticsExpr if not self.supports_create_statistics(): raise ValueError("CREATE STATISTICS requires PostgreSQL 10+") - if expr.schema: - full_name = f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.name)}" - table_full = f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.table_name)}" - else: - full_name = self.format_identifier(expr.name) - table_full = self.format_identifier(expr.table_name) + full_name = self._format_schema_object_name( + SchemaObject(self, expr.name, schema_name=expr.schema) + ) + table_full = self.format_table_object( + Table(self, expr.table_name, schema_name=expr.schema) + )[0] exists_clause = "IF NOT EXISTS " if expr.if_not_exists else "" @@ -98,10 +100,8 @@ def format_drop_statistics_statement(self, expr: "PostgresDropStatisticsExpressi Tuple of (SQL string, empty params tuple) """ - full_name = ( - f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.name)}" - if expr.schema - else self.format_identifier(expr.name) + full_name = self._format_schema_object_name( + SchemaObject(self, expr.name, schema_name=expr.schema) ) exists_clause = "IF EXISTS " if expr.if_exists else "" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py index 5d1be711..490f8e01 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/stored_procedure.py @@ -7,6 +7,8 @@ from typing import Any, Dict, Optional, Tuple, List +from rhosocial.activerecord.backend.expression.objects import Procedure + class PostgresStoredProcedureMixin: """PostgreSQL stored procedure implementation. @@ -16,6 +18,16 @@ class PostgresStoredProcedureMixin: - Transaction control within procedures (COMMIT/ROLLBACK) """ + def _format_procedure_ref(self, schema: Optional[str], name: str) -> str: + """Render the procedure a CALL / CREATE / DROP statement names. + + A :class:`Procedure` schema object carries the two namespace slots, so + the three statements that name a procedure share one qualification and + one quoting rule instead of three copies of it. + """ + sql, _ = self.format_procedure_object(Procedure(self, name, schema_name=schema)) + return sql + def supports_call_statement(self) -> bool: """CALL statement is supported since PostgreSQL 11.""" return self.version >= (11, 0, 0) @@ -63,7 +75,7 @@ def format_create_procedure_statement( if not self.supports_call_statement(): raise ValueError("CREATE PROCEDURE requires PostgreSQL 11+") - full_name = f"{self.format_identifier(schema)}.{self.format_identifier(name)}" if schema else self.format_identifier(name) # noqa: E501 + full_name = self._format_procedure_ref(schema, name) # OR REPLACE clause replace_clause = "OR REPLACE " if or_replace else "" @@ -126,7 +138,7 @@ def format_drop_procedure_statement( Returns: Tuple of (SQL statement, parameters tuple) """ - full_name = f"{self.format_identifier(schema)}.{self.format_identifier(name)}" if schema else self.format_identifier(name) # noqa: E501 + full_name = self._format_procedure_ref(schema, name) exists_clause = "IF EXISTS " if if_exists else "" @@ -158,7 +170,7 @@ def format_call_statement( if not self.supports_call_statement(): raise ValueError("CALL statement requires PostgreSQL 11+") - full_name = f"{self.format_identifier(schema)}.{self.format_identifier(name)}" if schema else self.format_identifier(name) # noqa: E501 + full_name = self._format_procedure_ref(schema, name) if arguments: placeholders = ", ".join([self.p()] * len(arguments)) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/vacuum.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/vacuum.py index 88a83129..6b0dc6b2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/vacuum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/vacuum.py @@ -7,6 +7,8 @@ from typing import Tuple, TYPE_CHECKING +from rhosocial.activerecord.backend.expression.objects import Table + if TYPE_CHECKING: from ...expression.ddl import PostgresVacuumExpression, PostgresAnalyzeExpression @@ -106,10 +108,10 @@ def format_vacuum_statement(self, expr: "PostgresVacuumExpression") -> Tuple[str # Add table name if specified if expr.table_name: - if expr.schema: - parts.append(f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.table_name)}") - else: - parts.append(self.format_identifier(expr.table_name)) + parts.append( + self.format_table_object(Table(self, expr.table_name, schema_name=expr.schema) + )[0] + ) # Add columns for ANALYZE if expr.columns: @@ -143,10 +145,10 @@ def format_analyze_statement(self, expr: "PostgresAnalyzeExpression") -> Tuple[s # Add table name if specified if expr.table_name: - if expr.schema: - parts.append(f"{self.format_identifier(expr.schema)}.{self.format_identifier(expr.table_name)}") - else: - parts.append(self.format_identifier(expr.table_name)) + parts.append( + self.format_table_object(Table(self, expr.table_name, schema_name=expr.schema) + )[0] + ) # Add columns if expr.columns: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/ltree.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/ltree.py index 2d069090..f51521e7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/ltree.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/ltree.py @@ -12,6 +12,8 @@ from typing import Optional, Tuple +from rhosocial.activerecord.backend.expression.objects import Table + class PostgresLtreeMixin: """ltree label tree implementation.""" @@ -43,6 +45,7 @@ def format_ltree_index_statement( Returns: Tuple of (SQL statement, parameters) """ - full_table = f"{schema}.{table_name}" if schema else table_name + full_table, _ = self.format_table_object(Table(self, table_name, schema_name=schema) + ) sql = f"CREATE INDEX {index_name} ON {full_table} USING {index_type} ({column_name})" return (sql, ()) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/orafce.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/orafce.py index 80b0f51c..dae78e0f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/orafce.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/orafce.py @@ -8,6 +8,13 @@ ``functions/orafce.py`` instead of the removed format_* methods. """ +from typing import Optional + +from rhosocial.activerecord.backend.expression.objects import Function + +#: Schema orafce installs its functions into when created with defaults. +DEFAULT_ORAFCE_SCHEMA = "oracle" + class PostgresOrafceMixin: """orafce Oracle compatibility functionality implementation.""" @@ -15,3 +22,38 @@ class PostgresOrafceMixin: def supports_orafce(self) -> bool: """Check if orafce extension is available.""" return self.is_extension_installed("orafce") + + def orafce_function_name( + self, name: str, schema: Optional[str] = None + ) -> str: + """Render one orafce routine's name as a callable reference. + + orafce installs into its own schema, and the factories in + ``functions/orafce.py`` must name it explicitly so the call resolves + regardless of ``search_path``. That qualification is produced here, by + the dialect, from a :class:`Function` schema object -- the factory + passes the routine's own name and never assembles a dotted string. + + Both slots are rendered unquoted. A call target is not a table + reference: the function-name path that consumes this text is + case-normalised, and a quoted mixed-case identifier would not match the + lower-case names the extension actually installs. + + Args: + name: The routine's own name, e.g. ``ADD_MONTHS``. + schema: The schema orafce was installed into, or ``None`` for the + default one. + + Returns: + The qualified, unquoted routine name for a function call. + """ + sql, _ = self.format_function_object( + Function( + self, + name, + schema_name=schema if schema is not None else DEFAULT_ORAFCE_SCHEMA, + schema_need_quote=False, + name_need_quote=False, + ) + ) + return sql \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_repack.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_repack.py index f2589156..34ef60df 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_repack.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_repack.py @@ -9,9 +9,52 @@ """ +from typing import Optional + +from rhosocial.activerecord.backend.expression.objects import Function + +#: Schema pg_repack installs its functions into when created with defaults. +DEFAULT_REPACK_SCHEMA = "repack" + + class PostgresPgRepackMixin: """pg_repack online rebuild functionality implementation.""" def supports_pg_repack(self) -> bool: """Check if pg_repack extension is available.""" return self.is_extension_installed("pg_repack") + + def pg_repack_function_name( + self, name: str, schema: Optional[str] = None + ) -> str: + """Render one pg_repack routine's name as a callable reference. + + pg_repack installs into its own schema, so the factories in + ``functions/pg_repack.py`` name it explicitly rather than relying on + ``search_path``. The qualification is produced here from a + :class:`Function` schema object; the factory passes the routine's own + name. + + Both slots are rendered unquoted, for the same reason as + :meth:`PostgresOrafceMixin.orafce_function_name`: the function-name + path case-normalises this text, and the extension installs lower-case + names. + + Args: + name: The routine's own name, e.g. ``repack_version``. + schema: The schema pg_repack was installed into, or ``None`` for + the default one. + + Returns: + The qualified, unquoted routine name for a function call. + """ + sql, _ = self.format_function_object( + Function( + self, + name, + schema_name=schema if schema is not None else DEFAULT_REPACK_SCHEMA, + schema_need_quote=False, + name_need_quote=False, + ) + ) + return sql diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_trgm.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_trgm.py index 38b0c62e..364631f6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_trgm.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pg_trgm.py @@ -12,6 +12,8 @@ from typing import Optional, Tuple +from rhosocial.activerecord.backend.expression.objects import Table + class PostgresPgTrgmMixin: """pg_trgm trigram functionality implementation.""" @@ -39,6 +41,7 @@ def format_trgm_index_statement( Returns: Tuple of (SQL statement, parameters) """ - full_table = f"{schema}.{table_name}" if schema else table_name + full_table, _ = self.format_table_object(Table(self, table_name, schema_name=schema) + ) sql = f"CREATE INDEX {index_name} ON {full_table} USING {index_type} ({column_name} gin_trgm_ops)" return (sql, ()) 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 7b33b05f..4f2653c1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/pgvector.py @@ -13,6 +13,8 @@ from typing import Optional, Tuple +from rhosocial.activerecord.backend.expression.objects import Table + class PostgresPgvectorMixin: """pgvector vector similarity search implementation.""" @@ -63,7 +65,8 @@ def format_create_vector_index_statement( Returns: Tuple of (SQL statement, parameters) """ - full_table = f"{schema}.{table_name}" if schema else table_name + full_table, _ = self.format_table_object(Table(self, table_name, schema_name=schema) + ) if index_type.lower() == "hnsw": with_clauses = [] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/postgis.py index b7c3a246..b92f7587 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/extensions/postgis.py @@ -12,6 +12,8 @@ from typing import Optional, Tuple +from rhosocial.activerecord.backend.expression.objects import Table + class PostgresPostGISMixin: """PostGIS spatial functionality implementation.""" @@ -46,6 +48,7 @@ def format_spatial_index_statement( Returns: Tuple of (SQL statement, parameters) """ - full_table = f"{schema}.{table_name}" if schema else table_name + full_table, _ = self.format_table_object(Table(self, table_name, schema_name=schema) + ) sql = f"CREATE INDEX {index_name} ON {full_table} USING gist ({column_name})" return (sql, ()) 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 8a5e026c..4100c4cb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py @@ -27,7 +27,7 @@ from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.dialect.mixins import ViewMixin -from rhosocial.activerecord.backend.expression.core import QualifiedIdentifierExpression +from rhosocial.activerecord.backend.expression.objects import MaterializedView from ..expression.ddl.mv import ( _CURRENT_ROLE_KEYWORDS, @@ -91,19 +91,14 @@ def _format_materialized_view_name(self, expr: Any) -> str: """Render a materialized view name, schema-qualified when available. Args: - expr: Any MV expression exposing ``view_name`` and optionally ``schema``. + expr: An MV expression carrying the ``MaterializedView`` object it + names. The object renders itself, so the schema it carries is + the one that appears. Returns: The quoted (and possibly schema-qualified) identifier. """ - name_sql, _ = self.format_qualified_identifier( - QualifiedIdentifierExpression( - self, - schema=getattr(expr, "schema", None), - name=expr.view_name, - ) - ) - return name_sql + return expr.view.to_sql()[0] def _format_storage_parameters(self, properties: Dict[Any, Any]) -> str: """Render a ``KEY = value, ...`` storage parameter list. @@ -140,8 +135,8 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie [WITH (storage_parameter = value, ...)] [TABLESPACE name] AS query [WITH [NO] DATA]``. - - ``expr.view_name`` — view name (identifier). - - ``expr.schema`` — optional schema (PostgreSQL extension expression). + - ``expr.view`` — the ``MaterializedView`` being created; it renders + itself, so a qualified name comes out of the namespace machinery. - ``expr.column_aliases`` — optional list of column aliases. - ``expr.if_not_exists`` — ``IF NOT EXISTS`` (PostgreSQL 9.4+). - ``expr.storage_options`` — optional dict of storage parameters (``WITH (… )``). @@ -156,9 +151,17 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie Tuple of (SQL string, params tuple) Raises: + TypeError: ``expr.view`` is not a :class:`MaterializedView`. A plain + :class:`View` is the wrong kind here and would have had its own + name rendered as the materialized view's. UnsupportedFeatureError: If the dialect or the target server version does not support the requested materialized view feature. """ + if not isinstance(expr.view, MaterializedView): + raise TypeError( + f"CreateMaterializedViewExpression.view must be a MaterializedView, " + f"got {type(expr.view).__name__}" + ) if not self.supports_materialized_view(): raise UnsupportedFeatureError(self.name, "CREATE MATERIALIZED VIEW") @@ -199,7 +202,18 @@ def format_drop_materialized_view_statement(self, expr: Any) -> Tuple[str, tuple """Format DROP MATERIALIZED VIEW statement for PostgreSQL. Extends the core formatter with schema qualification. + + Raises: + TypeError: ``expr.view`` is not a :class:`MaterializedView`. A plain + :class:`View` is the wrong kind here: the two share a name shape + but not a statement, and PostgreSQL refuses ``DROP MATERIALIZED + VIEW `` at execution time with a far worse message. """ + if not isinstance(expr.view, MaterializedView): + raise TypeError( + f"{type(expr).__name__}.view must be a MaterializedView, " + f"got {type(expr.view).__name__}" + ) if not self.supports_materialized_view(): raise UnsupportedFeatureError(self.name, "DROP MATERIALIZED VIEW") @@ -233,8 +247,14 @@ def format_refresh_materialized_view_pg_statement( Tuple of (SQL statement, parameters tuple) Raises: + TypeError: ``expr.view`` is not a :class:`MaterializedView`. UnsupportedFeatureError: If CONCURRENTLY is requested on PG < 9.4. """ + if not isinstance(expr.view, MaterializedView): + raise TypeError( + f"{type(expr).__name__}.view must be a MaterializedView, " + f"got {type(expr.view).__name__}" + ) self._check_concurrent_refresh_support(expr) parts = ["REFRESH MATERIALIZED VIEW"] @@ -267,37 +287,52 @@ def format_alter_materialized_view_statement( raise UnsupportedFeatureError(self.name, "ALTER MATERIALIZED VIEW") target = self._format_materialized_view_name(expr) - rendered = [ - f"ALTER MATERIALIZED VIEW {target} {self.format_materialized_view_alter_action(action)}" - for action in expr.actions - ] + rendered = [] + for action in expr.actions: + action_sql, action_params = self.format_materialized_view_alter_action( + action + ) + rendered.append(f"ALTER MATERIALIZED VIEW {target} {action_sql}") return ";\n".join(rendered), () - def format_materialized_view_alter_action(self, expr: Any) -> str: + def format_materialized_view_alter_action(self, expr: Any) -> Tuple[str, tuple]: """Render one ALTER MATERIALIZED VIEW action body. + Returns the ``(sql, params)`` tuple every ``format_*`` method returns. + The action SQL is a fragment -- it has no ``ALTER MATERIALIZED VIEW + `` prefix -- but the contract is the contract: each action class + declares this method as its ``format_method``, so ``to_sql()`` hands the + return value straight back to its caller. Returning a bare string here + made ``action.to_sql()`` a string, which then failed to unpack wherever + someone treated it as the documented tuple. + Args: expr: A ``MaterializedViewAlterAction`` subclass instance. Returns: - The action SQL without the leading ``ALTER MATERIALIZED VIEW ``. + A ``(sql, params)`` tuple whose ``sql`` is the action body without + the leading ``ALTER MATERIALIZED VIEW ``. ``params`` is always + empty -- a storage-parameter name is not a bind parameter. Raises: UnsupportedFeatureError: If the action is not a known PostgreSQL materialized view action. """ if isinstance(expr, PostgresRenameMaterializedViewAction): - return f"RENAME TO {self.format_identifier(expr.new_name)}" + return f"RENAME TO {self.format_identifier(expr.new_name)}", () if isinstance(expr, PostgresSetMaterializedViewSchemaAction): - return f"SET SCHEMA {self.format_identifier(expr.new_schema)}" + return f"SET SCHEMA {self.format_identifier(expr.new_schema)}", () if isinstance(expr, PostgresSetMaterializedViewPropertiesAction): - return f"SET ({self._format_storage_parameters(expr.properties)})" + return f"SET ({self._format_storage_parameters(expr.properties)})", () if isinstance(expr, PostgresResetMaterializedViewPropertiesAction): - return f"RESET ({self._format_storage_parameter_names(expr.parameters)})" + return ( + f"RESET ({self._format_storage_parameter_names(expr.parameters)})", + (), + ) if isinstance(expr, PostgresChangeMaterializedViewOwnerAction): if expr.new_owner.upper() in _CURRENT_ROLE_KEYWORDS: - return f"OWNER TO {expr.new_owner.upper()}" - return f"OWNER TO {self.format_identifier(expr.new_owner)}" + return f"OWNER TO {expr.new_owner.upper()}", () + return f"OWNER TO {self.format_identifier(expr.new_owner)}", () raise UnsupportedFeatureError( self.name, f"ALTER MATERIALIZED VIEW action {getattr(expr, 'action_kind', type(expr).__name__)}", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/namespace.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/namespace.py new file mode 100644 index 00000000..5cf0cd16 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/namespace.py @@ -0,0 +1,111 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/namespace.py +"""PostgreSQL's answer to the naming-side questions, in one place. + +The naming side asks two separate things about a name: which namespace levels +PostgreSQL has (:meth:`supports_catalog`, :meth:`supports_schema_qualification`) +and how those levels are spelled (:meth:`format_qualified_name`). They used to +live in two mixins -- ``PostgresCatalogMixin`` for the outer level and +``PostgresSchemaMixin`` for the inner one -- which meant a reader had to know +that the schema *qualification* switch was filed under the schema **DDL** mixin, +next to ``CREATE SCHEMA`` support. It is a different question with a different +owner: "can this engine have schemas" is DDL, "may a name be qualified with one" +is naming, and PostgreSQL answers both the same way for once. + +Converging them here is also what makes the shape explicit. PostgreSQL is a +two-level engine -- a database outside, a schema inside -- so +:meth:`format_qualified_name` is spelled out rather than inherited: catalog +first, then schema, joined by :attr:`separator`. That is not a fact core can +assume. MySQL and MariaDB have a database and no inner schema, Oracle has no +database at all, Firebird has one unnamed namespace, and only four of the ten +engines have both levels. Hardcoding "two slots, catalog on the outside, joined +by a dot" in a shared mixin happened to fit those four and had to be worked +around for the rest. + +MRO note: this subclasses core's ``NamespaceMixin``, and a subclass precedes its +base, so it has to appear *before* ``NamespaceMixin`` in ``PostgresDialect``'s +base list -- after the ``*NameMixin`` block, because those subclass +``NamespaceMixin`` too and each supplies one ``format__object``. +""" + +from typing import Tuple + +from rhosocial.activerecord.backend.dialect.mixins import NamespaceMixin +from rhosocial.activerecord.backend.expression.objects import SchemaObject + +__all__ = ["PostgresNamespaceMixin"] + + +class PostgresNamespaceMixin(NamespaceMixin): + """Spells a PostgreSQL name, and says which levels PostgreSQL has.""" + + #: PostgreSQL joins namespace levels with a dot: ``app.public.users``. + separator: str = "." + + def supports_catalog(self) -> bool: + """PostgreSQL sits on a database above the schema. + + The slot a PostgreSQL schema object carries as ``catalog_name`` is the + database. It is rendered when supplied -- ``db.schema.table`` is legal + PostgreSQL -- but callers normally leave it unset, which is why this is + not a version switch: there is no version below which a database name + stops being legal. + """ + return True + + def supports_catalog_qualification(self) -> bool: + """A database-qualified name renders when the caller supplies one.""" + return True + + def supports_schema_qualification(self) -> bool: + """A name may be qualified with its schema. + + PostgreSQL qualifies, and ``"app"."users"`` is the ordinary way to write + a table reference. Distinct from ``supports_schema``, which asks whether + the engine can ``CREATE SCHEMA`` at all and lives on + :class:`~...mixins.schema.PostgresSchemaMixin`. + """ + return True + + def validate_catalog_name(self, expr: "SchemaObject") -> None: + """Accept any non-empty database name. + + The base schema object has already rejected empty and non-string slots, + and PostgreSQL imposes no further database-name restriction that a schema + object cannot express, so there is nothing left to refuse here. + """ + return None + + def format_qualified_name(self, expr: "SchemaObject") -> Tuple[str, tuple]: + """Build ``[database.][schema.]name``, each level quoted separately. + + PostgreSQL is two-level with the database outside, so the spelling is + written out rather than left to core's default. ``db`` is the database, so + it is rendered only when the caller set ``catalog_name``; PostgreSQL + resolves a name against the connection's database otherwise, and dropping + the level silently would name a different object than the caller asked + for -- which is why :meth:`validate_namespace` raises instead of + discarding a level this dialect claims to support. + + Args: + expr: The object being named. + + Returns: + A ``(sql, params)`` tuple. ``params`` is empty because an identifier + is never a bind parameter. + + Raises: + UnsupportedFeatureError: ``validate_namespace`` refused a level this + dialect declares it can express. + """ + self.validate_namespace(expr) + parts = [] + if expr.catalog_name: + parts.append( + self.format_identifier(expr.catalog_name, expr.catalog_need_quote) + ) + if expr.schema_name: + parts.append( + self.format_identifier(expr.schema_name, expr.schema_need_quote) + ) + parts.append(self.format_identifier(expr.name, expr.name_need_quote)) + return self.separator.join(parts), () \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/object_name.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/object_name.py new file mode 100644 index 00000000..108451ed --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/object_name.py @@ -0,0 +1,45 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/object_name.py +"""Rendering the names of catalogue entries core has no object kind for. + +A statement now holds the object it acts on, and the dialect renders it through +one ``format__object`` method. Core supplies those methods for every kind +it models. Two PostgreSQL catalogue entries fall outside that set: a collation +(``CREATE COLLATION``, and the right-hand side of ``COLLATE``) and an extended +statistics object (``CREATE STATISTICS``). Neither has a class in the +schema-object tree, so neither has a core formatter. + +Both are ordinary schema-resident names, so both are carried by the base +:class:`~...expression.objects.SchemaObject` -- kind, catalog, schema, name -- +and rendered here from the two pieces core does provide: +:meth:`~...mixins.namespace.PostgresNamespaceMixin.validate_namespace` to accept +or refuse the levels the object carries, and +:meth:`~...mixins.namespace.PostgresNamespaceMixin.format_qualified_name` to spell +them. That is exactly what a core ``*NameMixin`` does; this is the same +composition for a kind core does not model, kept in one place so the two callers +cannot drift. +""" + +from rhosocial.activerecord.backend.expression.objects import SchemaObject + +__all__ = ["PostgresObjectNameMixin"] + + +class PostgresObjectNameMixin: + """Names a schema-resident catalogue entry that has no dedicated object kind.""" + + def _format_schema_object_name(self, obj: "SchemaObject") -> str: + """Render *obj*'s qualified name. + + Args: + obj: The :class:`SchemaObject` carrying the name and its namespace. + + Returns: + The quoted, optionally qualified identifier. + + Raises: + UnsupportedFeatureError: *obj* carries a namespace level PostgreSQL + declares it cannot express. + """ + self.validate_namespace(obj) + qualified_sql, _ = self.format_qualified_name(obj) + return qualified_sql diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py index e94b21fc..3923b646 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py @@ -1,15 +1,31 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py -"""PostgreSQL schema feature support implementation.""" +"""PostgreSQL schema feature support implementation. + +Everything here is the DDL side: whether the engine has schemas, and what +``CREATE``/``DROP SCHEMA`` can carry. The naming side -- whether a *name* may be +qualified with a schema, and how that qualification is spelled -- belongs to +:class:`~...mixins.namespace.PostgresNamespaceMixin`. PostgreSQL answers both +with ``True``, which is exactly why they should not be one switch: an engine can +have ``CREATE SCHEMA`` and still refuse to qualify, and reading the wrong one +gives the wrong answer with no way to tell. +""" class PostgresSchemaMixin: - """PostgreSQL schema override implementation. + """PostgreSQL schema DDL override implementation. All features are native, using version number for detection. """ def supports_schema(self) -> bool: - """PostgreSQL models named schema namespaces natively.""" + """PostgreSQL models named schema namespaces natively. + + A DDL-side switch: it answers whether schemas exist in the engine, which is + what decides whether ``CREATE SCHEMA`` can be offered at all. Whether a + name may be *qualified* with a schema is + :meth:`~...mixins.namespace.PostgresNamespaceMixin.supports_schema_qualification`, + and it lives with the rest of the naming-side answers. + """ return True def supports_create_schema(self) -> bool: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py index 99603159..15808d30 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py @@ -4,6 +4,7 @@ from typing import Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Table if TYPE_CHECKING: from ....expression.statements.ddl_truncate import TruncateExpression @@ -24,18 +25,21 @@ def supports_truncate_cascade(self) -> bool: def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tuple]: """Format TRUNCATE statement for PostgreSQL. - - ``expr.table_name`` — target table. + - ``expr.table`` — the table being truncated, which renders its own + name and namespace. - ``expr.restart_identity`` — add ``RESTART IDENTITY`` (PG 8.4+). - ``expr.cascade`` — add ``CASCADE``. + + Raises: + TypeError: ``expr.table`` is not a :class:`Table`. Another object kind + would have had its own name rendered as the table's. """ - parts = ["TRUNCATE TABLE"] - table_name = self.format_identifier(expr.table_name) - if expr.schema: - table_name = ( - f"{self.format_identifier(expr.schema)}." - f"{table_name}" + if not isinstance(expr.table, Table): + raise TypeError( + f"TruncateExpression.table must be a Table, " + f"got {type(expr.table).__name__}" ) - parts.append(table_name) + parts = ["TRUNCATE TABLE", expr.table.to_sql()[0]] if expr.restart_identity: if not self.supports_truncate_restart_identity(): 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 b679093f..7621396b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/enum.py @@ -5,6 +5,7 @@ from typing import Any, List, Optional, Sequence, Tuple, cast, TYPE_CHECKING +from rhosocial.activerecord.backend.expression.objects import Type from rhosocial.activerecord.backend.expression.statements.ddl_type import ( AlterTypeExpression, CreateTypeExpression, @@ -46,11 +47,13 @@ def format_enum_type_name( name: str, schema: Optional[str] = None, ) -> Tuple[str, tuple]: - value = f"{schema}.{name}" if schema is not None else name - parts = value.split(".") - if not parts or any(not part.strip() for part in parts): - raise ValueError("ENUM type identifiers must contain non-empty segments") - return ".".join(self.format_identifier(part) for part in parts), () + """Render an ENUM type name, optionally schema-qualified. + + The two slots are fixed on the object, so there is no joined string to + split back apart: the dialect receives a ``Type`` whose schema slot it + either renders or ignores. + """ + return self.format_type_object(Type(self, name, schema_name=schema)) def format_enum_type_name_expression(self, expr: Any) -> Tuple[str, tuple]: return self.format_enum_type_name(expr.name, expr.schema) @@ -74,9 +77,8 @@ def format_create_enum_type_raw( dialect = self._as_dialect() expr = CreateTypeExpression( dialect, - name, + Type(dialect, name, schema_name=schema), PostgresEnumTypeDefinition(dialect, values), - schema_name=schema, if_not_exists=if_not_exists, ) return self.format_create_type_statement(expr) @@ -128,7 +130,9 @@ def format_alter_enum_add_value_raw( before=before, after=after, ) - expr = AlterTypeExpression(dialect, type_name, [action], schema_name=schema) + expr = AlterTypeExpression( + dialect, Type(dialect, type_name, schema_name=schema), [action] + ) return self.format_alter_type_statement(expr) def format_alter_enum_add_value_raw_expression(self, expr: Any) -> Tuple[str, tuple]: @@ -228,8 +232,7 @@ def format_alter_enum_type_rename_value(self, expr: Any) -> Tuple[str, tuple]: ) statement = AlterTypeExpression( dialect, - expr.type_name, + Type(dialect, expr.type_name, schema_name=expr.schema), [action], - schema_name=expr.schema, ) return self.format_alter_type_statement(statement) 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 098ea29a..88c9cbb3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/multirange.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/multirange.py @@ -4,6 +4,7 @@ from typing import Optional, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Table if TYPE_CHECKING: from rhosocial.activerecord.backend.impl.postgres.expression.ddl.multirange import ( @@ -56,8 +57,8 @@ def format_multirange_agg_function( "multirange_agg", suggestion="requires PostgreSQL 14+", ) - full_name = f"{schema}.{table_name}" if schema is not None else table_name - table_sql = ".".join(self.format_identifier(part) for part in full_name.split(".")) + table_sql, _ = self.format_table_object(Table(self, table_name, schema_name=schema) + ) column_sql = self.format_identifier(range_column) sql = f"SELECT multirange_agg({column_sql}) FROM {table_sql}" if where_clause: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py index 6f5eeb91..79419491 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py @@ -100,6 +100,7 @@ from .join import PostgresJoinSupport from .truncate import PostgresTruncateSupport from .schema import PostgresSchemaSupport +from .catalog import PostgresCatalogSupport, PostgresNamespaceSupport from .sequence import PostgresSequenceSupport from .transaction import PostgresTransactionSupport from .view import PostgresViewSupport @@ -196,6 +197,8 @@ "PostgresJoinSupport", "PostgresTruncateSupport", "PostgresSchemaSupport", + "PostgresCatalogSupport", # deprecated alias of PostgresNamespaceSupport + "PostgresNamespaceSupport", "PostgresSequenceSupport", "PostgresTransactionSupport", "PostgresViewSupport", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/catalog.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/catalog.py new file mode 100644 index 00000000..0181e37e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/catalog.py @@ -0,0 +1,53 @@ +# src/rhosocial/activerecord/backend/impl/postgres/protocols/catalog.py +"""PostgreSQL naming-side protocol: which namespace levels a name may carry. + +Declared here, implemented by +:class:`~...mixins.namespace.PostgresNamespaceMixin` -- one mixin for the whole +naming side. It used to be split across two, with the schema *qualification* +switch filed under the schema DDL protocol; the two questions have different +owners and PostgreSQL happens to answer both the same way, which is the case +where conflating them hides the most. +""" + +from typing import Protocol, runtime_checkable + +from rhosocial.activerecord.backend.dialect.protocols import NamespaceSupport + + +@runtime_checkable +class PostgresNamespaceSupport(NamespaceSupport, Protocol): + """PostgreSQL namespace-level support protocol. + + PostgreSQL's outer namespace is the *database*: ``db.schema.table`` is a + legal three-part reference. The database is normally left implicit, but the + namespace exists, so this backend declares it rather than reporting that + PostgreSQL has no catalog at all. PostgreSQL is two-level with the database + outside, which is why the mixin spells ``format_qualified_name`` itself + rather than inheriting core's two-slot default. + + Derives from :class:`NamespaceSupport`, which is where the namespace + questions live now that naming was split from rendering. Kept separate from + :class:`~.schema.PostgresSchemaSupport` because the two are independent: an + engine can have ``CREATE SCHEMA`` and still not qualify names with one. + """ + + def supports_catalog(self) -> bool: ... + + def supports_catalog_qualification(self) -> bool: ... + + def supports_schema_qualification(self) -> bool: ... + + def validate_catalog_name(self, expr) -> None: ... + + +#: Deprecated alias of :class:`PostgresNamespaceSupport`. +#: +#: One protocol under two names, not two protocols: a subclass here would declare +#: the same members twice and both spellings would then have to be kept in step. +#: ``supports_schema_qualification`` was added to this protocol because the +#: PostgreSQL mixin that implements it now answers that question too -- it used +#: to live on ``PostgresSchemaSupport``, which is a DDL question with a +#: different owner. +PostgresCatalogSupport = PostgresNamespaceSupport + +__all__ = ["PostgresNamespaceSupport", "PostgresCatalogSupport"] \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/domain.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/domain.py index 87c382af..ce46d128 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/domain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/domain.py @@ -3,7 +3,11 @@ from typing import Protocol, Tuple, runtime_checkable -from rhosocial.activerecord.backend.dialect.protocols import DomainSupport +from rhosocial.activerecord.backend.dialect.protocols import ( + AlterDomainSupport, + CreateDomainSupport, + DropDomainSupport, +) from rhosocial.activerecord.backend.expression.statements.ddl_domain import AlterDomainExpression @@ -11,7 +15,12 @@ @runtime_checkable -class PostgresDomainSupport(DomainSupport, Protocol): +class PostgresDomainSupport( + CreateDomainSupport, + AlterDomainSupport, + DropDomainSupport, + Protocol, +): def format_postgres_alter_domain_statement( self, expr: AlterDomainExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/index.py index a156daed..1ac7b68a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/index.py @@ -8,7 +8,12 @@ from enum import Enum # noqa: F401 from typing import Protocol, runtime_checkable, Optional, Tuple, List, Dict, Any, TYPE_CHECKING -from rhosocial.activerecord.backend.dialect.protocols import IndexSupport +from rhosocial.activerecord.backend.dialect.protocols import ( + CreateIndexSupport, + DropIndexSupport, + FulltextIndexSupport, + IndexObjectSupport, +) if TYPE_CHECKING: # pragma: no cover from ...expression.ddl import ( @@ -19,7 +24,13 @@ @runtime_checkable -class PostgresIndexSupport(IndexSupport, Protocol): +class PostgresIndexSupport( + CreateIndexSupport, + DropIndexSupport, + FulltextIndexSupport, + IndexObjectSupport, + Protocol, +): """PostgreSQL index enhancements protocol. Feature Source: Native support (no extension required) @@ -84,6 +95,15 @@ def supports_reindex_concurrently(self) -> bool: """ ... # pragma: no cover + def get_supported_index_types(self) -> List[str]: + """Return every index access method this dialect accepts in ``USING``. + + The generic mixin answers ``["BTREE"]``; PostgreSQL adds HASH, GIST, + GIN, SPGIST and BRIN. An empty answer would mean no ``USING`` clause is + possible, so the list has to name the engine's real access methods. + """ + ... # pragma: no cover + def supports_btree_deduplication(self) -> bool: """Whether B-tree deduplication is supported. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/trigger.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/trigger.py index 8542a595..083bd01b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/trigger.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/trigger.py @@ -7,11 +7,20 @@ from typing import Protocol, runtime_checkable, Tuple -from rhosocial.activerecord.backend.dialect.protocols import TriggerSupport +from rhosocial.activerecord.backend.dialect.protocols import ( + CreateTriggerSupport, + DropTriggerSupport, + TriggerObjectSupport, +) @runtime_checkable -class PostgresTriggerSupport(TriggerSupport, Protocol): +class PostgresTriggerSupport( + CreateTriggerSupport, + DropTriggerSupport, + TriggerObjectSupport, + Protocol, +): """PostgreSQL trigger DDL protocol. Feature Source: Native support (no extension required) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/type.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/type.py index 69142060..cd830353 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/ddl/type.py @@ -3,7 +3,12 @@ from typing import List, Optional, Protocol, Tuple, Union, runtime_checkable -from rhosocial.activerecord.backend.dialect.protocols import UserDefinedTypeSupport +from rhosocial.activerecord.backend.dialect.protocols import ( + AlterTypeSupport, + CreateTypeSupport, + DropTypeSupport, + TypeObjectSupport, +) from rhosocial.activerecord.backend.expression.statements.ddl_type import DropTypeExpression @@ -11,7 +16,13 @@ @runtime_checkable -class PostgresTypeSupport(UserDefinedTypeSupport, Protocol): +class PostgresTypeSupport( + CreateTypeSupport, + AlterTypeSupport, + DropTypeSupport, + TypeObjectSupport, + Protocol, +): def supports_drop_type_cascade(self) -> bool: ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py index ed82eae3..2a23b390 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py @@ -135,7 +135,7 @@ def format_alter_materialized_view_statement( """ ... - def format_materialized_view_alter_action(self, expr: Any) -> str: + def format_materialized_view_alter_action(self, expr: Any) -> Tuple[str, tuple]: """Render one ALTER MATERIALIZED VIEW action body. Storage parameter names must come from @@ -145,6 +145,10 @@ def format_materialized_view_alter_action(self, expr: Any) -> str: expr: A ``MaterializedViewAlterAction`` subclass instance. Returns: - The action SQL without the leading ``ALTER MATERIALIZED VIEW ``. + A ``(sql, params)`` tuple whose ``sql`` is the action body without the + leading ``ALTER MATERIALIZED VIEW ``. Each action class names + this method as its ``format_method``, so the return value is what + ``to_sql()`` hands back -- it is a tuple even though the SQL is a + fragment. """ ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py index 31723b24..7f31ed66 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py @@ -1,12 +1,22 @@ # src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py -"""PostgreSQL schema feature support protocol.""" +"""PostgreSQL schema DDL protocol. + +Everything declared here is the DDL side: does the engine have schemas, and what +``CREATE``/``DROP SCHEMA`` can carry. The naming side -- whether a name may be +*qualified* with a schema -- is +:class:`~.catalog.PostgresNamespaceSupport`, implemented by +:class:`~...mixins.namespace.PostgresNamespaceMixin`. PostgreSQL answers both +switches the same way, which is exactly why they must stay two: an engine can +offer ``CREATE SCHEMA`` and still refuse to qualify a name, and reading the wrong +one gives the wrong answer with nothing to tell the two cases apart. +""" from typing import Protocol, runtime_checkable @runtime_checkable class PostgresSchemaSupport(Protocol): - """PostgreSQL schema feature support protocol.""" + """PostgreSQL schema DDL support protocol.""" def supports_schema(self)-> bool: ... @@ -20,4 +30,4 @@ def supports_schema_if_exists(self)-> bool: ... def supports_schema_cascade(self)-> bool: ... - def supports_schema_authorization(self)-> bool: ... + def supports_schema_authorization(self)-> bool: ... \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py index 3b8fd30f..37573427 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/table.py @@ -7,7 +7,12 @@ from typing import TYPE_CHECKING, Protocol, runtime_checkable -from rhosocial.activerecord.backend.dialect.protocols import TableSupport +from rhosocial.activerecord.backend.dialect.protocols import ( + AlterTableSupport, + CreateTableSupport, + DropTableSupport, + TableObjectSupport, +) if TYPE_CHECKING: from rhosocial.activerecord.backend.expression.statements import ( @@ -16,7 +21,13 @@ @runtime_checkable -class PostgresTableSupport(TableSupport, Protocol): +class PostgresTableSupport( + CreateTableSupport, + DropTableSupport, + AlterTableSupport, + TableObjectSupport, + Protocol, +): """PostgreSQL table extended features protocol. PostgreSQL's table support includes exclusive features: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py index 423d066a..3e1c52b6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py @@ -372,13 +372,7 @@ # pg_lsn Type "PG_LSN", # pgvector Types - "VECTOR", - "HALFVEC", - "BITVEC", - "SPARSEVEC", # PostGIS Types - "GEOMETRY", - "GEOGRAPHY", # hstore Types "HSTORE", ] diff --git a/tests/providers/fixtures/_common.py b/tests/providers/fixtures/_common.py index d07e02b9..935dd5eb 100644 --- a/tests/providers/fixtures/_common.py +++ b/tests/providers/fixtures/_common.py @@ -11,15 +11,15 @@ from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table def drop_table(dialect, table_name: str) -> DropTableExpression: """Build a ``DROP TABLE IF EXISTS table_name CASCADE`` expression.""" return DropTableExpression( dialect=dialect, - table=TableExpression(dialect, table_name), + table=Table(dialect, table_name), if_exists=True, ) diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py index 1443a91f..ba4cb7d2 100644 --- a/tests/providers/fixtures/basic.py +++ b/tests/providers/fixtures/basic.py @@ -38,6 +38,7 @@ TimestampTzType, VarCharType, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresSerialType, PostgresUUIDType, @@ -54,7 +55,7 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -83,7 +84,7 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpress def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresUUIDType(dialect=dialect), @@ -125,7 +126,7 @@ def create_type_cases_table(dialect, table_name: str = "type_cases") -> CreateTa def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresUUIDType(dialect=dialect), @@ -159,7 +160,7 @@ def create_type_tests_table(dialect, table_name: str = "type_tests") -> CreateTa def create_validated_field_users_table(dialect, table_name: str = "validated_field_users") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -191,7 +192,7 @@ def create_validated_field_users_table(dialect, table_name: str = "validated_fie def create_validated_users_table(dialect, table_name: str = "validated_users") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -212,7 +213,7 @@ def create_validated_users_table(dialect, table_name: str = "validated_users") - def create_pydantic_validated_models_table(dialect, table_name: str = "pydantic_validated_models") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -237,7 +238,7 @@ def create_pydantic_validated_models_table(dialect, table_name: str = "pydantic_ def create_bulk_users_table(dialect, table_name: str = "bulk_users") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -259,7 +260,7 @@ def create_bulk_users_table(dialect, table_name: str = "bulk_users") -> CreateTa def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -276,7 +277,7 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["author"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -289,7 +290,7 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress def create_comments_table(dialect, table_name: str = "comments") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -307,9 +308,9 @@ def create_comments_table(dialect, table_name: str = "comments") -> CreateTableE constraints=[ColumnConstraint(dialect, ColumnConstraintType.DEFAULT, default_value=False)]), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["post_ref"], foreign_key_table="posts", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["post_ref"], foreign_key_table=Table(dialect, 'posts'), foreign_key_columns=["id"], on_delete=_CASCADE), - ForeignKeyConstraint(dialect, columns=["author"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["author"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -322,7 +323,7 @@ def create_comments_table(dialect, table_name: str = "comments") -> CreateTableE def create_column_mapping_items_table(dialect, table_name: str = "column_mapping_items") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -343,7 +344,7 @@ def create_column_mapping_items_table(dialect, table_name: str = "column_mapping def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annotation_items") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -365,7 +366,7 @@ def create_mixed_annotation_items_table(dialect, table_name: str = "mixed_annota def create_type_adapter_tests_table(dialect, table_name: str = "type_adapter_tests") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=True, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -390,7 +391,7 @@ def create_type_adapter_tests_table(dialect, table_name: str = "type_adapter_tes def create_composite_pk_order_items_table(dialect, table_name: str = "order_items") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "order_id", IntegerType(dialect=dialect), @@ -417,7 +418,7 @@ def create_composite_pk_order_items_table(dialect, table_name: str = "order_item def create_store_inventory_table(dialect, table_name: str = "store_inventory") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "store_id", IntegerType(dialect=dialect), @@ -444,7 +445,7 @@ def create_store_inventory_table(dialect, table_name: str = "store_inventory") - def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -464,7 +465,7 @@ def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpre def create_product_table(dialect, table_name: str = "product") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=True, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py index a31e189c..fd66a8f0 100644 --- a/tests/providers/fixtures/events.py +++ b/tests/providers/fixtures/events.py @@ -21,6 +21,7 @@ VarCharType, ) from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType +from rhosocial.activerecord.backend.expression.objects import Table # --------------------------------------------------------------------------- @@ -30,7 +31,7 @@ def create_event_tests_table(dialect, table_name: str = "event_tests") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -57,7 +58,7 @@ def create_event_tests_table(dialect, table_name: str = "event_tests") -> Create def create_event_tracking_models_table(dialect, table_name: str = "event_tracking_models") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py index a70e53a7..7559277b 100644 --- a/tests/providers/fixtures/mixins.py +++ b/tests/providers/fixtures/mixins.py @@ -23,6 +23,7 @@ VarCharType, ) from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType +from rhosocial.activerecord.backend.expression.objects import Table # --------------------------------------------------------------------------- @@ -32,7 +33,7 @@ def create_combined_articles_table(dialect, table_name: str = "combined_articles") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -61,7 +62,7 @@ def create_combined_articles_table(dialect, table_name: str = "combined_articles def create_tasks_table(dialect, table_name: str = "tasks") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -84,7 +85,7 @@ def create_tasks_table(dialect, table_name: str = "tasks") -> CreateTableExpress def create_timestamped_posts_table(dialect, table_name: str = "timestamped_posts") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -105,7 +106,7 @@ def create_timestamped_posts_table(dialect, table_name: str = "timestamped_posts def create_versioned_products_table(dialect, table_name: str = "versioned_products") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py index 61c423ab..41ec37d1 100644 --- a/tests/providers/fixtures/query.py +++ b/tests/providers/fixtures/query.py @@ -26,6 +26,7 @@ VarCharType, ) from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType +from rhosocial.activerecord.backend.expression.objects import Table _CASCADE = ReferentialAction.CASCADE @@ -37,7 +38,7 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -66,7 +67,7 @@ def create_users_table(dialect, table_name: str = "users") -> CreateTableExpress def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -83,7 +84,7 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -96,7 +97,7 @@ def create_posts_table(dialect, table_name: str = "posts") -> CreateTableExpress def create_comments_table(dialect, table_name: str = "comments") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -113,9 +114,9 @@ def create_comments_table(dialect, table_name: str = "comments") -> CreateTableE ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"], on_delete=_CASCADE), - ForeignKeyConstraint(dialect, columns=["post_id"], foreign_key_table="posts", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["post_id"], foreign_key_table=Table(dialect, 'posts'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -128,7 +129,7 @@ def create_comments_table(dialect, table_name: str = "comments") -> CreateTableE def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -147,7 +148,7 @@ def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpre ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"]), ], ) @@ -159,7 +160,7 @@ def create_orders_table(dialect, table_name: str = "orders") -> CreateTableExpre def create_order_items_table(dialect, table_name: str = "order_items") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -180,7 +181,7 @@ def create_order_items_table(dialect, table_name: str = "order_items") -> Create ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["order_id"], foreign_key_table="orders", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["order_id"], foreign_key_table=Table(dialect, 'orders'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -193,7 +194,7 @@ def create_order_items_table(dialect, table_name: str = "order_items") -> Create def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -206,7 +207,7 @@ def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableE ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"]), + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"]), ], ) @@ -218,7 +219,7 @@ def create_profiles_table(dialect, table_name: str = "profiles") -> CreateTableE def create_json_users_table(dialect, table_name: str = "json_users") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -248,7 +249,7 @@ def create_json_users_table(dialect, table_name: str = "json_users") -> CreateTa def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -264,7 +265,7 @@ def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpress ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["parent_id"], foreign_key_table="nodes", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["parent_id"], foreign_key_table=Table(dialect, 'nodes'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -277,7 +278,7 @@ def create_nodes_table(dialect, table_name: str = "nodes") -> CreateTableExpress def create_searchable_items_table(dialect, table_name: str = "searchable_items") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -297,7 +298,7 @@ def create_searchable_items_table(dialect, table_name: str = "searchable_items") def create_extended_orders_table(dialect, table_name: str = "extended_orders") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -327,7 +328,7 @@ def create_extended_orders_table(dialect, table_name: str = "extended_orders") - ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table="users", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["user_id"], foreign_key_table=Table(dialect, 'users'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) @@ -340,7 +341,7 @@ def create_extended_orders_table(dialect, table_name: str = "extended_orders") - def create_extended_order_items_table(dialect, table_name: str = "extended_order_items") -> CreateTableExpression: return CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect), @@ -360,7 +361,7 @@ def create_extended_order_items_table(dialect, table_name: str = "extended_order ColumnDefinition(dialect, "updated_at", TimestampTzType(dialect=dialect)), ], table_constraints=[ - ForeignKeyConstraint(dialect, columns=["order_id"], foreign_key_table="extended_orders", foreign_key_columns=["id"], + ForeignKeyConstraint(dialect, columns=["order_id"], foreign_key_table=Table(dialect, 'extended_orders'), foreign_key_columns=["id"], on_delete=_CASCADE), ], ) diff --git a/tests/providers/partition.py b/tests/providers/partition.py index 571711e9..f46b9b70 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -35,6 +35,7 @@ ) from .scenarios import get_enabled_scenarios, get_scenario +from rhosocial.activerecord.backend.expression.objects import Table class PartitionProvider(IPartitionProvider): @@ -297,13 +298,13 @@ def _attach_partition_sql(self, dialect, partition_name: str, from_value: str, t return expr.to_sql() def _truncate_partition_sql(self, dialect, partition_name: str): - expr = TruncateExpression(dialect=dialect, table_name=partition_name) + expr = TruncateExpression(dialect=dialect, table=Table(dialect, partition_name)) return expr.to_sql() def _drop_partition_sql(self, dialect, table_name: str): expr = DropTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_exists=True, cascade=True, ) 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 3d335e1d..71ab6e15 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 @@ -22,6 +22,7 @@ ) from rhosocial.activerecord.backend.expression.types import TextType from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Table @pytest.fixture @@ -61,7 +62,7 @@ def test_inside_alter_table(self, dialect): if_not_exists=True, ) expr = AlterTableExpression( - dialect, table_name="users", actions=[action] + dialect, table=Table(dialect, 'users'), actions=[action] ) sql, params = expr.to_sql() assert 'ALTER TABLE "users"' in sql @@ -189,7 +190,7 @@ def test_rename_column_inside_alter_table(self, dialect): expr = AlterTableExpression( dialect, - table_name="orders", + table=Table(dialect, 'orders'), actions=[RenameObject(dialect, old_name="id", new_name="order_id")], ) sql, params = expr.to_sql() @@ -230,7 +231,7 @@ def test_unlogged_renders_qualifier(self, dialect): expr = CreateTableExpression( dialect, - table="audit", + table=Table(dialect, 'audit'), columns=[ColumnDefinition(dialect, "id", TextType(dialect=dialect))], table_options=self._options(dialect), ) @@ -245,7 +246,7 @@ def test_plain_omits_qualifier(self, dialect): expr = CreateTableExpression( dialect, - table="audit", + table=Table(dialect, 'audit'), columns=[ColumnDefinition(dialect, "id", TextType(dialect=dialect))], ) sql, params = expr.to_sql() @@ -262,7 +263,7 @@ def test_unlogged_with_temporary_rejected(self, dialect): expr = CreateTableExpression( dialect, - table="audit", + table=Table(dialect, 'audit'), columns=[ColumnDefinition(dialect, "id", TextType(dialect=dialect))], temporary=True, table_options=self._options(dialect), @@ -284,7 +285,7 @@ def test_version_gate_94(self): low = PostgresDialect(version=(9, 4, 0)) expr = CreateTableExpression( low, - table="audit", + table=Table(low, 'audit'), columns=[ColumnDefinition(low, "id", TextType(dialect=low))], table_options=PostgresCreateTableOptions(low, unlogged=True), ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py index 564abdfd..cc0db174 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py @@ -11,6 +11,7 @@ from rhosocial.activerecord.backend.expression import DropTableExpression from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Table @pytest.fixture @@ -24,20 +25,20 @@ def test_capability_switches(self, dialect): assert dialect.supports_drop_table_restrict() is True def test_cascade_renders_standard_token(self, dialect): - expr = DropTableExpression(dialect, table="users", cascade=True) + expr = DropTableExpression(dialect, table=Table(dialect, 'users'), cascade=True) sql, params = expr.to_sql() assert sql.endswith(" CASCADE") assert "CASCADE CONSTRAINTS" not in sql assert params == () def test_restrict_renders_standard_token(self, dialect): - expr = DropTableExpression(dialect, table="users", cascade=False) + expr = DropTableExpression(dialect, table=Table(dialect, 'users'), cascade=False) sql, params = expr.to_sql() assert sql.endswith(" RESTRICT") assert params == () def test_cascade_none_omits_token(self, dialect): - expr = DropTableExpression(dialect, table="users", cascade=None) + expr = DropTableExpression(dialect, table=Table(dialect, 'users'), cascade=None) sql, params = expr.to_sql() assert "CASCADE" not in sql assert "RESTRICT" not in sql @@ -45,7 +46,7 @@ def test_cascade_none_omits_token(self, dialect): def test_if_exists_combined_with_cascade(self, dialect): expr = DropTableExpression( - dialect, table="users", if_exists=True, cascade=True + dialect, table=Table(dialect, 'users'), if_exists=True, cascade=True ) sql, params = expr.to_sql() assert sql.startswith("DROP TABLE IF EXISTS") diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py index fb83bdf1..6af35262 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_insert_on_conflict_clauses.py @@ -18,6 +18,7 @@ ValuesSource, ) from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Table @pytest.fixture @@ -51,7 +52,7 @@ def test_multiple_on_conflict_clauses_rejected(self, dialect): source = ValuesSource(dialect, values_list=[[Literal(dialect, 1)]]) clause1 = OnConflictClause(dialect, conflict_target=["col_a"], do_nothing=True) clause2 = OnConflictClause(dialect, conflict_target=["col_b"], do_nothing=True) - expr = InsertExpression(dialect, into="t", source=source, on_conflict=[clause1, clause2]) + expr = InsertExpression(dialect, into=Table(dialect, 't'), source=source, on_conflict=[clause1, clause2]) with pytest.raises(UnsupportedFeatureError, match="multiple ON CONFLICT clauses"): expr.to_sql() @@ -63,7 +64,7 @@ class TestPostgresOnConflictRendering: def test_do_nothing(self, dialect): source = ValuesSource(dialect, values_list=[[Literal(dialect, 1)]]) clause = OnConflictClause(dialect, conflict_target=["id"], do_nothing=True) - expr = InsertExpression(dialect, into="users", columns=["id"], source=source, on_conflict=clause) + expr = InsertExpression(dialect, into=Table(dialect, 'users'), columns=["id"], source=source, on_conflict=clause) sql, params = expr.to_sql() assert sql == 'INSERT INTO "users" ("id") VALUES (%s) ON CONFLICT ("id") DO NOTHING' assert params == (1,) @@ -79,7 +80,7 @@ def test_do_update_with_excluded(self, dialect): update_assignments={"name": Column(dialect, "name", "EXCLUDED")}, ) expr = InsertExpression( - dialect, into="users", columns=["id", "name"], source=source, on_conflict=clause + dialect, into=Table(dialect, 'users'), columns=["id", "name"], source=source, on_conflict=clause ) sql, params = expr.to_sql() assert sql == ( @@ -99,7 +100,7 @@ def test_do_update_with_where(self, dialect): update_where=Column(dialect, "qty", "users") > Column(dialect, "qty", "EXCLUDED"), ) expr = InsertExpression( - dialect, into="users", columns=["id", "qty"], source=source, on_conflict=clause + dialect, into=Table(dialect, 'users'), columns=["id", "qty"], source=source, on_conflict=clause ) sql, params = expr.to_sql() assert sql == ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py index bfb5145c..2de9ffb7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py @@ -24,8 +24,8 @@ DropMaterializedViewExpression, QueryExpression, RefreshMaterializedViewExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import MaterializedView, Table from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( MaterializedViewAlterAction, @@ -65,14 +65,20 @@ def _source_query(dialect): return QueryExpression( dialect=dialect, select=[Column(dialect, "product_id")], - from_=TableExpression(dialect, "sales"), + from_=Table(dialect, "sales"), ) def _create(dialect, **kwargs): - kwargs.setdefault("view_name", "sales_summary") + """Build a CREATE MATERIALIZED VIEW, defaulting the view it names.""" + schema = kwargs.pop("schema", None) + name = kwargs.pop("view_name", "sales_summary") kwargs.setdefault("query", _source_query(dialect)) - return CreateMaterializedViewExpression(dialect=dialect, **kwargs) + return CreateMaterializedViewExpression( + dialect=dialect, + view=MaterializedView(dialect, name, schema_name=schema), + **kwargs, + ) class TestMaterializedViewMixinResolution: @@ -94,7 +100,7 @@ def test_drop_mv_formatter_owned_by_pg_mixin(self): owner = dialect.format_drop_materialized_view_statement.__qualname__ assert owner.startswith("PostgresMaterializedViewMixin.") sql, _ = DropMaterializedViewExpression( - dialect=dialect, view_name="sales_summary", if_exists=True, cascade=True + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), if_exists=True, cascade=True ).to_sql() assert sql == 'DROP MATERIALIZED VIEW IF EXISTS "sales_summary" CASCADE' @@ -180,7 +186,7 @@ class TestDropMaterializedViewFormatting: def test_drop_plain(self): dialect = _dialect() sql, params = DropMaterializedViewExpression( - dialect=dialect, view_name="sales_summary" + dialect=dialect, view=MaterializedView(dialect, 'sales_summary') ).to_sql() assert sql == 'DROP MATERIALIZED VIEW "sales_summary"' assert params == () @@ -188,7 +194,7 @@ def test_drop_plain(self): def test_drop_if_exists_cascade(self): dialect = _dialect() sql, _ = DropMaterializedViewExpression( - dialect=dialect, view_name="sales_summary", if_exists=True, cascade=True + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), if_exists=True, cascade=True ).to_sql() assert sql == 'DROP MATERIALIZED VIEW IF EXISTS "sales_summary" CASCADE' @@ -199,7 +205,7 @@ class TestRefreshMaterializedViewFormatting: def test_refresh_plain(self): dialect = _dialect() sql, params = RefreshMaterializedViewExpression( - dialect=dialect, view_name="sales_summary" + dialect=dialect, view=MaterializedView(dialect, 'sales_summary') ).to_sql() assert sql == 'REFRESH MATERIALIZED VIEW "sales_summary"' assert params == () @@ -208,14 +214,14 @@ def test_refresh_plain(self): def test_refresh_with_data_variants(self, with_data, expected): dialect = _dialect() sql, _ = RefreshMaterializedViewExpression( - dialect=dialect, view_name="sales_summary", with_data=with_data + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), with_data=with_data ).to_sql() assert sql == f'REFRESH MATERIALIZED VIEW "sales_summary"{expected}' def test_refresh_without_with_data_omits_clause(self): dialect = _dialect() sql, _ = RefreshMaterializedViewExpression( - dialect=dialect, view_name="sales_summary" + dialect=dialect, view=MaterializedView(dialect, 'sales_summary') ).to_sql() assert "WITH DATA" not in sql @@ -224,7 +230,7 @@ def test_refresh_concurrently_allowed_since_94(self, version): """T-12: CONCURRENTLY is emitted from PG 9.4 onwards.""" dialect = _dialect(version) sql, _ = RefreshMaterializedViewExpression( - dialect=dialect, view_name="sales_summary", concurrent=True + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), concurrent=True ).to_sql() assert sql == 'REFRESH MATERIALIZED VIEW CONCURRENTLY "sales_summary"' @@ -233,7 +239,7 @@ def test_refresh_concurrently_rejected_before_94(self): dialect = _dialect(PG_93) with pytest.raises(UnsupportedFeatureError) as exc: RefreshMaterializedViewExpression( - dialect=dialect, view_name="sales_summary", concurrent=True + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), concurrent=True ).to_sql() assert "CONCURRENTLY" in str(exc.value) @@ -242,13 +248,13 @@ def test_pg_expression_shares_same_gate(self): dialect = _dialect(PG_93) with pytest.raises(UnsupportedFeatureError): PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="sales_summary", concurrently=True + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), concurrently=True ).to_sql() def test_pg_expression_renders_concurrently_since_94(self): dialect = _dialect() sql, _ = PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="sales_summary", concurrently=True, with_data=True + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), concurrently=True, with_data=True ).to_sql() assert sql == 'REFRESH MATERIALIZED VIEW CONCURRENTLY "sales_summary" WITH DATA' @@ -272,23 +278,23 @@ def test_version_gate_matrix(self, version, feature, should_raise): dialect = _dialect(version) build = { "concurrent": lambda: RefreshMaterializedViewExpression( - dialect=dialect, view_name="mv", concurrent=True + dialect=dialect, view=MaterializedView(dialect, 'mv'), concurrent=True ), "if_not_exists": lambda: PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv", + view=MaterializedView(dialect, 'mv'), query=_source_query(dialect), if_not_exists=True, ), "tablespace": lambda: PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv", + view=MaterializedView(dialect, 'mv'), query=_source_query(dialect), tablespace="fast_ssd", ), "storage_options": lambda: PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv", + view=MaterializedView(dialect, 'mv'), query=_source_query(dialect), storage_options={"fillfactor": 70}, ), @@ -310,16 +316,14 @@ def test_refresh_schema_qualified_and_quoted(self): """T-17.""" dialect = _dialect() sql, _ = PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="mv_sales", schema="reporting" - ).to_sql() + dialect=dialect, view=MaterializedView(dialect, 'mv_sales', schema_name='reporting')).to_sql() assert sql == 'REFRESH MATERIALIZED VIEW "reporting"."mv_sales"' def test_mixed_case_and_reserved_word_quoted(self): """T-18: the previous bare f-string concatenation broke here.""" dialect = _dialect() sql, _ = PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="Order Summary", schema="Reporting" - ).to_sql() + dialect=dialect, view=MaterializedView(dialect, 'Order Summary', schema_name='Reporting')).to_sql() assert sql == 'REFRESH MATERIALIZED VIEW "Reporting"."Order Summary"' def test_create_mv_schema_qualified(self): @@ -327,9 +331,8 @@ def test_create_mv_schema_qualified(self): dialect = _dialect() sql, _ = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv_sales", + view=MaterializedView(dialect, 'mv_sales', schema_name='reporting'), query=_source_query(dialect), - schema="reporting", ).to_sql() assert sql.startswith('CREATE MATERIALIZED VIEW "reporting"."mv_sales" ') @@ -337,7 +340,7 @@ def test_drop_mv_schema_qualified(self): """T-20.""" dialect = _dialect() sql, _ = PostgresDropMaterializedViewExpression( - dialect=dialect, view_name="mv_sales", schema="reporting", if_exists=True + dialect=dialect, view=MaterializedView(dialect, 'mv_sales', schema_name='reporting'), if_exists=True ).to_sql() assert sql == 'DROP MATERIALIZED VIEW IF EXISTS "reporting"."mv_sales"' @@ -345,7 +348,7 @@ def test_no_schema_emits_bare_identifier(self): """T-21: no stray separator when schema is absent.""" dialect = _dialect() sql, _ = PostgresDropMaterializedViewExpression( - dialect=dialect, view_name="mv_sales" + dialect=dialect, view=MaterializedView(dialect, 'mv_sales') ).to_sql() assert sql == 'DROP MATERIALIZED VIEW "mv_sales"' assert "." not in sql @@ -354,12 +357,11 @@ def test_rejects_empty_names(self): dialect = _dialect() with pytest.raises(ValueError): PostgresCreateMaterializedViewExpression( - dialect=dialect, view_name=" ", query=_source_query(dialect) + dialect=dialect, view=MaterializedView(dialect, ' '), query=_source_query(dialect) ) with pytest.raises(ValueError): PostgresCreateMaterializedViewExpression( - dialect=dialect, view_name="mv", query=_source_query(dialect), schema="" - ) + dialect=dialect, view=MaterializedView(dialect, 'mv', schema_name=''), query=_source_query(dialect)) class TestCreateMaterializedViewIfNotExists: @@ -370,7 +372,7 @@ def test_if_not_exists_rendered(self): dialect = _dialect() sql, _ = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="sales_summary", + view=MaterializedView(dialect, 'sales_summary'), query=_source_query(dialect), if_not_exists=True, ).to_sql() @@ -383,7 +385,7 @@ def test_if_not_exists_false_omitted(self): """T-23.""" dialect = _dialect() sql, _ = PostgresCreateMaterializedViewExpression( - dialect=dialect, view_name="sales_summary", query=_source_query(dialect) + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), query=_source_query(dialect) ).to_sql() assert "IF NOT EXISTS" not in sql @@ -393,7 +395,7 @@ def test_if_not_exists_rejected_before_94(self): with pytest.raises(UnsupportedFeatureError): PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="sales_summary", + view=MaterializedView(dialect, 'sales_summary'), query=_source_query(dialect), if_not_exists=True, ).to_sql() @@ -406,13 +408,13 @@ class TestMaterializedViewExceptionContract: "build", [ lambda dialect: RefreshMaterializedViewExpression( - dialect=dialect, view_name="mv", concurrent=True + dialect=dialect, view=MaterializedView(dialect, 'mv'), concurrent=True ), lambda dialect: PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="mv", concurrently=True + dialect=dialect, view=MaterializedView(dialect, 'mv'), concurrently=True ), lambda dialect: PostgresCreateMaterializedViewExpression( - dialect=dialect, view_name="mv", query=_source_query(dialect), if_not_exists=True + dialect=dialect, view=MaterializedView(dialect, 'mv'), query=_source_query(dialect), if_not_exists=True ), ], ) @@ -429,7 +431,7 @@ def test_error_message_mentions_dialect_and_feature(self): dialect = _dialect(PG_93) with pytest.raises(UnsupportedFeatureError) as exc: RefreshMaterializedViewExpression( - dialect=dialect, view_name="mv", concurrent=True + dialect=dialect, view=MaterializedView(dialect, 'mv'), concurrent=True ).to_sql() message = str(exc.value) assert dialect.name in message @@ -444,9 +446,13 @@ class TestAlterMaterializedView: """G6: ALTER MATERIALIZED VIEW actions.""" def _alter(self, dialect, *actions, **kwargs): + schema = kwargs.pop("schema", None) + name = kwargs.pop("view_name", "sales_summary") return PostgresAlterMaterializedViewExpression( - dialect=dialect, view_name=kwargs.pop("view_name", "sales_summary"), - actions=list(actions), **kwargs + dialect=dialect, + view=MaterializedView(dialect, name, schema_name=schema), + actions=list(actions), + **kwargs, ) def test_rename_to(self): @@ -553,14 +559,14 @@ def test_requires_at_least_one_action(self): dialect = _dialect() with pytest.raises(ValueError): PostgresAlterMaterializedViewExpression( - dialect=dialect, view_name="mv", actions=[] + dialect=dialect, view=MaterializedView(dialect, 'mv'), actions=[] ) def test_rejects_non_action_objects(self): dialect = _dialect() with pytest.raises(TypeError): PostgresAlterMaterializedViewExpression( - dialect=dialect, view_name="mv", actions=[object()] + dialect=dialect, view=MaterializedView(dialect, 'mv'), actions=[object()] ) def test_rejects_unlisted_storage_parameters(self): @@ -575,7 +581,7 @@ def test_rejects_unlisted_storage_parameters(self): with pytest.raises(ValueError): PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv", + view=MaterializedView(dialect, 'mv'), query=_source_query(dialect), storage_options={bogus: 1}, ) @@ -585,7 +591,7 @@ def test_allows_unlisted_storage_parameters_explicitly(self): dialect = _dialect() expression = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv", + view=MaterializedView(dialect, 'mv'), query=_source_query(dialect), storage_options={"compresslevel": 4}, allow_unlisted_storage_parameters=True, @@ -704,7 +710,13 @@ def test_expressions_render_enum_members(self): ) by_name = PostgresSetMaterializedViewPropertiesAction(dialect, {"fillfactor": 70}) assert by_member.to_sql() == by_name.to_sql() - assert "fillfactor = 70" in by_member.to_sql() + # ``to_sql`` returns ``(sql, params)``. It used to return the bare SQL + # string here, which made the ``in`` below a substring test by accident and + # made ``by_member.to_sql() == by_name.to_sql()`` compare strings rather + # than the documented shape. + member_sql, member_params = by_member.to_sql() + assert member_params == () + assert "fillfactor = 70" in member_sql class TestMaterializedViewProtocolAndSurface: @@ -759,15 +771,15 @@ def test_format_method_targets_exist(self): expressions = [ _create(dialect), PostgresCreateMaterializedViewExpression( - dialect=dialect, view_name="mv", query=query + dialect=dialect, view=MaterializedView(dialect, 'mv'), query=query ), - DropMaterializedViewExpression(dialect=dialect, view_name="mv"), - PostgresDropMaterializedViewExpression(dialect=dialect, view_name="mv"), - RefreshMaterializedViewExpression(dialect=dialect, view_name="mv"), - PostgresRefreshMaterializedViewExpression(dialect=dialect, name="mv"), + DropMaterializedViewExpression(dialect=dialect, view=MaterializedView(dialect, 'mv')), + PostgresDropMaterializedViewExpression(dialect=dialect, view=MaterializedView(dialect, 'mv')), + RefreshMaterializedViewExpression(dialect=dialect, view=MaterializedView(dialect, 'mv')), + PostgresRefreshMaterializedViewExpression(dialect=dialect, view=MaterializedView(dialect, 'mv')), PostgresAlterMaterializedViewExpression( dialect=dialect, - view_name="mv", + view=MaterializedView(dialect, 'mv'), actions=[PostgresRenameMaterializedViewAction(dialect, "mv2")], ), ] @@ -778,10 +790,12 @@ def test_format_method_targets_exist(self): def test_legacy_aliases_preserved(self): """T-40: examples rely on the name=/concurrently= aliases.""" dialect = _dialect() + view = MaterializedView(dialect, "sales_summary", schema_name="reporting") expression = PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="sales_summary", concurrently=True + dialect=dialect, view=view, concurrently=True ) - assert expression.name == expression.view_name == "sales_summary" + assert expression.name == view.name == "sales_summary" + assert expression.schema == "reporting" assert expression.concurrently is True assert expression.concurrent is True diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_object_kind_guards.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_object_kind_guards.py new file mode 100644 index 00000000..e7e4f163 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_object_kind_guards.py @@ -0,0 +1,288 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_object_kind_guards.py +""" +A statement handed the wrong kind of catalogue object must refuse, not render. + +Why this file exists +==================== + +A statement now holds the object it acts on, and ``to_sql()`` dispatches through +*that object's* ``format_method``. So +``CreateIndexExpression(dialect, Index(dialect, "i"), View(dialect, "v"))`` +rendered ``CREATE INDEX i ON v (...)`` -- well-formed SQL naming the wrong kind of +object, with no error and no warning. Before objects carried names as strings, +``format_identifier`` accepted anything and a wrong type raised; switching to +objects removed the accidental check without putting a real one anywhere. + +The check belongs in the formatter that applies it, and this backend shadows +several of core's. A shadowing body is the one that runs the MRO lookup, so the +check core's body carries is never reached -- which is why the guards had to be +written here as well as in core. + +What is asserted +================ + +For every (statement class, slot, required kind, impostor kind) probe below, the +slot is filled with a different schema-object kind that is not a subclass of the +required one, and ``to_sql()`` must refuse. The real instance is built first and +one slot is then swapped, so everything else stays valid and the only thing the +refusal can be about is the slot under test. + +A slot annotated ``SchemaObject`` legitimately accepts any object, so it is never +probed -- doing so would report a defect that is not there. + +How the probe table is built (so it can be audited) +=================================================== + +The (class, slot, kind) triples are derived, not hand-listed: the ``__init__`` +signatures of the classes collected by ``collect_expression_classes`` over the +PostgreSQL expression package and core's -- never from ``ExpressionRegistry``, +which is process-global -- are read for parameters annotated with a schema-object +class. Statements whose ``format_method`` the PostgreSQL dialect cannot resolve are +excluded, because an entry point that is never dispatched is not something +``to_sql()`` renders. Each impostor is then the first two buildable object kinds, +in sorted order, that are neither the required class nor a subclass of it. +""" + +import copy +import importlib +import inspect +import typing + +import pytest + +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression import objects as objects_mod +from rhosocial.activerecord.testsuite.utils.expression import ( + collect_expression_classes, + make_instance, +) + +PG_EXPR_PKG = "rhosocial.activerecord.backend.impl.postgres.expression" +CORE_EXPR_PKG = "rhosocial.activerecord.backend.expression" + +#: Suffixes of the schema-object classes, used to recognise one in an annotation. +_OBJECT_SUFFIXES = ( + "Table", "View", "Index", "Schema", "Sequence", "Type", "Domain", "Function", + "Procedure", "Trigger", "Database", "Synonym", "PropertyGraph", "Routine", + "RelationObject", "SchemaObject", +) + + +def _annotation_names(annotation): + """Every class name mentioned anywhere inside an annotation.""" + found = set() + stack = [annotation] + while stack: + item = stack.pop() + if item is inspect.Parameter.empty or item is None: + continue + if isinstance(item, str): + found.add(item.strip("'\"")) + continue + if isinstance(item, type): + found.add(item.__name__) + continue + origin = typing.get_origin(item) + if origin is not None: + stack.extend(typing.get_args(item)) + return found + + +def _object_classes(): + """The schema-object classes this package defines, keyed by name.""" + classes = {} + for attr in dir(objects_mod): + obj = getattr(objects_mod, attr) + if isinstance(obj, type) and attr.endswith(_OBJECT_SUFFIXES): + classes[attr] = obj + return classes + + +def _statement_classes(): + found = {} + for package in (PG_EXPR_PKG, CORE_EXPR_PKG): + found.update(collect_expression_classes(package)) + return found + + +def _buildable_kinds(dialect): + """Object kinds that construct from a dialect and a bare name.""" + buildable = [] + for name in sorted(_object_classes()): + cls = getattr(objects_mod, name, None) + if cls is None: + continue + try: + cls(dialect, "x") + except Exception: # noqa: BLE001 + # A kind that cannot self-construct is not a usable impostor. This is + # not a failure: a Database and a Table both take a bare name, a + # RelationObject base deliberately does not. + continue + buildable.append(cls) + return buildable + + +def _renderable(dialect): + """Only statements whose ``format_method`` the dialect can resolve. + + Some PostgreSQL expressions name a formatter that does not exist and are only + ever called directly as entry points; ``to_sql()`` never dispatches to them, so + refusing is not their contract and probing them would be meaningless. + """ + resolvable = set() + for klass in type(dialect).__mro__: + resolvable.update(klass.__dict__) + out = {} + for fqn, cls in _statement_classes().items(): + try: + method = cls.format_method.fget(None) + except Exception: # noqa: BLE001 - abstract property, or none at all + continue + if method in resolvable and callable(getattr(dialect, method, None)): + out[fqn] = cls + return out + + +def _object_slots(): + """{statement fqn: {slot: required object class}}. + + Read from the constructor signature, so a slot added later shows up without + this file being touched. + """ + objects = _object_classes() + slots = {} + for fqn, cls in sorted(_statement_classes().items()): + try: + sig = inspect.signature(cls.__init__) + except (TypeError, ValueError): + continue + for pname, param in sig.parameters.items(): + if pname in ("self", "dialect"): + continue + matched = _annotation_names(param.annotation) & set(objects) + if matched: + slots.setdefault(fqn, {})[pname] = objects[sorted(matched)[0]] + return slots + + +def _build_probes(dialect): + """``[(fqn, slot, required name, impostor name)]``, derived not listed. + + Only statements whose *unmodified* instance renders are probed. A statement + that already fails for an unrelated reason -- the property-graph statements, + which PostgreSQL does not support at all, and ``CreateTriggerExpression``, + whose introspective construction supplies a string where an enum belongs -- + cannot demonstrate anything about a slot: it raises either way. Including them + would make the table look broader while proving less, which is exactly the + substitution this file was written to end. + """ + renderable = _renderable(dialect) + buildable = _buildable_kinds(dialect) + rows = [] + skipped = [] + for fqn, slots in sorted(_object_slots().items()): + if fqn not in renderable: + skipped.append((fqn, "format_method is not resolvable on this dialect")) + continue + instance, source = make_instance(renderable[fqn], dialect) + if instance is None: + skipped.append((fqn, f"the harness cannot construct it ({source})")) + continue + try: + instance.to_sql() + except Exception as exc: # noqa: BLE001 + skipped.append( + (fqn, f"it does not render as built ({type(exc).__name__})") + ) + continue + for slot, required in sorted(slots.items()): + impostors = [ + cls + for cls in buildable + if cls is not required and not issubclass(cls, required) + ][:2] + for impostor in impostors: + rows.append( + (fqn, slot, required.__name__, impostor.__name__) + ) + return rows, skipped + + +def _probe_dialect(): + """A PostgreSQL 15 dialect, so the table is built from a real object graph.""" + dialect_module = importlib.import_module( + "rhosocial.activerecord.backend.impl.postgres.dialect" + ) + return dialect_module.PostgresDialect(version=(15, 0, 0)) + + +PROBES, SKIPPED = _build_probes(_probe_dialect()) + +#: Labels pytest can put in a test id; a dotted fqn is already readable, so this +#: only shortens the object name. +_IDS = [f"{fqn.rsplit('.', 1)[-1]}.{slot}!={required}->{impostor}" + for fqn, slot, required, impostor in PROBES] + + +def test_probe_table_is_not_empty(): + """A probe table that silently emptied would make every test below vacuous.""" + assert len(PROBES) > 50, ( + f"only {len(PROBES)} wrong-kind probes were built; the collection method " + f"has probably stopped matching the statement tree" + ) + + +def test_skipped_statements_are_still_the_ones_we_think(): + """Nothing left the probe table for a new reason. + + The exclusions are structural -- a statement PostgreSQL cannot render at all, + or one the harness cannot build correctly -- so a new arrival means the + construction or the rendering changed, and the table should be revisited. + """ + for fqn, reason in SKIPPED: + assert reason in ( + "format_method is not resolvable on this dialect", + ) or "cannot construct it" in reason or "does not render as built" in reason, ( + f"{fqn} was excluded from the probe table for an unrecognised reason: " + f"{reason}" + ) + + +@pytest.mark.parametrize( + "fqn,slot,required_name,impostor_name", PROBES, ids=_IDS +) +def test_wrong_object_kind_is_refused(fqn, slot, required_name, impostor_name): + """A statement handed the wrong object kind must refuse at render time. + + ``TypeError`` is the contract: reaching the formatter with the wrong object is + a programming error, not a missing feature, and the message has to name the + slot or the kind so the mistake can be found. ``UnsupportedFeatureError`` is + accepted as a refusal too -- a dialect may refuse the statement for an + unrelated reason on the way past -- but it is not required to name the slot, + because it is not talking about the slot. Anything that *renders* is the defect + this file exists for: well-formed SQL naming an object the caller never asked + for. + """ + dialect = _probe_dialect() + cls = _renderable(dialect)[fqn] + instance, source = make_instance(cls, dialect) + assert instance is not None, f"{fqn}: could not be constructed ({source})" + impostor = getattr(objects_mod, impostor_name) + swapped = copy.copy(instance) + object.__setattr__(swapped, slot, impostor(dialect, "wrongkind")) + try: + swapped.to_sql() + except TypeError as exc: + assert slot in str(exc) or required_name in str(exc), ( + f"{fqn}.{slot} refused with a TypeError naming neither the slot nor " + f"the required kind: {exc}" + ) + return + except UnsupportedFeatureError: + return + pytest.fail( + f"{fqn}.{slot} must be {required_name}; handed a {impostor_name} and it " + f"rendered {swapped.to_sql()!r} -- well-formed SQL naming the wrong kind " + f"of object" + ) \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py index 2d183b96..936197d6 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_property_graph_query_format.py @@ -29,6 +29,11 @@ ) from rhosocial.activerecord.backend.expression.core import Column, Literal from rhosocial.activerecord.backend.expression.query_parts import WhereClause +from rhosocial.activerecord.backend.expression.objects import ( + EdgeTable as EdgeTableObject, + NodeTable, + PropertyGraph, +) class TestPGQProtocolVersionGating: @@ -75,14 +80,14 @@ def test_graph_table_override_without_match_fails_closed(self): (19, 0, 4), graph_feature_overrides={"graph_table": True}, ) - vertex = GraphVertex(dialect, "p", "person") + vertex = GraphVertex(dialect, "p", NodeTable(dialect, 'person')) match = MatchClause(dialect, vertex) columns = ColumnsClause(dialect, GraphColumn("p", "name")) assert dialect.supports_graph_match() is False assert dialect.supports_graph_table() is False with pytest.raises(UnsupportedFeatureError, match="GRAPH_TABLE"): - GraphTableExpression(dialect, "g", match, columns).to_sql() + GraphTableExpression(dialect, PropertyGraph(dialect, 'g'), match, columns).to_sql() def test_unknown_graph_feature_override_is_rejected(self): with pytest.raises(ValueError, match="Unknown graph feature override"): @@ -107,7 +112,7 @@ def test_comma_separated_patterns_unsupported_by_default(self): def test_quantified_path_raises_on_format(self, pg19_dialect: PostgresDialect): from rhosocial.activerecord.backend.expression import QuantifiedPath - edge = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.RIGHT) + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.RIGHT) quantified_path = QuantifiedPath(pg19_dialect, edge) with pytest.raises(UnsupportedFeatureError): quantified_path.to_sql() @@ -128,13 +133,13 @@ class TestPGQGraphVertexFormat: """SQL formatting tests for GraphVertex with PG19 dialect.""" def test_basic(self, pg19_dialect: PostgresDialect): - v = GraphVertex(pg19_dialect, "p", "person") + v = GraphVertex(pg19_dialect, "p", NodeTable(pg19_dialect, 'person')) sql, params = v.to_sql() assert sql == '(p IS "person")' assert params == () def test_anonymous(self, pg19_dialect: PostgresDialect): - v = GraphVertex(pg19_dialect, variable=None, table="person") + v = GraphVertex(pg19_dialect, variable=None, table=NodeTable(pg19_dialect, 'person')) sql, params = v.to_sql() assert sql == '("person")' assert params == () @@ -144,14 +149,14 @@ def test_with_where(self, pg19_dialect: PostgresDialect): pg19_dialect, condition=Column(pg19_dialect, "age") > Literal(pg19_dialect, 18), ) - vertex = GraphVertex(pg19_dialect, "p", "person", where=where) + vertex = GraphVertex(pg19_dialect, "p", NodeTable(pg19_dialect, 'person'), where=where) sql, params = vertex.to_sql() assert "(p IS" in sql assert "WHERE" in sql assert params == (18,) def test_invalid_variable_name(self, pg19_dialect: PostgresDialect): - vertex = GraphVertex(pg19_dialect, "bad var", "person") + vertex = GraphVertex(pg19_dialect, "bad var", NodeTable(pg19_dialect, 'person')) with pytest.raises(ValueError, match="Invalid variable name"): vertex.to_sql() @@ -160,16 +165,16 @@ class TestPGQGraphEdgeFormat: """SQL formatting tests for GraphEdge with explicit capability overrides.""" def test_right(self, pg19_dialect: PostgresDialect): - edge = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.RIGHT) + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.RIGHT) sql, params = edge.to_sql() assert sql == '-[e IS "knows"]->' def test_left(self, pg19_dialect: PostgresDialect): - edge = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.LEFT) + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.LEFT) assert edge.to_sql()[0] == '<-[e IS "knows"]-' def test_any(self, pg19_dialect: PostgresDialect): - edge = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.ANY) + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.ANY) assert edge.to_sql()[0] == '<-[e IS "knows"]->' def test_anonymous(self, pg19_dialect: PostgresDialect): @@ -177,16 +182,16 @@ def test_anonymous(self, pg19_dialect: PostgresDialect): assert edge.to_sql()[0] == '-[]->' def test_none_direction(self, pg19_dialect: PostgresDialect): - edge = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.NONE) + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.NONE) assert edge.to_sql()[0] == '-[e IS "knows"]-' def test_table_without_variable_is_rejected(self, pg19_dialect: PostgresDialect): - edge = GraphEdge(pg19_dialect, table="knows", direction=GraphEdgeDirection.RIGHT) + edge = GraphEdge(pg19_dialect, table=EdgeTableObject(pg19_dialect, 'knows'), direction=GraphEdgeDirection.RIGHT) with pytest.raises(ValueError, match="table requires a variable"): edge.to_sql() def test_invalid_direction_is_rejected(self, pg19_dialect: PostgresDialect): - edge = GraphEdge(pg19_dialect, "e", "knows", direction="right") + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), direction="right") with pytest.raises(ValueError, match="Invalid graph edge direction"): edge.to_sql() @@ -198,7 +203,7 @@ def test_invalid_variable_name(self, pg19_dialect: PostgresDialect): edge = GraphEdge( pg19_dialect, variable="bad var", - table="knows", + table=EdgeTableObject(pg19_dialect, 'knows'), direction=GraphEdgeDirection.RIGHT, ) with pytest.raises(ValueError, match="Invalid variable name"): @@ -209,16 +214,16 @@ class TestPGQMatchClauseFormat: """SQL formatting tests for MatchClause with explicit capability overrides.""" def test_single_vertex(self, pg19_dialect: PostgresDialect): - vertex = GraphVertex(pg19_dialect, "p", "person") + vertex = GraphVertex(pg19_dialect, "p", NodeTable(pg19_dialect, 'person')) match = MatchClause(pg19_dialect, vertex) sql, params = match.to_sql() assert "MATCH" in sql assert "(p IS" in sql def test_path_pattern(self, pg19_dialect: PostgresDialect): - first = GraphVertex(pg19_dialect, "a", "person") - edge = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.RIGHT) - second = GraphVertex(pg19_dialect, "b", "person") + first = GraphVertex(pg19_dialect, "a", NodeTable(pg19_dialect, 'person')) + edge = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.RIGHT) + second = GraphVertex(pg19_dialect, "b", NodeTable(pg19_dialect, 'person')) match = MatchClause(pg19_dialect, first, edge, second) sql, params = match.to_sql() assert "MATCH" in sql @@ -232,7 +237,7 @@ def test_empty_path_is_rejected(self, pg19_dialect: PostgresDialect): match.to_sql() def test_anonymous_vertex(self, pg19_dialect: PostgresDialect): - vertex = GraphVertex(pg19_dialect, variable=None, table="person") + vertex = GraphVertex(pg19_dialect, variable=None, table=NodeTable(pg19_dialect, 'person')) match = MatchClause(pg19_dialect, vertex) sql, params = match.to_sql() assert sql == 'MATCH ("person")' @@ -243,10 +248,10 @@ class TestPGQGraphTableFormat: """SQL formatting tests for GraphTableExpression with PG19 dialect.""" def test_basic(self, pg19_dialect: PostgresDialect): - v = GraphVertex(pg19_dialect, "p", "person") + v = GraphVertex(pg19_dialect, "p", NodeTable(pg19_dialect, 'person')) cols = ColumnsClause(pg19_dialect, GraphColumn("p", "name")) m = MatchClause(pg19_dialect, v) - gt = GraphTableExpression(pg19_dialect, "g", m, cols) + gt = GraphTableExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), m, cols) sql, params = gt.to_sql() assert 'GRAPH_TABLE ("g" MATCH' in sql assert "COLUMNS" in sql @@ -254,21 +259,21 @@ def test_basic(self, pg19_dialect: PostgresDialect): def test_with_where(self, pg19_dialect: PostgresDialect): where = WhereClause(pg19_dialect, condition=Column(pg19_dialect, "age") > Literal(pg19_dialect, 18)) - v = GraphVertex(pg19_dialect, "p", "person", where=where) - e = GraphEdge(pg19_dialect, "e", "knows", GraphEdgeDirection.RIGHT) - b = GraphVertex(pg19_dialect, "b", "person") + v = GraphVertex(pg19_dialect, "p", NodeTable(pg19_dialect, 'person'), where=where) + e = GraphEdge(pg19_dialect, "e", EdgeTableObject(pg19_dialect, 'knows'), GraphEdgeDirection.RIGHT) + b = GraphVertex(pg19_dialect, "b", NodeTable(pg19_dialect, 'person')) cols = ColumnsClause(pg19_dialect, GraphColumn("b", "name")) m = MatchClause(pg19_dialect, v, e, b) - gt = GraphTableExpression(pg19_dialect, "g", m, cols) + gt = GraphTableExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), m, cols) sql, params = gt.to_sql() assert "WHERE" in sql assert params == (18,) def test_with_alias(self, pg19_dialect: PostgresDialect): - v = GraphVertex(pg19_dialect, "p", "person") + v = GraphVertex(pg19_dialect, "p", NodeTable(pg19_dialect, 'person')) cols = ColumnsClause(pg19_dialect, GraphColumn("p", "name")) m = MatchClause(pg19_dialect, v) - gt = GraphTableExpression(pg19_dialect, "g", m, cols, alias="t") + gt = GraphTableExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), m, cols, alias="t") sql, params = gt.to_sql() assert 'GRAPH_TABLE ("g" MATCH' in sql assert 'AS "t"' in sql @@ -284,16 +289,16 @@ class TestPGQDDLFormat: """SQL formatting tests for PGQ DDL expressions with PG19 dialect.""" def test_create_property_graph(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "people", + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'people'), labels=["person"], key_columns=["id"], properties=TablePropertiesClause(pg19_dialect, columns=["id", "name"])) - et = EdgeTable(pg19_dialect, "knows", ["person_a"], ["person_b"], + et = EdgeTable(pg19_dialect, EdgeTableObject(pg19_dialect, 'knows'), ["person_a"], ["person_b"], references_source=("people", ["id"]), references_destination=("people", ["id"]), labels=["knows"], properties=TablePropertiesClause(pg19_dialect, columns=["since"])) - expr = CreatePropertyGraphExpression(pg19_dialect, "test_graph", [vt], [et]) + expr = CreatePropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'test_graph'), [vt], [et]) sql, params = expr.to_sql() assert "CREATE PROPERTY GRAPH" in sql assert '"people"' in sql @@ -302,21 +307,21 @@ def test_create_property_graph(self, pg19_dialect: PostgresDialect): assert "DESTINATION KEY" in sql def test_create_property_graph_if_not_exists(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "person") - expr = CreatePropertyGraphExpression(pg19_dialect, "my_graph", [vt], + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'person')) + expr = CreatePropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'my_graph'), [vt], if_not_exists=True) sql, params = expr.to_sql() assert "CREATE PROPERTY GRAPH IF NOT EXISTS" in sql def test_create_property_graph_no_edges(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "person") - expr = CreatePropertyGraphExpression(pg19_dialect, "my_graph", [vt]) + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'person')) + expr = CreatePropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'my_graph'), [vt]) sql, params = expr.to_sql() assert "EDGE TABLES" not in sql assert 'VERTEX TABLES ("person"' in sql def test_vertex_table_with_alias(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "person", alias="p", + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'person'), alias="p", labels=["Person"], key_columns=["id"]) sql, params = vt.to_sql() @@ -325,7 +330,7 @@ def test_vertex_table_with_alias(self, pg19_dialect: PostgresDialect): assert "KEY" in sql def test_edge_table_with_alias(self, pg19_dialect: PostgresDialect): - et = EdgeTable(pg19_dialect, "knows", ["pid"], ["fid"], + et = EdgeTable(pg19_dialect, EdgeTableObject(pg19_dialect, 'knows'), ["pid"], ["fid"], alias="k", labels=["Knows"]) sql, params = et.to_sql() @@ -347,24 +352,24 @@ def test_properties_none(self, pg19_dialect: PostgresDialect): assert params == () def test_drop_property_graph(self, pg19_dialect: PostgresDialect): - expr = DropPropertyGraphExpression(pg19_dialect, "test_graph", if_exists=True) + expr = DropPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'test_graph'), if_exists=True) sql, params = expr.to_sql() assert 'DROP PROPERTY GRAPH IF EXISTS "test_graph"' in sql def test_drop_cascade(self, pg19_dialect: PostgresDialect): - expr = DropPropertyGraphExpression(pg19_dialect, "test_graph", cascade=True) + expr = DropPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'test_graph'), cascade=True) sql, params = expr.to_sql() assert "CASCADE" in sql def test_edge_table_no_references(self, pg19_dialect: PostgresDialect): - et = EdgeTable(pg19_dialect, "knows", ["pid"], ["fid"]) + et = EdgeTable(pg19_dialect, EdgeTableObject(pg19_dialect, 'knows'), ["pid"], ["fid"]) sql, params = et.to_sql() assert 'SOURCE KEY ("pid")' in sql assert 'DESTINATION KEY ("fid")' in sql assert "REFERENCES" not in sql def test_edge_table_with_key_columns(self, pg19_dialect: PostgresDialect): - et = EdgeTable(pg19_dialect, "knows", ["pid"], ["fid"], + et = EdgeTable(pg19_dialect, EdgeTableObject(pg19_dialect, 'knows'), ["pid"], ["fid"], key_columns=["id"], references_source=("people", ["id"]), references_destination=("people", ["id"])) @@ -372,16 +377,16 @@ def test_edge_table_with_key_columns(self, pg19_dialect: PostgresDialect): assert 'KEY ("id")' in sql def test_alter_add_vertex(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "new_table", labels=["NewLabel"]) - expr = AlterPropertyGraphExpression(pg19_dialect, "g", "ADD", "VERTEX TABLES", + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'new_table'), labels=["NewLabel"]) + expr = AlterPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), "ADD", "VERTEX TABLES", vertex_tables=[vt]) sql, params = expr.to_sql() assert "ALTER PROPERTY GRAPH" in sql assert "ADD" in sql def test_alter_drop_vertex_tables(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "old_table") - expr = AlterPropertyGraphExpression(pg19_dialect, "g", "DROP", "VERTEX TABLES", + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'old_table')) + expr = AlterPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), "DROP", "VERTEX TABLES", vertex_tables=[vt]) sql, params = expr.to_sql() assert "DROP" in sql @@ -389,17 +394,17 @@ def test_alter_drop_vertex_tables(self, pg19_dialect: PostgresDialect): assert '"old_table"' in sql def test_alter_with_edge_tables(self, pg19_dialect: PostgresDialect): - et = EdgeTable(pg19_dialect, "knows", ["pid"], ["fid"]) - expr = AlterPropertyGraphExpression(pg19_dialect, "g", "DROP", "EDGE TABLES", + et = EdgeTable(pg19_dialect, EdgeTableObject(pg19_dialect, 'knows'), ["pid"], ["fid"]) + expr = AlterPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), "DROP", "EDGE TABLES", edge_tables=[et]) sql, params = expr.to_sql() assert "DROP" in sql assert '"knows"' in sql def test_alter_with_both_tables(self, pg19_dialect: PostgresDialect): - vt = VertexTable(pg19_dialect, "person") - et = EdgeTable(pg19_dialect, "knows", ["pid"], ["fid"]) - expr = AlterPropertyGraphExpression(pg19_dialect, "g", "ADD", "TABLES", + vt = VertexTable(pg19_dialect, NodeTable(pg19_dialect, 'person')) + et = EdgeTable(pg19_dialect, EdgeTableObject(pg19_dialect, 'knows'), ["pid"], ["fid"]) + expr = AlterPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), "ADD", "TABLES", vertex_tables=[vt], edge_tables=[et]) sql, params = expr.to_sql() assert '"person"' in sql @@ -417,7 +422,7 @@ def test_alter_with_both_tables(self, pg19_dialect: PostgresDialect): def test_alter_rejects_non_whitelisted_keywords( self, pg19_dialect, action, target, message ): - expr = AlterPropertyGraphExpression(pg19_dialect, "g", action, target) + expr = AlterPropertyGraphExpression(pg19_dialect, PropertyGraph(pg19_dialect, 'g'), action, target) with pytest.raises(ValueError, match=f"Invalid ALTER PROPERTY GRAPH {message}"): expr.to_sql() @@ -431,17 +436,17 @@ def withdrawn_dialect(self): return PostgresDialect((19, 0, 4)) def test_graph_vertex_unsupported(self, withdrawn_dialect: PostgresDialect): - vertex = GraphVertex(withdrawn_dialect, "p", "person") + vertex = GraphVertex(withdrawn_dialect, "p", NodeTable(withdrawn_dialect, 'person')) with pytest.raises(UnsupportedFeatureError): vertex.to_sql() def test_graph_edge_unsupported(self, withdrawn_dialect: PostgresDialect): - edge = GraphEdge(withdrawn_dialect, "e", "knows", GraphEdgeDirection.RIGHT) + edge = GraphEdge(withdrawn_dialect, "e", EdgeTableObject(withdrawn_dialect, 'knows'), GraphEdgeDirection.RIGHT) with pytest.raises(UnsupportedFeatureError): edge.to_sql() def test_match_clause_unsupported(self, withdrawn_dialect: PostgresDialect): - vertex = GraphVertex(withdrawn_dialect, "p", "person") + vertex = GraphVertex(withdrawn_dialect, "p", NodeTable(withdrawn_dialect, 'person')) match = MatchClause(withdrawn_dialect, vertex) with pytest.raises(UnsupportedFeatureError): match.to_sql() @@ -452,38 +457,38 @@ def test_graph_columns_clause_unsupported(self, withdrawn_dialect: PostgresDiale columns.to_sql() def test_graph_table_unsupported(self, withdrawn_dialect: PostgresDialect): - vertex = GraphVertex(withdrawn_dialect, "p", "person") + vertex = GraphVertex(withdrawn_dialect, "p", NodeTable(withdrawn_dialect, 'person')) columns = ColumnsClause(withdrawn_dialect, GraphColumn("p", "name")) match = MatchClause(withdrawn_dialect, vertex) - graph_table = GraphTableExpression(withdrawn_dialect, "g", match, columns) + graph_table = GraphTableExpression(withdrawn_dialect, PropertyGraph(withdrawn_dialect, 'g'), match, columns) with pytest.raises(UnsupportedFeatureError): graph_table.to_sql() def test_vertex_table_unsupported(self, withdrawn_dialect: PostgresDialect): - vertex_table = VertexTable(withdrawn_dialect, "person") + vertex_table = VertexTable(withdrawn_dialect, NodeTable(withdrawn_dialect, 'person')) with pytest.raises(UnsupportedFeatureError): vertex_table.to_sql() def test_edge_table_unsupported(self, withdrawn_dialect: PostgresDialect): - edge_table = EdgeTable(withdrawn_dialect, "knows", ["pid"], ["fid"]) + edge_table = EdgeTable(withdrawn_dialect, EdgeTableObject(withdrawn_dialect, 'knows'), ["pid"], ["fid"]) with pytest.raises(UnsupportedFeatureError): edge_table.to_sql() def test_create_property_graph_unsupported(self, withdrawn_dialect: PostgresDialect): - vertex_table = VertexTable(withdrawn_dialect, "person") - expression = CreatePropertyGraphExpression(withdrawn_dialect, "g", [vertex_table]) + vertex_table = VertexTable(withdrawn_dialect, NodeTable(withdrawn_dialect, 'person')) + expression = CreatePropertyGraphExpression(withdrawn_dialect, PropertyGraph(withdrawn_dialect, 'g'), [vertex_table]) with pytest.raises(UnsupportedFeatureError): expression.to_sql() def test_drop_property_graph_unsupported(self, withdrawn_dialect: PostgresDialect): - expression = DropPropertyGraphExpression(withdrawn_dialect, "g") + expression = DropPropertyGraphExpression(withdrawn_dialect, PropertyGraph(withdrawn_dialect, 'g')) with pytest.raises(UnsupportedFeatureError): expression.to_sql() def test_alter_property_graph_unsupported(self, withdrawn_dialect: PostgresDialect): expression = AlterPropertyGraphExpression( withdrawn_dialect, - "g", + PropertyGraph(withdrawn_dialect, 'g'), "ADD", "VERTEX TABLES", ) 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 index b8c4a03e..b92caca1 100644 --- 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 @@ -1,11 +1,21 @@ # tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_schema_support.py -"""Tests for the SchemaSupport capability declared on the PostgreSQL dialect. +"""Tests for the schema 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. +Two questions, and they are not the same one. Does the engine *have* schemas -- +the DDL question, answered by ``supports_schema()`` and its granular siblings? +May a *name* be qualified with one -- the naming question, answered by +``supports_schema_qualification()``? PostgreSQL answers yes to both, and it also +has a database above its schema, so the outer namespace is reported too. + +The rendering tests use the ``Schema`` object rather than a string, because a +schema name is now catalogue identity: the object carries the name and renders +itself. """ -from rhosocial.activerecord.backend.dialect.protocols import SchemaSupport +from rhosocial.activerecord.backend.dialect.protocols import NamespaceSupport +from rhosocial.activerecord.backend.expression.objects import ( + Schema, + Table, +) from rhosocial.activerecord.backend.expression.statements.ddl_schema import ( CreateSchemaExpression, DropSchemaExpression, @@ -22,8 +32,14 @@ def _dialect(self) -> 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_implements_namespace_support_protocol(self): + """The naming question lives on NamespaceSupport now. + + Naming was split from rendering, so the protocol that carries the + namespace switches is no longer the schema-DDL one; it is the base + every object protocol derives from. + """ + assert isinstance(self._dialect(), NamespaceSupport) def test_granular_schema_ddl_capabilities(self): d = self._dialect() @@ -37,30 +53,79 @@ def test_schema_authorization_capability(self): assert self._dialect().supports_schema_authorization() is True +class TestSchemaQualification: + """PostgreSQL renders a name with its schema, and with a database above it. + + Distinct from :class:`TestSchemaCapability`: having schemas is not the same + as qualifying a name with one, and a dialect can answer the two + differently. PostgreSQL qualifies both, so this is asserted rather than + assumed. + """ + + def _dialect(self) -> PostgresDialect: + return PostgresDialect() + + def test_supports_schema_qualification_is_true(self): + assert self._dialect().supports_schema_qualification() is True + + def test_supports_catalog_is_true(self): + assert self._dialect().supports_catalog() is True + + def test_supports_catalog_qualification_is_true(self): + assert self._dialect().supports_catalog_qualification() is True + + def test_schema_qualified_table_renders_both_levels(self): + d = self._dialect() + sql, params = d.format_table_object( + Table(d, "users", catalog_name="appdb", schema_name="app") + ) + assert sql == '"appdb"."app"."users"' + assert params == () + + def test_schema_qualified_table_without_catalog_omits_it(self): + d = self._dialect() + sql, _ = d.format_table_object(Table(d, "users", schema_name="app")) + assert sql == '"app"."users"' + + def test_unqualified_table_is_not_qualified(self): + d = self._dialect() + sql, _ = d.format_table_object(Table(d, "users")) + assert sql == '"users"' + + class TestSchemaDDLFormatting: """CREATE/DROP SCHEMA rendering through the standard core formatters.""" def _dialect(self) -> PostgresDialect: return PostgresDialect() + def _schema(self) -> Schema: + return Schema(self._dialect(), "app") + def test_create_schema(self): - sql, params = CreateSchemaExpression(self._dialect(), "app").to_sql() + sql, params = CreateSchemaExpression(self._dialect(), self._schema()).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() + sql, _ = CreateSchemaExpression( + self._dialect(), self._schema(), 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() + sql, _ = CreateSchemaExpression( + self._dialect(), self._schema(), 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() + sql, params = DropSchemaExpression(self._dialect(), self._schema()).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' + sql, _ = DropSchemaExpression( + self._dialect(), self._schema(), if_exists=True, cascade=True + ).to_sql() + assert sql == 'DROP SCHEMA IF EXISTS "app" CASCADE' \ No newline at end of file diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/conftest.py index 1a24b64b..016ddb9f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/conftest.py @@ -9,8 +9,8 @@ FunctionCall, GroupByHavingClause, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import MaterializedView, Table from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateMaterializedViewExpression, ) @@ -64,7 +64,7 @@ def _build(): FunctionCall(dialect, "COUNT", Column(dialect, "id")), FunctionCall(dialect, "SUM", Column(dialect, "amount")), ], - from_=TableExpression(dialect, "mv_sales"), + from_=Table(dialect, "mv_sales"), group_by_having=GroupByHavingClause( dialect=dialect, group_by=[Column(dialect, "product_id")] ), @@ -84,7 +84,7 @@ def _build(): Column(dialect, "sale_date"), FunctionCall(dialect, "SUM", Column(dialect, "amount")), ], - from_=TableExpression(dialect, "mv_sales"), + from_=Table(dialect, "mv_sales"), group_by_having=GroupByHavingClause( dialect=dialect, group_by=[Column(dialect, "sale_date")] ), @@ -129,10 +129,10 @@ async def async_mv_backend(async_postgres_backend_single): def create_summary_mv(mv_backend, summary_query): """Factory that creates a summary materialized view on the live server.""" - def _create(view_name="mv_sales_summary", query=None, **kwargs): + def _create(view=None, query=None, **kwargs): expression = PostgresCreateMaterializedViewExpression( dialect=mv_backend.dialect, - view_name=view_name, + view=view if view is not None else MaterializedView(mv_backend.dialect, "mv_sales_summary"), query=query if query is not None else summary_query(), **kwargs, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom.py index 49550b40..a2a96504 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom.py @@ -9,6 +9,7 @@ from rhosocial.activerecord.backend.expression import CreateIndexExpression from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) @@ -25,8 +26,8 @@ def test_format_bloom_index(self): """CreateIndexExpression with index_type bloom should render a bloom index.""" expr = CreateIndexExpression( self.dialect, - index_name="idx_name", - table_name="table_name", + index=Index(self.dialect, 'idx_name'), + table=Table(self.dialect, 'table_name'), columns=["col1", "col2"], index_type="bloom", ) @@ -40,8 +41,8 @@ def test_format_bloom_index_with_fill_factor(self): """CreateIndexExpression with fillfactor should include WITH (fillfactor = ...).""" expr = PostgresCreateIndexExpression( self.dialect, - index_name="idx_name", - table_name="table_name", + index=Index(self.dialect, 'idx_name'), + table=Table(self.dialect, 'table_name'), columns=["col1", "col2"], index_type="bloom", with_options={"fillfactor": 90}, 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 2badd88a..6ba020c0 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 @@ -24,10 +24,10 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, CreateIndexExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.options import ExecutionOptions @@ -54,7 +54,7 @@ def bloom_env(postgres_backend_single): # Clean up residual tables from previous runs for table in [TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -72,7 +72,7 @@ def bloom_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -88,7 +88,7 @@ def bloom_env(postgres_backend_single): ]) insert_expr = InsertExpression( dialect=dialect, - into=TABLE_NAME, + into=Table(dialect, TABLE_NAME), columns=["name", "email"], source=ValuesSource(dialect, rows), ) @@ -98,8 +98,8 @@ def bloom_env(postgres_backend_single): # Setup: create bloom index using expression create_idx = CreateIndexExpression( dialect=dialect, - index_name="idx_bloom_test", - table_name=TABLE_NAME, + index=Index(dialect, 'idx_bloom_test'), + table=Table(dialect, TABLE_NAME), columns=["name", "email"], index_type="bloom", if_not_exists=True, @@ -112,7 +112,7 @@ def bloom_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -131,7 +131,7 @@ def test_bloom_index_creation(self, bloom_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, TABLE_NAME), ) sql, params = query.to_sql() @@ -147,7 +147,7 @@ def test_bloom_index_query(self, bloom_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "email")], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), where=Column(dialect, "name") == Literal(dialect, "user25"), ) sql, params = query.to_sql() @@ -175,7 +175,7 @@ def test_bloom_index_with_options(self, bloom_env): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_bloom_opts", + table=Table(dialect, 'test_bloom_opts'), columns=columns, if_not_exists=True, ) @@ -188,7 +188,7 @@ def test_bloom_index_with_options(self, bloom_env): rows.append([Literal(dialect, f"CODE{i}")]) insert_expr = InsertExpression( dialect=dialect, - into="test_bloom_opts", + into=Table(dialect, 'test_bloom_opts'), columns=["code"], source=ValuesSource(dialect, rows), ) @@ -205,7 +205,7 @@ def test_bloom_index_with_options(self, bloom_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_bloom_opts"), ) sql, params = query.to_sql() @@ -215,7 +215,7 @@ def test_bloom_index_with_options(self, bloom_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_bloom_opts", + table=Table(dialect, 'test_bloom_opts'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -231,7 +231,7 @@ async def async_bloom_env(async_postgres_backend_single): # Clean up residual tables from previous runs for table in [ASYNC_TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) @@ -249,7 +249,7 @@ async def async_bloom_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -265,7 +265,7 @@ async def async_bloom_env(async_postgres_backend_single): ]) insert_expr = InsertExpression( dialect=dialect, - into=ASYNC_TABLE_NAME, + into=Table(dialect, ASYNC_TABLE_NAME), columns=["name", "email"], source=ValuesSource(dialect, rows), ) @@ -275,8 +275,8 @@ async def async_bloom_env(async_postgres_backend_single): # Setup: create bloom index using expression create_idx = CreateIndexExpression( dialect=dialect, - index_name="idx_bloom_test_async", - table_name=ASYNC_TABLE_NAME, + index=Index(dialect, 'idx_bloom_test_async'), + table=Table(dialect, ASYNC_TABLE_NAME), columns=["name", "email"], index_type="bloom", if_not_exists=True, @@ -289,7 +289,7 @@ async def async_bloom_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -308,7 +308,7 @@ async def test_async_bloom_index_creation(self, async_bloom_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, ASYNC_TABLE_NAME), ) sql, params = query.to_sql() @@ -325,7 +325,7 @@ async def test_async_bloom_index_query(self, async_bloom_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "email")], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), where=Column(dialect, "name") == Literal(dialect, "user25"), ) sql, params = query.to_sql() @@ -352,7 +352,7 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_bloom_opts_async", + table=Table(dialect, 'test_bloom_opts_async'), columns=columns, if_not_exists=True, ) @@ -364,7 +364,7 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): rows.append([Literal(dialect, f"CODE{i}")]) insert_expr = InsertExpression( dialect=dialect, - into="test_bloom_opts_async", + into=Table(dialect, 'test_bloom_opts_async'), columns=["code"], source=ValuesSource(dialect, rows), ) @@ -380,7 +380,7 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_bloom_opts_async"), ) sql, params = query.to_sql() @@ -390,7 +390,7 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_bloom_opts_async", + table=Table(dialect, 'test_bloom_opts_async'), if_exists=True, ) sql, params = drop_expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin.py index 993a73ad..72884fbd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin.py @@ -9,6 +9,7 @@ from rhosocial.activerecord.backend.expression import CreateIndexExpression from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Index, Table class TestPostgresBtreeGinMixin: @@ -22,8 +23,8 @@ def test_format_gin_index(self): """CreateIndexExpression with index_type gin should render a gin index.""" expr = CreateIndexExpression( self.dialect, - index_name="idx_name", - table_name="table_name", + index=Index(self.dialect, 'idx_name'), + table=Table(self.dialect, 'table_name'), columns=["column"], index_type="gin", ) 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 0466260f..d6c23f96 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 @@ -30,9 +30,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) @@ -73,7 +73,7 @@ def btree_gin_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_btree_gin", + table=Table(dialect, 'test_btree_gin'), columns=columns, if_not_exists=True, ) @@ -88,7 +88,7 @@ def btree_gin_env(postgres_backend_single): ] insert_expr = InsertExpression( dialect=dialect, - into="test_btree_gin", + into=Table(dialect, 'test_btree_gin'), columns=["name", "value", "tags"], source=ValuesSource(dialect, rows), ) @@ -100,7 +100,7 @@ def btree_gin_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_btree_gin", + table=Table(dialect, 'test_btree_gin'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -118,8 +118,8 @@ def test_btree_gin_int4_ops_index(self, btree_gin_env): # Create a GIN index with btree_gin operator class on INTEGER column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gin_value", - table_name="test_btree_gin", + index=Index(dialect, "idx_btree_gin_value"), + table=Table(dialect, "test_btree_gin"), columns=["value"], index_type="GIN", if_not_exists=True, @@ -132,7 +132,7 @@ def test_btree_gin_int4_ops_index(self, btree_gin_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gin"), ) sql, params = query.to_sql() @@ -149,8 +149,8 @@ def test_btree_gin_text_ops_index(self, btree_gin_env): # Create a GIN index with btree_gin operator class on TEXT column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gin_name", - table_name="test_btree_gin", + index=Index(dialect, "idx_btree_gin_name"), + table=Table(dialect, "test_btree_gin"), columns=["name"], index_type="GIN", if_not_exists=True, @@ -163,7 +163,7 @@ def test_btree_gin_text_ops_index(self, btree_gin_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gin"), ) sql, params = query.to_sql() @@ -180,8 +180,8 @@ def test_btree_gin_index_query(self, btree_gin_env): # Create a GIN index with btree_gin operator class on INTEGER column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gin_value", - table_name="test_btree_gin", + index=Index(dialect, "idx_btree_gin_value"), + table=Table(dialect, "test_btree_gin"), columns=["value"], index_type="GIN", if_not_exists=True, @@ -194,7 +194,7 @@ def test_btree_gin_index_query(self, btree_gin_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "value")], - from_=TableExpression(dialect, "test_btree_gin"), + from_=Table(dialect, "test_btree_gin"), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -228,7 +228,7 @@ async def async_btree_gin_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_btree_gin_async", + table=Table(dialect, 'test_btree_gin_async'), columns=columns, if_not_exists=True, ) @@ -243,7 +243,7 @@ async def async_btree_gin_env(async_postgres_backend_single): ] insert_expr = InsertExpression( dialect=dialect, - into="test_btree_gin_async", + into=Table(dialect, 'test_btree_gin_async'), columns=["name", "value", "tags"], source=ValuesSource(dialect, rows), ) @@ -255,7 +255,7 @@ async def async_btree_gin_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_btree_gin_async", + table=Table(dialect, 'test_btree_gin_async'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -274,8 +274,8 @@ async def test_async_btree_gin_int4_ops_index(self, async_btree_gin_env): # Create a GIN index with btree_gin operator class on INTEGER column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gin_value_async", - table_name="test_btree_gin_async", + index=Index(dialect, "idx_btree_gin_value_async"), + table=Table(dialect, "test_btree_gin_async"), columns=["value"], index_type="GIN", if_not_exists=True, @@ -288,7 +288,7 @@ async def test_async_btree_gin_int4_ops_index(self, async_btree_gin_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gin_async"), ) sql, params = query.to_sql() @@ -306,8 +306,8 @@ async def test_async_btree_gin_text_ops_index(self, async_btree_gin_env): # Create a GIN index with btree_gin operator class on TEXT column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gin_name_async", - table_name="test_btree_gin_async", + index=Index(dialect, "idx_btree_gin_name_async"), + table=Table(dialect, "test_btree_gin_async"), columns=["name"], index_type="GIN", if_not_exists=True, @@ -320,7 +320,7 @@ async def test_async_btree_gin_text_ops_index(self, async_btree_gin_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gin_async"), ) sql, params = query.to_sql() @@ -338,8 +338,8 @@ async def test_async_btree_gin_index_query(self, async_btree_gin_env): # Create a GIN index with btree_gin operator class on INTEGER column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gin_value_async", - table_name="test_btree_gin_async", + index=Index(dialect, "idx_btree_gin_value_async"), + table=Table(dialect, "test_btree_gin_async"), columns=["value"], index_type="GIN", if_not_exists=True, @@ -352,7 +352,7 @@ async def test_async_btree_gin_index_query(self, async_btree_gin_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "value")], - from_=TableExpression(dialect, "test_btree_gin_async"), + from_=Table(dialect, "test_btree_gin_async"), ) sql, params = query.to_sql() result = await backend.execute(sql, params, options=opts) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist.py index c0dfee06..d5131b09 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist.py @@ -9,6 +9,7 @@ from rhosocial.activerecord.backend.expression import CreateIndexExpression from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Index, Table class TestPostgresBtreeGistMixin: @@ -22,8 +23,8 @@ def test_format_gist_index(self): """CreateIndexExpression with index_type gist should render a gist index.""" expr = CreateIndexExpression( self.dialect, - index_name="idx_name", - table_name="table_name", + index=Index(self.dialect, 'idx_name'), + table=Table(self.dialect, 'table_name'), columns=["column"], index_type="gist", ) @@ -37,8 +38,8 @@ def test_format_gist_index_with_include(self): """CreateIndexExpression with include should include additional columns.""" expr = CreateIndexExpression( self.dialect, - index_name="idx_name", - table_name="table_name", + index=Index(self.dialect, 'idx_name'), + table=Table(self.dialect, 'table_name'), columns=["column"], index_type="gist", include=["extra_col"], 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 9a0fbe3a..216991e4 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 @@ -31,9 +31,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) @@ -75,7 +75,7 @@ def btree_gist_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_btree_gist", + table=Table(dialect, 'test_btree_gist'), columns=columns, if_not_exists=True, ) @@ -91,7 +91,7 @@ def btree_gist_env(postgres_backend_single): ] insert_expr = InsertExpression( dialect=dialect, - into="test_btree_gist", + into=Table(dialect, 'test_btree_gist'), columns=["name", "created_at", "value"], source=ValuesSource(dialect, rows), ) @@ -103,7 +103,7 @@ def btree_gist_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_btree_gist", + table=Table(dialect, 'test_btree_gist'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -121,8 +121,8 @@ def test_btree_gist_timestamp_index(self, btree_gist_env): # Create a GiST index with btree_gist operator class on TIMESTAMP column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gist_created_at", - table_name="test_btree_gist", + index=Index(dialect, "idx_btree_gist_created_at"), + table=Table(dialect, "test_btree_gist"), columns=["created_at"], index_type="GIST", if_not_exists=True, @@ -135,7 +135,7 @@ def test_btree_gist_timestamp_index(self, btree_gist_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gist"), ) sql, params = query.to_sql() @@ -152,8 +152,8 @@ def test_btree_gist_int4_index(self, btree_gist_env): # Create a GiST index with btree_gist operator class on INTEGER column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gist_value", - table_name="test_btree_gist", + index=Index(dialect, "idx_btree_gist_value"), + table=Table(dialect, "test_btree_gist"), columns=["value"], index_type="GIST", if_not_exists=True, @@ -166,7 +166,7 @@ def test_btree_gist_int4_index(self, btree_gist_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gist"), ) sql, params = query.to_sql() @@ -183,8 +183,8 @@ def test_btree_gist_range_query(self, btree_gist_env): # Create a GiST index with btree_gist operator class on TIMESTAMP column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gist_created_at", - table_name="test_btree_gist", + index=Index(dialect, "idx_btree_gist_created_at"), + table=Table(dialect, "test_btree_gist"), columns=["created_at"], index_type="GIST", if_not_exists=True, @@ -198,7 +198,7 @@ def test_btree_gist_range_query(self, btree_gist_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "created_at")], - from_=TableExpression(dialect, "test_btree_gist"), + from_=Table(dialect, "test_btree_gist"), where=ComparisonPredicate( dialect, ">", @@ -239,7 +239,7 @@ async def async_btree_gist_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_btree_gist_async", + table=Table(dialect, 'test_btree_gist_async'), columns=columns, if_not_exists=True, ) @@ -255,7 +255,7 @@ async def async_btree_gist_env(async_postgres_backend_single): ] insert_expr = InsertExpression( dialect=dialect, - into="test_btree_gist_async", + into=Table(dialect, 'test_btree_gist_async'), columns=["name", "created_at", "value"], source=ValuesSource(dialect, rows), ) @@ -267,7 +267,7 @@ async def async_btree_gist_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_btree_gist_async", + table=Table(dialect, 'test_btree_gist_async'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -286,8 +286,8 @@ async def test_async_btree_gist_timestamp_index(self, async_btree_gist_env): # Create a GiST index with btree_gist operator class on TIMESTAMP column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gist_created_at_async", - table_name="test_btree_gist_async", + index=Index(dialect, "idx_btree_gist_created_at_async"), + table=Table(dialect, "test_btree_gist_async"), columns=["created_at"], index_type="GIST", if_not_exists=True, @@ -300,7 +300,7 @@ async def test_async_btree_gist_timestamp_index(self, async_btree_gist_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gist_async"), ) sql, params = query.to_sql() @@ -318,8 +318,8 @@ async def test_async_btree_gist_int4_index(self, async_btree_gist_env): # Create a GiST index with btree_gist operator class on INTEGER column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gist_value_async", - table_name="test_btree_gist_async", + index=Index(dialect, "idx_btree_gist_value_async"), + table=Table(dialect, "test_btree_gist_async"), columns=["value"], index_type="GIST", if_not_exists=True, @@ -332,7 +332,7 @@ async def test_async_btree_gist_int4_index(self, async_btree_gist_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_btree_gist_async"), ) sql, params = query.to_sql() @@ -350,8 +350,8 @@ async def test_async_btree_gist_range_query(self, async_btree_gist_env): # Create a GiST index with btree_gist operator class on TIMESTAMP column create_idx = PostgresCreateIndexExpression( dialect=dialect, - index_name="idx_btree_gist_created_at_async", - table_name="test_btree_gist_async", + index=Index(dialect, "idx_btree_gist_created_at_async"), + table=Table(dialect, "test_btree_gist_async"), columns=["created_at"], index_type="GIST", if_not_exists=True, @@ -365,7 +365,7 @@ async def test_async_btree_gist_range_query(self, async_btree_gist_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), Column(dialect, "created_at")], - from_=TableExpression(dialect, "test_btree_gist_async"), + from_=Table(dialect, "test_btree_gist_async"), where=ComparisonPredicate( 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 ed508c4a..fbc33abb 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 @@ -29,9 +29,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -64,7 +64,7 @@ def _setup_citext_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -73,7 +73,7 @@ def _setup_citext_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource( dialect, @@ -104,7 +104,7 @@ def _setup_citext_unique_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -113,7 +113,7 @@ def _setup_citext_unique_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["email"], source=ValuesSource( dialect, @@ -138,7 +138,7 @@ def _setup_citext_like_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -147,7 +147,7 @@ def _setup_citext_like_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource( dialect, @@ -175,7 +175,7 @@ def _setup_citext_users_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -184,7 +184,7 @@ def _setup_citext_users_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["username"], source=ValuesSource( dialect, @@ -210,7 +210,7 @@ def _setup_citext_roles_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -219,7 +219,7 @@ def _setup_citext_roles_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["username", "role"], source=ValuesSource( dialect, @@ -234,7 +234,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -255,7 +255,7 @@ async def _async_setup_citext_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -264,7 +264,7 @@ async def _async_setup_citext_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource( dialect, @@ -295,7 +295,7 @@ async def _async_setup_citext_unique_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -304,7 +304,7 @@ async def _async_setup_citext_unique_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["email"], source=ValuesSource( dialect, @@ -329,7 +329,7 @@ async def _async_setup_citext_like_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -338,7 +338,7 @@ async def _async_setup_citext_like_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource( dialect, @@ -366,7 +366,7 @@ async def _async_setup_citext_users_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -375,7 +375,7 @@ async def _async_setup_citext_users_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["username"], source=ValuesSource( dialect, @@ -401,7 +401,7 @@ async def _async_setup_citext_roles_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -410,7 +410,7 @@ async def _async_setup_citext_roles_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["username", "role"], source=ValuesSource( dialect, @@ -425,7 +425,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -492,7 +492,7 @@ def test_citext_comparison(self, citext_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT), + from_=Table(dialect, T_CITEXT), where=where_pred, ) sql, params = query.to_sql() @@ -510,7 +510,7 @@ def test_citext_comparison(self, citext_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT), + from_=Table(dialect, T_CITEXT), where=where_pred2, ) sql2, params2 = query2.to_sql() @@ -530,7 +530,7 @@ def test_citext_comparison(self, citext_env): query3 = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT), + from_=Table(dialect, T_CITEXT), where=where_pred3, ) sql3, params3 = query3.to_sql() @@ -546,7 +546,7 @@ def test_citext_unique_constraint(self, citext_env): # Insert with different casing should violate unique constraint insert_expr = InsertExpression( dialect=dialect, - into=T_CITEXT_UNIQUE, + into=Table(dialect, T_CITEXT_UNIQUE), columns=["email"], source=ValuesSource( dialect, @@ -571,7 +571,7 @@ def test_citext_like_query(self, citext_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT_LIKE), + from_=Table(dialect, T_CITEXT_LIKE), where=where_pred, ) sql, params = query.to_sql() @@ -594,7 +594,7 @@ def test_citext_join(self, citext_env): query_user = QueryExpression( dialect=dialect, select=[Column(dialect, "username")], - from_=TableExpression(dialect, T_CITEXT_USERS), + from_=Table(dialect, T_CITEXT_USERS), ) sql, params = query_user.to_sql() user_result = backend.execute(sql, params, options=opts) @@ -611,7 +611,7 @@ def test_citext_join(self, citext_env): query_role = QueryExpression( dialect=dialect, select=[Column(dialect, "role")], - from_=TableExpression(dialect, T_CITEXT_ROLES), + from_=Table(dialect, T_CITEXT_ROLES), where=where_pred, ) sql2, params2 = query_role.to_sql() @@ -667,7 +667,7 @@ async def test_async_citext_comparison(self, async_citext_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT_ASYNC), + from_=Table(dialect, T_CITEXT_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -685,7 +685,7 @@ async def test_async_citext_comparison(self, async_citext_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT_ASYNC), + from_=Table(dialect, T_CITEXT_ASYNC), where=where_pred2, ) sql2, params2 = query2.to_sql() @@ -703,7 +703,7 @@ async def test_async_citext_comparison(self, async_citext_env): query3 = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT_ASYNC), + from_=Table(dialect, T_CITEXT_ASYNC), where=where_pred3, ) sql3, params3 = query3.to_sql() @@ -718,7 +718,7 @@ async def test_async_citext_unique_constraint(self, async_citext_env): insert_expr = InsertExpression( dialect=dialect, - into=T_CITEXT_UNIQUE_ASYNC, + into=Table(dialect, T_CITEXT_UNIQUE_ASYNC), columns=["email"], source=ValuesSource( dialect, @@ -743,7 +743,7 @@ async def test_async_citext_like_query(self, async_citext_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, T_CITEXT_LIKE_ASYNC), + from_=Table(dialect, T_CITEXT_LIKE_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -762,7 +762,7 @@ async def test_async_citext_join(self, async_citext_env): query_user = QueryExpression( dialect=dialect, select=[Column(dialect, "username")], - from_=TableExpression(dialect, T_CITEXT_USERS_ASYNC), + from_=Table(dialect, T_CITEXT_USERS_ASYNC), ) sql, params = query_user.to_sql() user_result = await backend.execute(sql, params, options=opts) @@ -779,7 +779,7 @@ async def test_async_citext_join(self, async_citext_env): query_role = QueryExpression( dialect=dialect, select=[Column(dialect, "role")], - from_=TableExpression(dialect, T_CITEXT_ROLES_ASYNC), + from_=Table(dialect, T_CITEXT_ROLES_ASYNC), where=where_pred, ) sql2, params2 = query_role.to_sql() 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 1998ebbb..4226cd99 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 @@ -26,11 +26,11 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, CreateIndexExpression, OrderByClause, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall, Subquery from rhosocial.activerecord.backend.expression import RawSQLExpression @@ -56,14 +56,14 @@ def cube_env(postgres_backend_single): # Clean up residual tables from previous runs for table in ["test_cubes", "test_cube_dist", "test_cube_idx"]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) # Setup: create test_cubes table create_cubes = CreateTableExpression( dialect=dialect, - table="test_cubes", + table=Table(dialect, 'test_cubes'), columns=[ ColumnDefinition( dialect, name="id", @@ -82,7 +82,7 @@ def cube_env(postgres_backend_single): # Insert data into test_cubes insert_cubes = InsertExpression( dialect=dialect, - into="test_cubes", + into=Table(dialect, 'test_cubes'), columns=["c"], source=ValuesSource( dialect, @@ -98,7 +98,7 @@ def cube_env(postgres_backend_single): # Setup: create test_cube_dist table create_dist = CreateTableExpression( dialect=dialect, - table="test_cube_dist", + table=Table(dialect, 'test_cube_dist'), columns=[ ColumnDefinition( dialect, name="id", @@ -117,7 +117,7 @@ def cube_env(postgres_backend_single): # Insert data into test_cube_dist insert_dist = InsertExpression( dialect=dialect, - into="test_cube_dist", + into=Table(dialect, 'test_cube_dist'), columns=["c"], source=ValuesSource( dialect, @@ -133,7 +133,7 @@ def cube_env(postgres_backend_single): # Setup: create test_cube_idx table create_idx_table = CreateTableExpression( dialect=dialect, - table="test_cube_idx", + table=Table(dialect, 'test_cube_idx'), columns=[ ColumnDefinition( dialect, name="id", @@ -152,7 +152,7 @@ def cube_env(postgres_backend_single): # Insert data into test_cube_idx insert_idx = InsertExpression( dialect=dialect, - into="test_cube_idx", + into=Table(dialect, 'test_cube_idx'), columns=["c"], source=ValuesSource( dialect, @@ -165,8 +165,8 @@ def cube_env(postgres_backend_single): # Create GiST index on test_cube_idx create_index = CreateIndexExpression( dialect=dialect, - index_name="idx_test_cube", - table_name="test_cube_idx", + index=Index(dialect, 'idx_test_cube'), + table=Table(dialect, 'test_cube_idx'), columns=["c"], index_type="GIST", if_not_exists=True, @@ -180,7 +180,7 @@ def cube_env(postgres_backend_single): for table in ["test_cubes", "test_cube_dist", "test_cube_idx"]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -198,7 +198,7 @@ def test_cube_type_crud(self, cube_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "c")], - from_=TableExpression(dialect, "test_cubes"), + from_=Table(dialect, "test_cubes"), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -320,7 +320,7 @@ def test_cube_index(self, cube_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_cube_idx"), ) sql, params = query.to_sql() @@ -342,14 +342,14 @@ async def async_cube_env(async_postgres_backend_single): # Clean up residual tables from previous runs for table in ["test_cubes_async", "test_cube_dist_async", "test_cube_idx_async"]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) # Setup: create test_cubes_async table create_cubes = CreateTableExpression( dialect=dialect, - table="test_cubes_async", + table=Table(dialect, 'test_cubes_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -368,7 +368,7 @@ async def async_cube_env(async_postgres_backend_single): # Insert data into test_cubes_async insert_cubes = InsertExpression( dialect=dialect, - into="test_cubes_async", + into=Table(dialect, 'test_cubes_async'), columns=["c"], source=ValuesSource( dialect, @@ -384,7 +384,7 @@ async def async_cube_env(async_postgres_backend_single): # Setup: create test_cube_dist_async table create_dist = CreateTableExpression( dialect=dialect, - table="test_cube_dist_async", + table=Table(dialect, 'test_cube_dist_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -403,7 +403,7 @@ async def async_cube_env(async_postgres_backend_single): # Insert data into test_cube_dist_async insert_dist = InsertExpression( dialect=dialect, - into="test_cube_dist_async", + into=Table(dialect, 'test_cube_dist_async'), columns=["c"], source=ValuesSource( dialect, @@ -419,7 +419,7 @@ async def async_cube_env(async_postgres_backend_single): # Setup: create test_cube_idx_async table create_idx_table = CreateTableExpression( dialect=dialect, - table="test_cube_idx_async", + table=Table(dialect, 'test_cube_idx_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -438,7 +438,7 @@ async def async_cube_env(async_postgres_backend_single): # Insert data into test_cube_idx_async insert_idx = InsertExpression( dialect=dialect, - into="test_cube_idx_async", + into=Table(dialect, 'test_cube_idx_async'), columns=["c"], source=ValuesSource( dialect, @@ -451,8 +451,8 @@ async def async_cube_env(async_postgres_backend_single): # Create GiST index on test_cube_idx_async create_index = CreateIndexExpression( dialect=dialect, - index_name="idx_test_cube_async", - table_name="test_cube_idx_async", + index=Index(dialect, 'idx_test_cube_async'), + table=Table(dialect, 'test_cube_idx_async'), columns=["c"], index_type="GIST", if_not_exists=True, @@ -466,7 +466,7 @@ async def async_cube_env(async_postgres_backend_single): for table in ["test_cubes_async", "test_cube_dist_async", "test_cube_idx_async"]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -485,7 +485,7 @@ async def test_async_cube_type_crud(self, async_cube_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "c")], - from_=TableExpression(dialect, "test_cubes_async"), + from_=Table(dialect, "test_cubes_async"), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -593,7 +593,7 @@ async def test_async_cube_index(self, async_cube_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_cube_idx_async"), ) sql, params = query.to_sql() 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 b6a0a2f4..b2cb9838 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 @@ -33,6 +33,7 @@ PostgresPointType, PostgresSerialType, ) +from rhosocial.activerecord.backend.expression.objects import Table TABLE_NAME = "test_earth" @@ -48,7 +49,7 @@ def earthdistance_env(postgres_backend_single): # Clean up residual tables from previous runs for table in [TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) @@ -65,7 +66,7 @@ def earthdistance_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -75,7 +76,7 @@ def earthdistance_env(postgres_backend_single): # Setup: insert test data using expression insert_expr = InsertExpression( dialect=dialect, - into=TABLE_NAME, + into=Table(dialect, TABLE_NAME), columns=["loc"], source=ValuesSource( dialect, @@ -93,7 +94,7 @@ def earthdistance_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -160,7 +161,7 @@ async def async_earthdistance_env(async_postgres_backend_single): # Clean up residual tables from previous runs for table in [ASYNC_TABLE_NAME]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) @@ -177,7 +178,7 @@ async def async_earthdistance_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -187,7 +188,7 @@ async def async_earthdistance_env(async_postgres_backend_single): # Setup: insert test data using expression insert_expr = InsertExpression( dialect=dialect, - into=ASYNC_TABLE_NAME, + into=Table(dialect, ASYNC_TABLE_NAME), columns=["loc"], source=ValuesSource( dialect, @@ -205,7 +206,7 @@ async def async_earthdistance_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() 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 e8add2f1..961ad740 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 @@ -28,9 +28,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -64,7 +64,7 @@ def _setup_lev_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -79,7 +79,7 @@ def _setup_lev_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["word"], source=ValuesSource(dialect, rows), ) @@ -101,7 +101,7 @@ def _setup_diff_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -115,7 +115,7 @@ def _setup_diff_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -127,7 +127,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -148,7 +148,7 @@ async def _async_setup_lev_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -163,7 +163,7 @@ async def _async_setup_lev_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["word"], source=ValuesSource(dialect, rows), ) @@ -185,7 +185,7 @@ async def _async_setup_diff_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -199,7 +199,7 @@ async def _async_setup_diff_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -211,7 +211,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -293,7 +293,7 @@ def test_levenshtein(self, fuzzystrmatch_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "word")], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), where=where_pred, ) sql, params = query.to_sql() @@ -382,7 +382,7 @@ def test_difference(self, fuzzystrmatch_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, TABLE_NAME_DIFF), + from_=Table(dialect, TABLE_NAME_DIFF), where=where_pred, ) sql, params = query.to_sql() @@ -538,7 +538,7 @@ async def test_async_levenshtein(self, async_fuzzystrmatch_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "word")], - from_=TableExpression(dialect, TABLE_NAME_ASYNC), + from_=Table(dialect, TABLE_NAME_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -629,7 +629,7 @@ async def test_async_difference(self, async_fuzzystrmatch_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, TABLE_NAME_DIFF_ASYNC), + from_=Table(dialect, TABLE_NAME_DIFF_ASYNC), where=where_pred, ) sql, params = query.to_sql() 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 2f7bf8fa..a7276855 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 @@ -28,10 +28,10 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, UpdateExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall, Subquery from rhosocial.activerecord.backend.expression.operators import BinaryExpression @@ -65,7 +65,7 @@ def _setup_hstore_table(backend, dialect, table, hstore_value): """Create and populate an hstore test table using expressions.""" create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=_make_hstore_columns(dialect), if_not_exists=True, ) @@ -74,7 +74,7 @@ def _setup_hstore_table(backend, dialect, table, hstore_value): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["data"], source=ValuesSource( dialect, @@ -89,7 +89,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -100,7 +100,7 @@ async def _async_setup_hstore_table(backend, dialect, table, hstore_value): """Async: create and populate an hstore test table using expressions.""" create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=_make_hstore_columns(dialect), if_not_exists=True, ) @@ -109,7 +109,7 @@ async def _async_setup_hstore_table(backend, dialect, table, hstore_value): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["data"], source=ValuesSource( dialect, @@ -124,7 +124,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -197,7 +197,7 @@ def test_hstore_create_and_query(self, hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "data")], - from_=TableExpression(dialect, T_HSTORE), + from_=Table(dialect, T_HSTORE), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -219,7 +219,7 @@ def test_hstore_create_and_query(self, hstore_env): query_a = QueryExpression( dialect=dialect, select=[val_a_expr], - from_=TableExpression(dialect, T_HSTORE), + from_=Table(dialect, T_HSTORE), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -240,7 +240,7 @@ def test_hstore_create_and_query(self, hstore_env): query_b = QueryExpression( dialect=dialect, select=[val_b_expr], - from_=TableExpression(dialect, T_HSTORE), + from_=Table(dialect, T_HSTORE), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -266,7 +266,7 @@ def test_hstore_operators_contains(self, hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr], - from_=TableExpression(dialect, T_HSTORE_OPS), + from_=Table(dialect, T_HSTORE_OPS), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -287,7 +287,7 @@ def test_hstore_operators_contains(self, hstore_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr2], - from_=TableExpression(dialect, T_HSTORE_OPS), + from_=Table(dialect, T_HSTORE_OPS), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -313,7 +313,7 @@ def test_hstore_operators_key_exists(self, hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), has_name_expr], - from_=TableExpression(dialect, T_HSTORE_KEY), + from_=Table(dialect, T_HSTORE_KEY), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -334,7 +334,7 @@ def test_hstore_operators_key_exists(self, hstore_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), has_email_expr], - from_=TableExpression(dialect, T_HSTORE_KEY), + from_=Table(dialect, T_HSTORE_KEY), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -360,7 +360,7 @@ def test_hstore_operators_concat(self, hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), merged_expr], - from_=TableExpression(dialect, T_HSTORE_CONCAT), + from_=Table(dialect, T_HSTORE_CONCAT), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -385,7 +385,7 @@ def test_hstore_functions_each(self, hstore_env): query = QueryExpression( dialect=dialect, select=[each_func], - from_=TableExpression(dialect, T_HSTORE_EACH), + from_=Table(dialect, T_HSTORE_EACH), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -410,7 +410,7 @@ def test_hstore_functions_akeys_avals(self, hstore_env): query = QueryExpression( dialect=dialect, select=[akeys_func], - from_=TableExpression(dialect, T_HSTORE_KEYS_VALS), + from_=Table(dialect, T_HSTORE_KEYS_VALS), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -431,7 +431,7 @@ def test_hstore_functions_akeys_avals(self, hstore_env): query2 = QueryExpression( dialect=dialect, select=[avals_func], - from_=TableExpression(dialect, T_HSTORE_KEYS_VALS), + from_=Table(dialect, T_HSTORE_KEYS_VALS), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -454,7 +454,7 @@ def test_hstore_update_key_value(self, hstore_env): # SET data = data || 'status=>inactive'::hstore update_expr = UpdateExpression( dialect=dialect, - table=T_HSTORE_UPDATE, + table=Table(dialect, T_HSTORE_UPDATE), assignments={ "data": BinaryExpression( dialect, "||", @@ -480,7 +480,7 @@ def test_hstore_update_key_value(self, hstore_env): query = QueryExpression( dialect=dialect, select=[status_expr], - from_=TableExpression(dialect, T_HSTORE_UPDATE), + from_=Table(dialect, T_HSTORE_UPDATE), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -502,7 +502,7 @@ def test_hstore_delete_key(self, hstore_env): # SET data = delete(data, 'remove') update_expr = UpdateExpression( dialect=dialect, - table=T_HSTORE_DELETE, + table=Table(dialect, T_HSTORE_DELETE), assignments={ "data": FunctionCall( dialect, "delete", @@ -533,7 +533,7 @@ def test_hstore_delete_key(self, hstore_env): query = QueryExpression( dialect=dialect, select=[has_remove_expr, has_keep_expr], - from_=TableExpression(dialect, T_HSTORE_DELETE), + from_=Table(dialect, T_HSTORE_DELETE), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -595,7 +595,7 @@ async def test_async_hstore_create_and_query(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "data")], - from_=TableExpression(dialect, T_HSTORE_ASYNC), + from_=Table(dialect, T_HSTORE_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -616,7 +616,7 @@ async def test_async_hstore_create_and_query(self, async_hstore_env): query_a = QueryExpression( dialect=dialect, select=[val_a_expr], - from_=TableExpression(dialect, T_HSTORE_ASYNC), + from_=Table(dialect, T_HSTORE_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -637,7 +637,7 @@ async def test_async_hstore_create_and_query(self, async_hstore_env): query_b = QueryExpression( dialect=dialect, select=[val_b_expr], - from_=TableExpression(dialect, T_HSTORE_ASYNC), + from_=Table(dialect, T_HSTORE_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -664,7 +664,7 @@ async def test_async_hstore_operators_contains(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr], - from_=TableExpression(dialect, T_HSTORE_OPS_ASYNC), + from_=Table(dialect, T_HSTORE_OPS_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -685,7 +685,7 @@ async def test_async_hstore_operators_contains(self, async_hstore_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr2], - from_=TableExpression(dialect, T_HSTORE_OPS_ASYNC), + from_=Table(dialect, T_HSTORE_OPS_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -712,7 +712,7 @@ async def test_async_hstore_operators_key_exists(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), has_name_expr], - from_=TableExpression(dialect, T_HSTORE_KEY_ASYNC), + from_=Table(dialect, T_HSTORE_KEY_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -733,7 +733,7 @@ async def test_async_hstore_operators_key_exists(self, async_hstore_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), has_email_expr], - from_=TableExpression(dialect, T_HSTORE_KEY_ASYNC), + from_=Table(dialect, T_HSTORE_KEY_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -760,7 +760,7 @@ async def test_async_hstore_operators_concat(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), merged_expr], - from_=TableExpression(dialect, T_HSTORE_CONCAT_ASYNC), + from_=Table(dialect, T_HSTORE_CONCAT_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -786,7 +786,7 @@ async def test_async_hstore_functions_each(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[each_func], - from_=TableExpression(dialect, T_HSTORE_EACH_ASYNC), + from_=Table(dialect, T_HSTORE_EACH_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -812,7 +812,7 @@ async def test_async_hstore_functions_akeys_avals(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[akeys_func], - from_=TableExpression(dialect, T_HSTORE_KEYS_VALS_ASYNC), + from_=Table(dialect, T_HSTORE_KEYS_VALS_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -833,7 +833,7 @@ async def test_async_hstore_functions_akeys_avals(self, async_hstore_env): query2 = QueryExpression( dialect=dialect, select=[avals_func], - from_=TableExpression(dialect, T_HSTORE_KEYS_VALS_ASYNC), + from_=Table(dialect, T_HSTORE_KEYS_VALS_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -857,7 +857,7 @@ async def test_async_hstore_update_key_value(self, async_hstore_env): # SET data = data || 'status=>inactive'::hstore update_expr = UpdateExpression( dialect=dialect, - table=T_HSTORE_UPDATE_ASYNC, + table=Table(dialect, T_HSTORE_UPDATE_ASYNC), assignments={ "data": BinaryExpression( dialect, "||", @@ -883,7 +883,7 @@ async def test_async_hstore_update_key_value(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[status_expr], - from_=TableExpression(dialect, T_HSTORE_UPDATE_ASYNC), + from_=Table(dialect, T_HSTORE_UPDATE_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -906,7 +906,7 @@ async def test_async_hstore_delete_key(self, async_hstore_env): # SET data = delete(data, 'remove') update_expr = UpdateExpression( dialect=dialect, - table=T_HSTORE_DELETE_ASYNC, + table=Table(dialect, T_HSTORE_DELETE_ASYNC), assignments={ "data": FunctionCall( dialect, "delete", @@ -937,7 +937,7 @@ async def test_async_hstore_delete_key(self, async_hstore_env): query = QueryExpression( dialect=dialect, select=[has_remove_expr, has_keep_expr], - from_=TableExpression(dialect, T_HSTORE_DELETE_ASYNC), + from_=Table(dialect, T_HSTORE_DELETE_ASYNC), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), 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 badb7f92..cca12723 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 @@ -10,6 +10,7 @@ import pytest import pytest_asyncio +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord_postgres_test.feature.backend.utils import ( ensure_extension_installed, async_ensure_extension_installed, @@ -62,7 +63,7 @@ def hypopg_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_hypopg_items", + table=Table(dialect, 'test_hypopg_items'), columns=columns, if_not_exists=True, ) @@ -80,7 +81,7 @@ def hypopg_env(postgres_backend_single): ) insert_expr = InsertExpression( dialect=dialect, - into="test_hypopg_items", + into=Table(dialect, 'test_hypopg_items'), columns=["name", "email"], source=ValuesSource(dialect, rows), ) @@ -102,7 +103,7 @@ def hypopg_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_hypopg_items", + table=Table(dialect, 'test_hypopg_items'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -278,7 +279,7 @@ async def async_hypopg_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_hypopg_items_async", + table=Table(dialect, 'test_hypopg_items_async'), columns=columns, if_not_exists=True, ) @@ -296,7 +297,7 @@ async def async_hypopg_env(async_postgres_backend_single): ) insert_expr = InsertExpression( dialect=dialect, - into="test_hypopg_items_async", + into=Table(dialect, 'test_hypopg_items_async'), columns=["name", "email"], source=ValuesSource(dialect, rows), ) @@ -318,7 +319,7 @@ async def async_hypopg_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_hypopg_items_async", + table=Table(dialect, 'test_hypopg_items_async'), if_exists=True, ) sql, params = drop_expr.to_sql() 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 fef4b978..789014c5 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 @@ -30,10 +30,10 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, OrderByClause, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall, Subquery from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -65,7 +65,7 @@ def _setup_intarray_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -74,7 +74,7 @@ def _setup_intarray_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -103,7 +103,7 @@ def _setup_intarray_overlap_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -112,7 +112,7 @@ def _setup_intarray_overlap_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -141,7 +141,7 @@ def _setup_intarray_contained_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -150,7 +150,7 @@ def _setup_intarray_contained_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -179,7 +179,7 @@ def _setup_intarray_idx_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -188,7 +188,7 @@ def _setup_intarray_idx_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -217,7 +217,7 @@ def _setup_intarray_gin_table(backend, dialect, table, index_name): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -233,7 +233,7 @@ def _setup_intarray_gin_table(backend, dialect, table, index_name): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -248,7 +248,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -269,7 +269,7 @@ async def _async_setup_intarray_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -278,7 +278,7 @@ async def _async_setup_intarray_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -307,7 +307,7 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -316,7 +316,7 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -345,7 +345,7 @@ async def _async_setup_intarray_contained_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -354,7 +354,7 @@ async def _async_setup_intarray_contained_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -383,7 +383,7 @@ async def _async_setup_intarray_idx_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -392,7 +392,7 @@ async def _async_setup_intarray_idx_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -421,7 +421,7 @@ async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -436,7 +436,7 @@ async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["tags"], source=ValuesSource( dialect, @@ -451,7 +451,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -523,7 +523,7 @@ def test_intarray_contains_operator(self, intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr], - from_=TableExpression(dialect, T_INTARRAY), + from_=Table(dialect, T_INTARRAY), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -552,7 +552,7 @@ def test_intarray_overlap_operator(self, intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), overlap_expr], - from_=TableExpression(dialect, T_INTARRAY_OVERLAP), + from_=Table(dialect, T_INTARRAY_OVERLAP), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -582,7 +582,7 @@ def test_intarray_contained_by_operator(self, intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contained_expr], - from_=TableExpression(dialect, T_INTARRAY_CONTAINED), + from_=Table(dialect, T_INTARRAY_CONTAINED), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -614,7 +614,7 @@ def test_intarray_idx_function(self, intarray_env): query = QueryExpression( dialect=dialect, select=[idx_func], - from_=TableExpression(dialect, T_INTARRAY_IDX), + from_=Table(dialect, T_INTARRAY_IDX), where=where_pred, ) sql, params = query.to_sql() @@ -631,7 +631,7 @@ def test_intarray_idx_function(self, intarray_env): query2 = QueryExpression( dialect=dialect, select=[idx_func2], - from_=TableExpression(dialect, T_INTARRAY_IDX), + from_=Table(dialect, T_INTARRAY_IDX), where=where_pred, ) sql2, params2 = query2.to_sql() @@ -648,7 +648,7 @@ def test_intarray_gin_index(self, intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=ComparisonPredicate( dialect, "=", Column(dialect, "tablename"), @@ -673,7 +673,7 @@ def test_intarray_gin_index(self, intarray_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr], - from_=TableExpression(dialect, T_INTARRAY_GIN), + from_=Table(dialect, T_INTARRAY_GIN), ) sql2, params2 = query2.to_sql() result2 = backend.execute(sql2, params2, options=opts) @@ -732,7 +732,7 @@ async def test_async_intarray_contains_operator(self, async_intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr], - from_=TableExpression(dialect, T_INTARRAY_ASYNC), + from_=Table(dialect, T_INTARRAY_ASYNC), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -761,7 +761,7 @@ async def test_async_intarray_overlap_operator(self, async_intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), overlap_expr], - from_=TableExpression(dialect, T_INTARRAY_OVERLAP_ASYNC), + from_=Table(dialect, T_INTARRAY_OVERLAP_ASYNC), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -790,7 +790,7 @@ async def test_async_intarray_contained_by_operator(self, async_intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contained_expr], - from_=TableExpression(dialect, T_INTARRAY_CONTAINED_ASYNC), + from_=Table(dialect, T_INTARRAY_CONTAINED_ASYNC), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -822,7 +822,7 @@ async def test_async_intarray_idx_function(self, async_intarray_env): query = QueryExpression( dialect=dialect, select=[idx_func], - from_=TableExpression(dialect, T_INTARRAY_IDX_ASYNC), + from_=Table(dialect, T_INTARRAY_IDX_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -839,7 +839,7 @@ async def test_async_intarray_idx_function(self, async_intarray_env): query2 = QueryExpression( dialect=dialect, select=[idx_func2], - from_=TableExpression(dialect, T_INTARRAY_IDX_ASYNC), + from_=Table(dialect, T_INTARRAY_IDX_ASYNC), where=where_pred, ) sql2, params2 = query2.to_sql() @@ -857,7 +857,7 @@ async def test_async_intarray_gin_index(self, async_intarray_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=ComparisonPredicate( dialect, "=", Column(dialect, "tablename"), @@ -882,7 +882,7 @@ async def test_async_intarray_gin_index(self, async_intarray_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), contains_expr], - from_=TableExpression(dialect, T_INTARRAY_GIN_ASYNC), + from_=Table(dialect, T_INTARRAY_GIN_ASYNC), ) sql2, params2 = query2.to_sql() result2 = await backend.execute(sql2, params2, options=opts) 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 0f6d4998..accf1f3a 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 @@ -26,12 +26,12 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, CreateIndexExpression, OrderByClause, RawSQLExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -59,14 +59,14 @@ def ltree_env(postgres_backend_single): # Clean up residual tables from previous runs for table in ["test_ltree", "test_ltree_tree", "test_ltree_func", "test_ltree_idx", "test_ltree_txtq"]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) # Setup: create test_ltree table create_ltree = CreateTableExpression( dialect=dialect, - table="test_ltree", + table=Table(dialect, 'test_ltree'), columns=[ ColumnDefinition( dialect, name="id", @@ -85,7 +85,7 @@ def ltree_env(postgres_backend_single): # Insert data into test_ltree insert_ltree = InsertExpression( dialect=dialect, - into="test_ltree", + into=Table(dialect, 'test_ltree'), columns=["path"], source=ValuesSource( dialect, @@ -104,7 +104,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_tree table create_tree = CreateTableExpression( dialect=dialect, - table="test_ltree_tree", + table=Table(dialect, 'test_ltree_tree'), columns=[ ColumnDefinition( dialect, name="id", @@ -124,7 +124,7 @@ def ltree_env(postgres_backend_single): # Insert data into test_ltree_tree insert_tree = InsertExpression( dialect=dialect, - into="test_ltree_tree", + into=Table(dialect, 'test_ltree_tree'), columns=["path", "label"], source=ValuesSource( dialect, @@ -143,7 +143,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_func table create_func = CreateTableExpression( dialect=dialect, - table="test_ltree_func", + table=Table(dialect, 'test_ltree_func'), columns=[ ColumnDefinition( dialect, name="id", @@ -162,7 +162,7 @@ def ltree_env(postgres_backend_single): # Insert data into test_ltree_func insert_func = InsertExpression( dialect=dialect, - into="test_ltree_func", + into=Table(dialect, 'test_ltree_func'), columns=["path"], source=ValuesSource( dialect, @@ -175,7 +175,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_idx table create_idx_table = CreateTableExpression( dialect=dialect, - table="test_ltree_idx", + table=Table(dialect, 'test_ltree_idx'), columns=[ ColumnDefinition( dialect, name="id", @@ -194,7 +194,7 @@ def ltree_env(postgres_backend_single): # Insert data into test_ltree_idx insert_idx = InsertExpression( dialect=dialect, - into="test_ltree_idx", + into=Table(dialect, 'test_ltree_idx'), columns=["path"], source=ValuesSource( dialect, @@ -211,8 +211,8 @@ def ltree_env(postgres_backend_single): # Create GiST index on test_ltree_idx create_index = CreateIndexExpression( dialect=dialect, - index_name="idx_ltree_path", - table_name="test_ltree_idx", + index=Index(dialect, 'idx_ltree_path'), + table=Table(dialect, 'test_ltree_idx'), columns=["path"], index_type="GIST", if_not_exists=True, @@ -223,7 +223,7 @@ def ltree_env(postgres_backend_single): # Setup: create test_ltree_txtq table create_txtq = CreateTableExpression( dialect=dialect, - table="test_ltree_txtq", + table=Table(dialect, 'test_ltree_txtq'), columns=[ ColumnDefinition( dialect, name="id", @@ -242,7 +242,7 @@ def ltree_env(postgres_backend_single): # Insert data into test_ltree_txtq insert_txtq = InsertExpression( dialect=dialect, - into="test_ltree_txtq", + into=Table(dialect, 'test_ltree_txtq'), columns=["path"], source=ValuesSource( dialect, @@ -268,7 +268,7 @@ def ltree_env(postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -291,7 +291,7 @@ def test_ltree_path_operations(self, ltree_env): query = QueryExpression( dialect=dialect, select=[ancestor_expr, Column(dialect, "path")], - from_=TableExpression(dialect, "test_ltree"), + from_=Table(dialect, "test_ltree"), where=ComparisonPredicate( dialect, "=", Column(dialect, "path"), @@ -311,7 +311,7 @@ def test_ltree_path_operations(self, ltree_env): query2 = QueryExpression( dialect=dialect, select=[not_ancestor_expr, Column(dialect, "path")], - from_=TableExpression(dialect, "test_ltree"), + from_=Table(dialect, "test_ltree"), where=ComparisonPredicate( dialect, "=", Column(dialect, "path"), @@ -331,7 +331,7 @@ def test_ltree_path_operations(self, ltree_env): query3 = QueryExpression( dialect=dialect, select=[descendant_expr, Column(dialect, "path")], - from_=TableExpression(dialect, "test_ltree"), + from_=Table(dialect, "test_ltree"), where=ComparisonPredicate( dialect, "=", Column(dialect, "path"), @@ -352,7 +352,7 @@ def test_ltree_path_operations(self, ltree_env): query4 = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), match_expr], - from_=TableExpression(dialect, "test_ltree"), + from_=Table(dialect, "test_ltree"), order_by=OrderByClause(dialect, [Column(dialect, "path")]), ) sql4, params4 = query4.to_sql() @@ -378,7 +378,7 @@ def test_ltree_ancestor_descendant(self, ltree_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), Column(dialect, "label"), ancestor_expr], - from_=TableExpression(dialect, "test_ltree_tree"), + from_=Table(dialect, "test_ltree_tree"), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -398,7 +398,7 @@ def test_ltree_ancestor_descendant(self, ltree_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), Column(dialect, "label"), descendant_expr], - from_=TableExpression(dialect, "test_ltree_tree"), + from_=Table(dialect, "test_ltree_tree"), order_by=OrderByClause(dialect, [Column(dialect, "path")]), ) sql2, params2 = query2.to_sql() @@ -423,7 +423,7 @@ def test_ltree_nlevel_subpath(self, ltree_env): query = QueryExpression( dialect=dialect, select=[nlevel_func], - from_=TableExpression(dialect, "test_ltree_func"), + from_=Table(dialect, "test_ltree_func"), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -445,7 +445,7 @@ def test_ltree_nlevel_subpath(self, ltree_env): query2 = QueryExpression( dialect=dialect, select=[subpath_func], - from_=TableExpression(dialect, "test_ltree_func"), + from_=Table(dialect, "test_ltree_func"), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -467,7 +467,7 @@ def test_ltree_nlevel_subpath(self, ltree_env): query3 = QueryExpression( dialect=dialect, select=[subpath_func2], - from_=TableExpression(dialect, "test_ltree_func"), + from_=Table(dialect, "test_ltree_func"), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -490,7 +490,7 @@ def test_ltree_gist_index(self, ltree_env): query = QueryExpression( dialect=dialect, select=[RawSQLExpression(dialect, "COUNT(*) AS cnt")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=ComparisonPredicate( dialect, "AND", ComparisonPredicate( @@ -518,7 +518,7 @@ def test_ltree_gist_index(self, ltree_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), descendant_expr], - from_=TableExpression(dialect, "test_ltree_idx"), + from_=Table(dialect, "test_ltree_idx"), order_by=OrderByClause(dialect, [Column(dialect, "path")]), ) sql2, params2 = query2.to_sql() @@ -540,7 +540,7 @@ def test_ltree_ltxtquery_search(self, ltree_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), match_expr], - from_=TableExpression(dialect, "test_ltree_txtq"), + from_=Table(dialect, "test_ltree_txtq"), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -565,14 +565,14 @@ async def async_ltree_env(async_postgres_backend_single): for table in ["test_ltree_async", "test_ltree_tree_async", "test_ltree_func_async", "test_ltree_idx_async", "test_ltree_txtq_async"]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) # Setup: create test_ltree_async table create_ltree = CreateTableExpression( dialect=dialect, - table="test_ltree_async", + table=Table(dialect, 'test_ltree_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -591,7 +591,7 @@ async def async_ltree_env(async_postgres_backend_single): # Insert data into test_ltree_async insert_ltree = InsertExpression( dialect=dialect, - into="test_ltree_async", + into=Table(dialect, 'test_ltree_async'), columns=["path"], source=ValuesSource( dialect, @@ -610,7 +610,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_tree_async table create_tree = CreateTableExpression( dialect=dialect, - table="test_ltree_tree_async", + table=Table(dialect, 'test_ltree_tree_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -630,7 +630,7 @@ async def async_ltree_env(async_postgres_backend_single): # Insert data into test_ltree_tree_async insert_tree = InsertExpression( dialect=dialect, - into="test_ltree_tree_async", + into=Table(dialect, 'test_ltree_tree_async'), columns=["path", "label"], source=ValuesSource( dialect, @@ -649,7 +649,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_func_async table create_func = CreateTableExpression( dialect=dialect, - table="test_ltree_func_async", + table=Table(dialect, 'test_ltree_func_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -668,7 +668,7 @@ async def async_ltree_env(async_postgres_backend_single): # Insert data into test_ltree_func_async insert_func = InsertExpression( dialect=dialect, - into="test_ltree_func_async", + into=Table(dialect, 'test_ltree_func_async'), columns=["path"], source=ValuesSource( dialect, @@ -681,7 +681,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_idx_async table create_idx_table = CreateTableExpression( dialect=dialect, - table="test_ltree_idx_async", + table=Table(dialect, 'test_ltree_idx_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -700,7 +700,7 @@ async def async_ltree_env(async_postgres_backend_single): # Insert data into test_ltree_idx_async insert_idx = InsertExpression( dialect=dialect, - into="test_ltree_idx_async", + into=Table(dialect, 'test_ltree_idx_async'), columns=["path"], source=ValuesSource( dialect, @@ -717,8 +717,8 @@ async def async_ltree_env(async_postgres_backend_single): # Create GiST index on test_ltree_idx_async create_index = CreateIndexExpression( dialect=dialect, - index_name="idx_ltree_path_async", - table_name="test_ltree_idx_async", + index=Index(dialect, 'idx_ltree_path_async'), + table=Table(dialect, 'test_ltree_idx_async'), columns=["path"], index_type="GIST", if_not_exists=True, @@ -729,7 +729,7 @@ async def async_ltree_env(async_postgres_backend_single): # Setup: create test_ltree_txtq_async table create_txtq = CreateTableExpression( dialect=dialect, - table="test_ltree_txtq_async", + table=Table(dialect, 'test_ltree_txtq_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -748,7 +748,7 @@ async def async_ltree_env(async_postgres_backend_single): # Insert data into test_ltree_txtq_async insert_txtq = InsertExpression( dialect=dialect, - into="test_ltree_txtq_async", + into=Table(dialect, 'test_ltree_txtq_async'), columns=["path"], source=ValuesSource( dialect, @@ -774,7 +774,7 @@ async def async_ltree_env(async_postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -798,7 +798,7 @@ async def test_async_ltree_path_operations(self, async_ltree_env): query = QueryExpression( dialect=dialect, select=[ancestor_expr, Column(dialect, "path")], - from_=TableExpression(dialect, "test_ltree_async"), + from_=Table(dialect, "test_ltree_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "path"), @@ -818,7 +818,7 @@ async def test_async_ltree_path_operations(self, async_ltree_env): query2 = QueryExpression( dialect=dialect, select=[not_ancestor_expr, Column(dialect, "path")], - from_=TableExpression(dialect, "test_ltree_async"), + from_=Table(dialect, "test_ltree_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "path"), @@ -838,7 +838,7 @@ async def test_async_ltree_path_operations(self, async_ltree_env): query3 = QueryExpression( dialect=dialect, select=[descendant_expr, Column(dialect, "path")], - from_=TableExpression(dialect, "test_ltree_async"), + from_=Table(dialect, "test_ltree_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "path"), @@ -858,7 +858,7 @@ async def test_async_ltree_path_operations(self, async_ltree_env): query4 = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), match_expr], - from_=TableExpression(dialect, "test_ltree_async"), + from_=Table(dialect, "test_ltree_async"), order_by=OrderByClause(dialect, [Column(dialect, "path")]), ) sql4, params4 = query4.to_sql() @@ -884,7 +884,7 @@ async def test_async_ltree_ancestor_descendant(self, async_ltree_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), Column(dialect, "label"), ancestor_expr], - from_=TableExpression(dialect, "test_ltree_tree_async"), + from_=Table(dialect, "test_ltree_tree_async"), ) sql, params = query.to_sql() result = await backend.execute(sql, params, options=opts) @@ -904,7 +904,7 @@ async def test_async_ltree_ancestor_descendant(self, async_ltree_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), Column(dialect, "label"), descendant_expr], - from_=TableExpression(dialect, "test_ltree_tree_async"), + from_=Table(dialect, "test_ltree_tree_async"), order_by=OrderByClause(dialect, [Column(dialect, "path")]), ) sql2, params2 = query2.to_sql() @@ -930,7 +930,7 @@ async def test_async_ltree_nlevel_subpath(self, async_ltree_env): query = QueryExpression( dialect=dialect, select=[nlevel_func], - from_=TableExpression(dialect, "test_ltree_func_async"), + from_=Table(dialect, "test_ltree_func_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -952,7 +952,7 @@ async def test_async_ltree_nlevel_subpath(self, async_ltree_env): query2 = QueryExpression( dialect=dialect, select=[subpath_func], - from_=TableExpression(dialect, "test_ltree_func_async"), + from_=Table(dialect, "test_ltree_func_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -974,7 +974,7 @@ async def test_async_ltree_nlevel_subpath(self, async_ltree_env): query3 = QueryExpression( dialect=dialect, select=[subpath_func2], - from_=TableExpression(dialect, "test_ltree_func_async"), + from_=Table(dialect, "test_ltree_func_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "id"), @@ -998,7 +998,7 @@ async def test_async_ltree_gist_index(self, async_ltree_env): query = QueryExpression( dialect=dialect, select=[RawSQLExpression(dialect, "COUNT(*) AS cnt")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=ComparisonPredicate( dialect, "AND", ComparisonPredicate( @@ -1026,7 +1026,7 @@ async def test_async_ltree_gist_index(self, async_ltree_env): query2 = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), descendant_expr], - from_=TableExpression(dialect, "test_ltree_idx_async"), + from_=Table(dialect, "test_ltree_idx_async"), order_by=OrderByClause(dialect, [Column(dialect, "path")]), ) sql2, params2 = query2.to_sql() @@ -1049,7 +1049,7 @@ async def test_async_ltree_ltxtquery_search(self, async_ltree_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "path"), match_expr], - from_=TableExpression(dialect, "test_ltree_txtq_async"), + from_=Table(dialect, "test_ltree_txtq_async"), ) sql, params = query.to_sql() result = await backend.execute(sql, params, options=opts) 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 1d7476f5..6c59ab7e 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 @@ -26,9 +26,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.query_parts import OrderByClause @@ -76,7 +76,7 @@ def orafce_backend(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -86,7 +86,7 @@ def orafce_backend(postgres_backend_single): # Insert seed data with NULL values for testing nvl insert_expr = InsertExpression( dialect=dialect, - into=TABLE_NAME, + into=Table(dialect, TABLE_NAME), columns=["dt", "ref_dt", "name", "discount", "description"], source=ValuesSource( dialect, @@ -123,7 +123,7 @@ def orafce_backend(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -148,7 +148,7 @@ def test_add_months(self, orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -176,7 +176,7 @@ def test_last_day(self, orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -206,7 +206,7 @@ def test_months_between(self, orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -236,7 +236,7 @@ def test_nvl(self, orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -267,7 +267,7 @@ def test_decode(self, orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -296,7 +296,7 @@ def test_instr(self, orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -337,7 +337,7 @@ async def async_orafce_backend(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -347,7 +347,7 @@ async def async_orafce_backend(async_postgres_backend_single): # Insert seed data with NULL values for testing nvl insert_expr = InsertExpression( dialect=dialect, - into=ASYNC_TABLE_NAME, + into=Table(dialect, ASYNC_TABLE_NAME), columns=["dt", "ref_dt", "name", "discount", "description"], source=ValuesSource( dialect, @@ -384,7 +384,7 @@ async def async_orafce_backend(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -410,7 +410,7 @@ async def test_async_add_months(self, async_orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -439,7 +439,7 @@ async def test_async_last_day(self, async_orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -470,7 +470,7 @@ async def test_async_months_between(self, async_orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -501,7 +501,7 @@ async def test_async_nvl(self, async_orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -533,7 +533,7 @@ async def test_async_decode(self, async_orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -563,7 +563,7 @@ async def test_async_instr(self, async_orafce_backend): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), func], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_stat_statements_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_stat_statements_integration.py index f38178e1..7e2e65fb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_stat_statements_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_stat_statements_integration.py @@ -21,9 +21,9 @@ ) from rhosocial.activerecord.backend.expression import ( QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.functions.pg_stat_statements import ( pg_stat_statements_reset, ) @@ -57,7 +57,7 @@ def test_pg_stat_statements_query(self, pg_stat_statements_env): Column(dialect, "query"), Column(dialect, "calls"), ], - from_=TableExpression(dialect, "pg_stat_statements"), + from_=Table(dialect, "pg_stat_statements"), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -122,7 +122,7 @@ async def test_async_pg_stat_statements_query(self, async_pg_stat_statements_env Column(dialect, "query"), Column(dialect, "calls"), ], - from_=TableExpression(dialect, "pg_stat_statements"), + from_=Table(dialect, "pg_stat_statements"), ) sql, params = query.to_sql() result = await backend.execute(sql, params, options=opts) 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 a1edff96..0bf6ec32 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 @@ -27,9 +27,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression import RawSQLExpression @@ -71,7 +71,7 @@ def _setup_trgm_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -86,7 +86,7 @@ def _setup_trgm_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -108,7 +108,7 @@ def _setup_trgm_ops_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -122,7 +122,7 @@ def _setup_trgm_ops_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -149,7 +149,7 @@ def _setup_trgm_idx_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -167,7 +167,7 @@ def _setup_trgm_idx_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -189,7 +189,7 @@ def _setup_trgm_word_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -201,7 +201,7 @@ def _setup_trgm_word_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["description"], source=ValuesSource(dialect, rows), ) @@ -213,7 +213,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -234,7 +234,7 @@ async def _async_setup_trgm_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -249,7 +249,7 @@ async def _async_setup_trgm_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -271,7 +271,7 @@ async def _async_setup_trgm_ops_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -285,7 +285,7 @@ async def _async_setup_trgm_ops_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -312,7 +312,7 @@ async def _async_setup_trgm_idx_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -330,7 +330,7 @@ async def _async_setup_trgm_idx_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["name"], source=ValuesSource(dialect, rows), ) @@ -352,7 +352,7 @@ async def _async_setup_trgm_word_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -364,7 +364,7 @@ async def _async_setup_trgm_word_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["description"], source=ValuesSource(dialect, rows), ) @@ -376,7 +376,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -432,7 +432,7 @@ def test_trgm_similarity(self, pg_trgm_env): query = QueryExpression( dialect=dialect, select=[sim_func], - from_=TableExpression(dialect, TABLE_TRGM), + from_=Table(dialect, TABLE_TRGM), where=where_pred, ) sql, params = query.to_sql() @@ -455,7 +455,7 @@ def test_trgm_similarity(self, pg_trgm_env): query_world = QueryExpression( dialect=dialect, select=[sim_func_world], - from_=TableExpression(dialect, TABLE_TRGM), + from_=Table(dialect, TABLE_TRGM), where=where_pred_world, ) sql, params = query_world.to_sql() @@ -475,7 +475,7 @@ def test_trgm_similarity(self, pg_trgm_env): query_diff = QueryExpression( dialect=dialect, select=[sim_func_diff], - from_=TableExpression(dialect, TABLE_TRGM), + from_=Table(dialect, TABLE_TRGM), where=where_pred_diff, ) sql, params = query_diff.to_sql() @@ -511,7 +511,7 @@ def test_trgm_similarity_operator(self, pg_trgm_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), sim_func_select], - from_=TableExpression(dialect, TABLE_TRGM_OPS), + from_=Table(dialect, TABLE_TRGM_OPS), where=where_pred, ) sql, params = query.to_sql() @@ -553,7 +553,7 @@ def test_trgm_index(self, pg_trgm_env): query = QueryExpression( dialect=dialect, select=[count_func], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=combined_pred, ) sql, params = query.to_sql() @@ -574,7 +574,7 @@ def test_trgm_index(self, pg_trgm_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, TABLE_TRGM_IDX), + from_=Table(dialect, TABLE_TRGM_IDX), where=where_pred, ) sql, params = query.to_sql() @@ -617,7 +617,7 @@ def test_trgm_word_similarity(self, pg_trgm_env): query = QueryExpression( dialect=dialect, select=[ws_func], - from_=TableExpression(dialect, TABLE_TRGM_WORD), + from_=Table(dialect, TABLE_TRGM_WORD), where=where_pred, ) sql, params = query.to_sql() @@ -677,7 +677,7 @@ async def test_async_trgm_similarity(self, async_pg_trgm_env): query = QueryExpression( dialect=dialect, select=[sim_func], - from_=TableExpression(dialect, TABLE_TRGM_ASYNC), + from_=Table(dialect, TABLE_TRGM_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -700,7 +700,7 @@ async def test_async_trgm_similarity(self, async_pg_trgm_env): query_world = QueryExpression( dialect=dialect, select=[sim_func_world], - from_=TableExpression(dialect, TABLE_TRGM_ASYNC), + from_=Table(dialect, TABLE_TRGM_ASYNC), where=where_pred_world, ) sql, params = query_world.to_sql() @@ -720,7 +720,7 @@ async def test_async_trgm_similarity(self, async_pg_trgm_env): query_diff = QueryExpression( dialect=dialect, select=[sim_func_diff], - from_=TableExpression(dialect, TABLE_TRGM_ASYNC), + from_=Table(dialect, TABLE_TRGM_ASYNC), where=where_pred_diff, ) sql, params = query_diff.to_sql() @@ -757,7 +757,7 @@ async def test_async_trgm_similarity_operator(self, async_pg_trgm_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name"), sim_func_select], - from_=TableExpression(dialect, TABLE_TRGM_OPS_ASYNC), + from_=Table(dialect, TABLE_TRGM_OPS_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -798,7 +798,7 @@ async def test_async_trgm_index(self, async_pg_trgm_env): query = QueryExpression( dialect=dialect, select=[count_func], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=combined_pred, ) sql, params = query.to_sql() @@ -819,7 +819,7 @@ async def test_async_trgm_index(self, async_pg_trgm_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "name")], - from_=TableExpression(dialect, TABLE_TRGM_IDX_ASYNC), + from_=Table(dialect, TABLE_TRGM_IDX_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -864,7 +864,7 @@ async def test_async_trgm_word_similarity(self, async_pg_trgm_env): query = QueryExpression( dialect=dialect, select=[ws_func], - from_=TableExpression(dialect, TABLE_TRGM_WORD_ASYNC), + from_=Table(dialect, TABLE_TRGM_WORD_ASYNC), where=where_pred, ) sql, params = query.to_sql() 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 c6867850..fb711524 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 @@ -28,9 +28,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall, Subquery from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -69,7 +69,7 @@ def _setup_users_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -87,7 +87,7 @@ def _setup_users_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["username", "password_hash"], source=ValuesSource(dialect, rows), ) @@ -109,7 +109,7 @@ def _setup_encrypt_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -127,7 +127,7 @@ def _setup_encrypt_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["secret_data"], source=ValuesSource(dialect, rows), ) @@ -139,7 +139,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -161,7 +161,7 @@ async def _async_setup_users_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -179,7 +179,7 @@ async def _async_setup_users_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["username", "password_hash"], source=ValuesSource(dialect, rows), ) @@ -201,7 +201,7 @@ async def _async_setup_encrypt_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -219,7 +219,7 @@ async def _async_setup_encrypt_table(backend, dialect, table): ] insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["secret_data"], source=ValuesSource(dialect, rows), ) @@ -231,7 +231,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -290,7 +290,7 @@ def test_crypt_password(self, pgcrypto_env): query = QueryExpression( dialect=dialect, select=[match_expr], - from_=TableExpression(dialect, TABLE_USERS), + from_=Table(dialect, TABLE_USERS), where=where_pred, ) sql, params = query.to_sql() @@ -312,7 +312,7 @@ def test_crypt_password(self, pgcrypto_env): query_wrong = QueryExpression( dialect=dialect, select=[match_expr_wrong], - from_=TableExpression(dialect, TABLE_USERS), + from_=Table(dialect, TABLE_USERS), where=where_pred, ) sql, params = query_wrong.to_sql() @@ -476,7 +476,7 @@ def test_pgp_sym_encrypt_decrypt(self, pgcrypto_env): query = QueryExpression( dialect=dialect, select=[decrypt_func], - from_=TableExpression(dialect, TABLE_ENCRYPT), + from_=Table(dialect, TABLE_ENCRYPT), where=where_pred, ) sql, params = query.to_sql() @@ -492,7 +492,7 @@ def test_pgp_sym_encrypt_decrypt(self, pgcrypto_env): query_wrong = QueryExpression( dialect=dialect, select=[decrypt_func_wrong], - from_=TableExpression(dialect, TABLE_ENCRYPT), + from_=Table(dialect, TABLE_ENCRYPT), where=where_pred, ) sql, params = query_wrong.to_sql() @@ -553,7 +553,7 @@ async def test_async_crypt_password(self, async_pgcrypto_env): query = QueryExpression( dialect=dialect, select=[match_expr], - from_=TableExpression(dialect, TABLE_USERS_ASYNC), + from_=Table(dialect, TABLE_USERS_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -575,7 +575,7 @@ async def test_async_crypt_password(self, async_pgcrypto_env): query_wrong = QueryExpression( dialect=dialect, select=[match_expr_wrong], - from_=TableExpression(dialect, TABLE_USERS_ASYNC), + from_=Table(dialect, TABLE_USERS_ASYNC), where=where_pred, ) sql, params = query_wrong.to_sql() @@ -744,7 +744,7 @@ async def test_async_pgp_sym_encrypt_decrypt(self, async_pgcrypto_env): query = QueryExpression( dialect=dialect, select=[decrypt_func], - from_=TableExpression(dialect, TABLE_ENCRYPT_ASYNC), + from_=Table(dialect, TABLE_ENCRYPT_ASYNC), where=where_pred, ) sql, params = query.to_sql() @@ -760,7 +760,7 @@ async def test_async_pgp_sym_encrypt_decrypt(self, async_pgcrypto_env): query_wrong = QueryExpression( dialect=dialect, select=[decrypt_func_wrong], - from_=TableExpression(dialect, TABLE_ENCRYPT_ASYNC), + from_=Table(dialect, TABLE_ENCRYPT_ASYNC), where=where_pred, ) sql, params = query_wrong.to_sql() 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 a6564e7f..4c883f43 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 @@ -9,6 +9,7 @@ import pytest import pytest_asyncio +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord_postgres_test.feature.backend.utils import ( ensure_extension_installed, async_ensure_extension_installed, @@ -66,7 +67,7 @@ def pgrouting_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_route_edges", + table=Table(dialect, 'test_route_edges'), columns=columns, if_not_exists=True, ) @@ -78,7 +79,7 @@ def pgrouting_env(postgres_backend_single): # 1 --(cost=4)--> 3 insert_expr = InsertExpression( dialect=dialect, - into="test_route_edges", + into=Table(dialect, 'test_route_edges'), columns=["source", "target", "cost", "reverse_cost", "x1", "y1", "x2", "y2"], source=ValuesSource( dialect, @@ -115,7 +116,7 @@ def pgrouting_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_route_edges", + table=Table(dialect, 'test_route_edges'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -195,7 +196,7 @@ async def async_pgrouting_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table="test_route_edges_async", + table=Table(dialect, 'test_route_edges_async'), columns=columns, if_not_exists=True, ) @@ -207,7 +208,7 @@ async def async_pgrouting_env(async_postgres_backend_single): # 1 --(cost=4)--> 3 insert_expr = InsertExpression( dialect=dialect, - into="test_route_edges_async", + into=Table(dialect, 'test_route_edges_async'), columns=["source", "target", "cost", "reverse_cost", "x1", "y1", "x2", "y2"], source=ValuesSource( dialect, @@ -244,7 +245,7 @@ async def async_pgrouting_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table="test_route_edges_async", + table=Table(dialect, 'test_route_edges_async'), if_exists=True, ) sql, params = drop_expr.to_sql() 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 6403ad0c..435529bb 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 @@ -27,11 +27,11 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, UpdateExpression, RawSQLExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal from rhosocial.activerecord.backend.expression.query_parts import OrderByClause @@ -111,7 +111,7 @@ def vector_env(postgres_backend_single): for table in table_names: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -120,7 +120,7 @@ def vector_env(postgres_backend_single): # --- test_vector_items --- create_items = CreateTableExpression( dialect=dialect, - table="test_vector_items", + table=Table(dialect, 'test_vector_items'), columns=[_id_column(), _embedding_column()], ) sql, params = create_items.to_sql() @@ -128,7 +128,7 @@ def vector_env(postgres_backend_single): insert_items = InsertExpression( dialect=dialect, - into="test_vector_items", + into=Table(dialect, 'test_vector_items'), columns=["embedding"], source=ValuesSource( dialect, @@ -144,7 +144,7 @@ def vector_env(postgres_backend_single): # --- test_vector_l2 --- create_l2 = CreateTableExpression( dialect=dialect, - table="test_vector_l2", + table=Table(dialect, 'test_vector_l2'), columns=[_id_column(), _embedding_column()], ) sql, params = create_l2.to_sql() @@ -152,7 +152,7 @@ def vector_env(postgres_backend_single): insert_l2 = InsertExpression( dialect=dialect, - into="test_vector_l2", + into=Table(dialect, 'test_vector_l2'), columns=["embedding"], source=ValuesSource( dialect, @@ -169,7 +169,7 @@ def vector_env(postgres_backend_single): # --- test_vector_cosine --- create_cosine = CreateTableExpression( dialect=dialect, - table="test_vector_cosine", + table=Table(dialect, 'test_vector_cosine'), columns=[_id_column(), _embedding_column()], ) sql, params = create_cosine.to_sql() @@ -177,7 +177,7 @@ def vector_env(postgres_backend_single): insert_cosine = InsertExpression( dialect=dialect, - into="test_vector_cosine", + into=Table(dialect, 'test_vector_cosine'), columns=["embedding"], source=ValuesSource( dialect, @@ -194,7 +194,7 @@ def vector_env(postgres_backend_single): # --- test_vector_ip --- create_ip = CreateTableExpression( dialect=dialect, - table="test_vector_ip", + table=Table(dialect, 'test_vector_ip'), columns=[_id_column(), _embedding_column()], ) sql, params = create_ip.to_sql() @@ -202,7 +202,7 @@ def vector_env(postgres_backend_single): insert_ip = InsertExpression( dialect=dialect, - into="test_vector_ip", + into=Table(dialect, 'test_vector_ip'), columns=["embedding"], source=ValuesSource( dialect, @@ -226,7 +226,7 @@ def vector_env(postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -244,7 +244,7 @@ def test_vector_type_crud(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "embedding")], - from_=TableExpression(dialect, "test_vector_items"), + from_=Table(dialect, "test_vector_items"), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -306,7 +306,7 @@ def test_l2_distance(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), distance_expr], - from_=TableExpression(dialect, "test_vector_l2"), + from_=Table(dialect, "test_vector_l2"), order_by=OrderByClause( dialect, # TODO: Replace with ComparisonPredicate when it supports .as_() alias @@ -335,7 +335,7 @@ def test_cosine_distance(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), distance_expr], - from_=TableExpression(dialect, "test_vector_cosine"), + from_=Table(dialect, "test_vector_cosine"), order_by=OrderByClause( dialect, # TODO: Replace with ComparisonPredicate when it supports .as_() alias @@ -364,7 +364,7 @@ def test_inner_product(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), ip_expr], - from_=TableExpression(dialect, "test_vector_ip"), + from_=Table(dialect, "test_vector_ip"), order_by=OrderByClause( dialect, # TODO: Replace with ComparisonPredicate when it supports .as_() alias @@ -386,7 +386,7 @@ def test_vector_update(self, vector_env): # Create temporary table create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_update", + table=Table(dialect, 'test_vector_update'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -396,7 +396,7 @@ def test_vector_update(self, vector_env): # Insert initial vector insert_expr = InsertExpression( dialect=dialect, - into="test_vector_update", + into=Table(dialect, 'test_vector_update'), columns=["embedding"], source=ValuesSource( dialect, @@ -409,7 +409,7 @@ def test_vector_update(self, vector_env): # Update the vector update_expr = UpdateExpression( dialect=dialect, - table="test_vector_update", + table=Table(dialect, 'test_vector_update'), assignments={"embedding": Literal(dialect, "[4.0, 5.0, 6.0]").cast("vector")}, where=Column(dialect, "id") == Literal(dialect, 1), ) @@ -421,7 +421,7 @@ def test_vector_update(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "embedding")], - from_=TableExpression(dialect, "test_vector_update"), + from_=Table(dialect, "test_vector_update"), where=Column(dialect, "id") == Literal(dialect, 1), ) sql, params = query.to_sql() @@ -431,7 +431,7 @@ def test_vector_update(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_update", + table=Table(dialect, 'test_vector_update'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -444,7 +444,7 @@ def test_ivfflat_index(self, vector_env): # Create table for IVFFlat index test create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_ivfflat", + table=Table(dialect, 'test_vector_ivfflat'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -458,7 +458,7 @@ def test_ivfflat_index(self, vector_env): ] insert_expr = InsertExpression( dialect=dialect, - into="test_vector_ivfflat", + into=Table(dialect, 'test_vector_ivfflat'), columns=["embedding"], source=ValuesSource(dialect, rows), ) @@ -477,7 +477,7 @@ def test_ivfflat_index(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_vector_ivfflat"), ) sql, params = query.to_sql() @@ -488,7 +488,7 @@ def test_ivfflat_index(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_ivfflat", + table=Table(dialect, 'test_vector_ivfflat'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -505,7 +505,7 @@ def test_hnsw_index(self, vector_env): # Create table for HNSW index test create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_hnsw", + table=Table(dialect, 'test_vector_hnsw'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -519,7 +519,7 @@ def test_hnsw_index(self, vector_env): ] insert_expr = InsertExpression( dialect=dialect, - into="test_vector_hnsw", + into=Table(dialect, 'test_vector_hnsw'), columns=["embedding"], source=ValuesSource(dialect, rows), ) @@ -538,7 +538,7 @@ def test_hnsw_index(self, vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_vector_hnsw"), ) sql, params = query.to_sql() @@ -549,7 +549,7 @@ def test_hnsw_index(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_hnsw", + table=Table(dialect, 'test_vector_hnsw'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -562,7 +562,7 @@ def test_vector_dimension_constraint(self, vector_env): # Create table with VECTOR(3) dimension constraint create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_dim", + table=Table(dialect, 'test_vector_dim'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -572,7 +572,7 @@ def test_vector_dimension_constraint(self, vector_env): # Inserting a vector with wrong dimension should fail insert_expr = InsertExpression( dialect=dialect, - into="test_vector_dim", + into=Table(dialect, 'test_vector_dim'), columns=["embedding"], source=ValuesSource( dialect, @@ -585,7 +585,7 @@ def test_vector_dimension_constraint(self, vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_dim", + table=Table(dialect, 'test_vector_dim'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -613,7 +613,7 @@ async def async_vector_env(async_postgres_backend_single): for table in async_table_names: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -622,7 +622,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_items_async --- create_items = CreateTableExpression( dialect=dialect, - table="test_vector_items_async", + table=Table(dialect, 'test_vector_items_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_items.to_sql() @@ -630,7 +630,7 @@ async def async_vector_env(async_postgres_backend_single): insert_items = InsertExpression( dialect=dialect, - into="test_vector_items_async", + into=Table(dialect, 'test_vector_items_async'), columns=["embedding"], source=ValuesSource( dialect, @@ -646,7 +646,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_l2_async --- create_l2 = CreateTableExpression( dialect=dialect, - table="test_vector_l2_async", + table=Table(dialect, 'test_vector_l2_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_l2.to_sql() @@ -654,7 +654,7 @@ async def async_vector_env(async_postgres_backend_single): insert_l2 = InsertExpression( dialect=dialect, - into="test_vector_l2_async", + into=Table(dialect, 'test_vector_l2_async'), columns=["embedding"], source=ValuesSource( dialect, @@ -671,7 +671,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_cosine_async --- create_cosine = CreateTableExpression( dialect=dialect, - table="test_vector_cosine_async", + table=Table(dialect, 'test_vector_cosine_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_cosine.to_sql() @@ -679,7 +679,7 @@ async def async_vector_env(async_postgres_backend_single): insert_cosine = InsertExpression( dialect=dialect, - into="test_vector_cosine_async", + into=Table(dialect, 'test_vector_cosine_async'), columns=["embedding"], source=ValuesSource( dialect, @@ -696,7 +696,7 @@ async def async_vector_env(async_postgres_backend_single): # --- test_vector_ip_async --- create_ip = CreateTableExpression( dialect=dialect, - table="test_vector_ip_async", + table=Table(dialect, 'test_vector_ip_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_ip.to_sql() @@ -704,7 +704,7 @@ async def async_vector_env(async_postgres_backend_single): insert_ip = InsertExpression( dialect=dialect, - into="test_vector_ip_async", + into=Table(dialect, 'test_vector_ip_async'), columns=["embedding"], source=ValuesSource( dialect, @@ -728,7 +728,7 @@ async def async_vector_env(async_postgres_backend_single): ]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -747,7 +747,7 @@ async def test_async_vector_type_crud(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "embedding")], - from_=TableExpression(dialect, "test_vector_items_async"), + from_=Table(dialect, "test_vector_items_async"), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -814,7 +814,7 @@ async def test_async_l2_distance(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), distance_expr], - from_=TableExpression(dialect, "test_vector_l2_async"), + from_=Table(dialect, "test_vector_l2_async"), order_by=OrderByClause( dialect, # TODO: Replace with ComparisonPredicate when it supports .as_() alias @@ -842,7 +842,7 @@ async def test_async_cosine_distance(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), distance_expr], - from_=TableExpression(dialect, "test_vector_cosine_async"), + from_=Table(dialect, "test_vector_cosine_async"), order_by=OrderByClause( dialect, # TODO: Replace with ComparisonPredicate when it supports .as_() alias @@ -870,7 +870,7 @@ async def test_async_inner_product(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), ip_expr], - from_=TableExpression(dialect, "test_vector_ip_async"), + from_=Table(dialect, "test_vector_ip_async"), order_by=OrderByClause( dialect, # TODO: Replace with ComparisonPredicate when it supports .as_() alias @@ -890,7 +890,7 @@ async def test_async_vector_update(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_update_async", + table=Table(dialect, 'test_vector_update_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -899,7 +899,7 @@ async def test_async_vector_update(self, async_vector_env): try: insert_expr = InsertExpression( dialect=dialect, - into="test_vector_update_async", + into=Table(dialect, 'test_vector_update_async'), columns=["embedding"], source=ValuesSource( dialect, @@ -911,7 +911,7 @@ async def test_async_vector_update(self, async_vector_env): update_expr = UpdateExpression( dialect=dialect, - table="test_vector_update_async", + table=Table(dialect, 'test_vector_update_async'), assignments={"embedding": Literal(dialect, "[4.0, 5.0, 6.0]").cast("vector")}, where=Column(dialect, "id") == Literal(dialect, 1), ) @@ -922,7 +922,7 @@ async def test_async_vector_update(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "embedding")], - from_=TableExpression(dialect, "test_vector_update_async"), + from_=Table(dialect, "test_vector_update_async"), where=Column(dialect, "id") == Literal(dialect, 1), ) sql, params = query.to_sql() @@ -932,7 +932,7 @@ async def test_async_vector_update(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_update_async", + table=Table(dialect, 'test_vector_update_async'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -945,7 +945,7 @@ async def test_async_ivfflat_index(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_ivfflat_async", + table=Table(dialect, 'test_vector_ivfflat_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -958,7 +958,7 @@ async def test_async_ivfflat_index(self, async_vector_env): ] insert_expr = InsertExpression( dialect=dialect, - into="test_vector_ivfflat_async", + into=Table(dialect, 'test_vector_ivfflat_async'), columns=["embedding"], source=ValuesSource(dialect, rows), ) @@ -976,7 +976,7 @@ async def test_async_ivfflat_index(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_vector_ivfflat_async"), ) sql, params = query.to_sql() @@ -987,7 +987,7 @@ async def test_async_ivfflat_index(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_ivfflat_async", + table=Table(dialect, 'test_vector_ivfflat_async'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -1003,7 +1003,7 @@ async def test_async_hnsw_index(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_hnsw_async", + table=Table(dialect, 'test_vector_hnsw_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -1016,7 +1016,7 @@ async def test_async_hnsw_index(self, async_vector_env): ] insert_expr = InsertExpression( dialect=dialect, - into="test_vector_hnsw_async", + into=Table(dialect, 'test_vector_hnsw_async'), columns=["embedding"], source=ValuesSource(dialect, rows), ) @@ -1034,7 +1034,7 @@ async def test_async_hnsw_index(self, async_vector_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=Column(dialect, "tablename") == Literal(dialect, "test_vector_hnsw_async"), ) sql, params = query.to_sql() @@ -1045,7 +1045,7 @@ async def test_async_hnsw_index(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_hnsw_async", + table=Table(dialect, 'test_vector_hnsw_async'), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -1058,7 +1058,7 @@ async def test_async_vector_dimension_constraint(self, async_vector_env): create_expr = CreateTableExpression( dialect=dialect, - table="test_vector_dim_async", + table=Table(dialect, 'test_vector_dim_async'), columns=[_id_column(), _embedding_column()], ) sql, params = create_expr.to_sql() @@ -1067,7 +1067,7 @@ async def test_async_vector_dimension_constraint(self, async_vector_env): try: insert_expr = InsertExpression( dialect=dialect, - into="test_vector_dim_async", + into=Table(dialect, 'test_vector_dim_async'), columns=["embedding"], source=ValuesSource( dialect, @@ -1080,7 +1080,7 @@ async def test_async_vector_dimension_constraint(self, async_vector_env): finally: drop_expr = DropTableExpression( dialect=dialect, - table="test_vector_dim_async", + table=Table(dialect, 'test_vector_dim_async'), if_exists=True, ) sql, params = drop_expr.to_sql() 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 2464a103..ea2eebb1 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 @@ -23,11 +23,11 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, CreateIndexExpression, OrderByClause, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -57,7 +57,7 @@ def _setup_geo_points_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -66,7 +66,7 @@ def _setup_geo_points_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -94,7 +94,7 @@ def _setup_geo_distance_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -103,7 +103,7 @@ def _setup_geo_distance_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -131,7 +131,7 @@ def _setup_geo_contains_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -140,7 +140,7 @@ def _setup_geo_contains_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -174,7 +174,7 @@ def _setup_geo_index_table(backend, dialect, table, index_name): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -183,7 +183,7 @@ def _setup_geo_index_table(backend, dialect, table, index_name): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -197,8 +197,8 @@ def _setup_geo_index_table(backend, dialect, table, index_name): create_index = CreateIndexExpression( dialect=dialect, - index_name=index_name, - table_name=table, + index=Index(dialect, index_name), + table=Table(dialect, table), columns=["geom"], index_type="GIST", if_not_exists=True, @@ -221,7 +221,7 @@ def _setup_geog_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -230,7 +230,7 @@ def _setup_geog_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geog"], source=ValuesSource( dialect, @@ -247,7 +247,7 @@ def _teardown_table(backend, dialect, table): """Drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -268,7 +268,7 @@ async def _async_setup_geo_points_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -277,7 +277,7 @@ async def _async_setup_geo_points_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -305,7 +305,7 @@ async def _async_setup_geo_distance_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -314,7 +314,7 @@ async def _async_setup_geo_distance_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -342,7 +342,7 @@ async def _async_setup_geo_contains_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -351,7 +351,7 @@ async def _async_setup_geo_contains_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -385,7 +385,7 @@ async def _async_setup_geo_index_table(backend, dialect, table, index_name): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -394,7 +394,7 @@ async def _async_setup_geo_index_table(backend, dialect, table, index_name): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geom"], source=ValuesSource( dialect, @@ -408,8 +408,8 @@ async def _async_setup_geo_index_table(backend, dialect, table, index_name): create_index = CreateIndexExpression( dialect=dialect, - index_name=index_name, - table_name=table, + index=Index(dialect, index_name), + table=Table(dialect, table), columns=["geom"], index_type="GIST", if_not_exists=True, @@ -432,7 +432,7 @@ async def _async_setup_geog_table(backend, dialect, table): ] create_expr = CreateTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), columns=columns, if_not_exists=True, ) @@ -441,7 +441,7 @@ async def _async_setup_geog_table(backend, dialect, table): insert_expr = InsertExpression( dialect=dialect, - into=table, + into=Table(dialect, table), columns=["geog"], source=ValuesSource( dialect, @@ -458,7 +458,7 @@ async def _async_teardown_table(backend, dialect, table): """Async: drop a test table using expression.""" drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -521,7 +521,7 @@ def test_geometry_type_crud(self, postgis_env): select=[ FunctionCall(dialect, "ST_AsText", Column(dialect, "geom")).as_("wkt"), ], - from_=TableExpression(dialect, T_GEO_POINTS), + from_=Table(dialect, T_GEO_POINTS), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -696,7 +696,7 @@ def test_spatial_index(self, postgis_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=ComparisonPredicate( dialect, "=", @@ -720,7 +720,7 @@ def test_geography_type(self, postgis_env): select=[ FunctionCall(dialect, "ST_AsText", Column(dialect, "geog")).as_("wkt"), ], - from_=TableExpression(dialect, T_GEOG), + from_=Table(dialect, T_GEOG), ) sql, params = query.to_sql() result = backend.execute(sql, params, options=opts) @@ -853,7 +853,7 @@ async def test_async_geometry_type_crud(self, async_postgis_env): select=[ FunctionCall(dialect, "ST_AsText", Column(dialect, "geom")).as_("wkt"), ], - from_=TableExpression(dialect, T_GEO_POINTS_ASYNC), + from_=Table(dialect, T_GEO_POINTS_ASYNC), order_by=OrderByClause(dialect, [Column(dialect, "id")]), ) sql, params = query.to_sql() @@ -1031,7 +1031,7 @@ async def test_async_spatial_index(self, async_postgis_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "indexname")], - from_=TableExpression(dialect, "pg_indexes"), + from_=Table(dialect, "pg_indexes"), where=ComparisonPredicate( dialect, "=", @@ -1056,7 +1056,7 @@ async def test_async_geography_type(self, async_postgis_env): select=[ FunctionCall(dialect, "ST_AsText", Column(dialect, "geog")).as_("wkt"), ], - from_=TableExpression(dialect, T_GEOG_ASYNC), + from_=Table(dialect, T_GEOG_ASYNC), ) sql, params = query.to_sql() result = await backend.execute(sql, params, options=opts) 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 e012292f..c539ffbf 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 @@ -24,9 +24,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.impl.postgres.functions.postgis_raster import ( @@ -100,7 +100,7 @@ def raster_env(postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -111,7 +111,7 @@ def raster_env(postgres_backend_single): rast_expr = _make_empty_raster(dialect) insert_expr = InsertExpression( dialect=dialect, - into=TABLE_NAME, + into=Table(dialect, TABLE_NAME), columns=["name", "rast"], source=ValuesSource( dialect, @@ -131,7 +131,7 @@ def raster_env(postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=TABLE_NAME, + table=Table(dialect, TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -149,7 +149,7 @@ def test_st_summary(self, raster_env): query = QueryExpression( dialect=dialect, select=[summary_func.as_("summary")], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), ) sql, params = query.to_sql() opts = ExecutionOptions(stmt_type=StatementType.DQL) @@ -176,7 +176,7 @@ def test_st_value(self, raster_env): query = QueryExpression( dialect=dialect, select=[value_func.as_("pixel_value")], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), ) sql, params = query.to_sql() opts = ExecutionOptions(stmt_type=StatementType.DQL) @@ -219,7 +219,7 @@ async def async_raster_env(async_postgres_backend_single): ] create_expr = CreateTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), columns=columns, if_not_exists=True, ) @@ -230,7 +230,7 @@ async def async_raster_env(async_postgres_backend_single): rast_expr = _make_empty_raster(dialect) insert_expr = InsertExpression( dialect=dialect, - into=ASYNC_TABLE_NAME, + into=Table(dialect, ASYNC_TABLE_NAME), columns=["name", "rast"], source=ValuesSource( dialect, @@ -250,7 +250,7 @@ async def async_raster_env(async_postgres_backend_single): # Teardown: drop table using expression drop_expr = DropTableExpression( dialect=dialect, - table=ASYNC_TABLE_NAME, + table=Table(dialect, ASYNC_TABLE_NAME), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -269,7 +269,7 @@ async def test_async_st_summary(self, async_raster_env): query = QueryExpression( dialect=dialect, select=[summary_func.as_("summary")], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), ) sql, params = query.to_sql() opts = ExecutionOptions(stmt_type=StatementType.DQL) @@ -297,7 +297,7 @@ async def test_async_st_value(self, async_raster_env): query = QueryExpression( dialect=dialect, select=[value_func.as_("pixel_value")], - from_=TableExpression(dialect, ASYNC_TABLE_NAME), + from_=Table(dialect, ASYNC_TABLE_NAME), ) sql, params = query.to_sql() opts = ExecutionOptions(stmt_type=StatementType.DQL) 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 1fc5b3ae..b57ac9b2 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 @@ -49,6 +49,7 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.objects import Table @pytest.fixture @@ -73,7 +74,7 @@ def tablefunc_env(postgres_backend_single): ] create_sales = CreateTableExpression( dialect=dialect, - table="test_tf_sales", + table=Table(dialect, 'test_tf_sales'), columns=sales_columns, if_not_exists=True, ) @@ -91,7 +92,7 @@ def tablefunc_env(postgres_backend_single): ] insert_sales = InsertExpression( dialect=dialect, - into="test_tf_sales", + into=Table(dialect, 'test_tf_sales'), columns=["month", "category", "amount"], source=ValuesSource(dialect, sales_rows), ) @@ -112,7 +113,7 @@ def tablefunc_env(postgres_backend_single): ] create_tree = CreateTableExpression( dialect=dialect, - table="test_tf_tree", + table=Table(dialect, 'test_tf_tree'), columns=tree_columns, if_not_exists=True, ) @@ -128,7 +129,7 @@ def tablefunc_env(postgres_backend_single): ] insert_tree = InsertExpression( dialect=dialect, - into="test_tf_tree", + into=Table(dialect, 'test_tf_tree'), columns=["node_name", "parent_id"], source=ValuesSource(dialect, tree_rows), ) @@ -140,7 +141,7 @@ def tablefunc_env(postgres_backend_single): # Teardown: drop both tables using DropTableExpression drop_sales = DropTableExpression( dialect=dialect, - table="test_tf_sales", + table=Table(dialect, 'test_tf_sales'), if_exists=True, ) sql, params = drop_sales.to_sql() @@ -148,7 +149,7 @@ def tablefunc_env(postgres_backend_single): drop_tree = DropTableExpression( dialect=dialect, - table="test_tf_tree", + table=Table(dialect, 'test_tf_tree'), if_exists=True, ) sql, params = drop_tree.to_sql() @@ -351,7 +352,7 @@ async def async_tablefunc_env(async_postgres_backend_single): ] create_sales = CreateTableExpression( dialect=dialect, - table="test_tf_sales_async", + table=Table(dialect, 'test_tf_sales_async'), columns=sales_columns, if_not_exists=True, ) @@ -369,7 +370,7 @@ async def async_tablefunc_env(async_postgres_backend_single): ] insert_sales = InsertExpression( dialect=dialect, - into="test_tf_sales_async", + into=Table(dialect, 'test_tf_sales_async'), columns=["month", "category", "amount"], source=ValuesSource(dialect, sales_rows), ) @@ -390,7 +391,7 @@ async def async_tablefunc_env(async_postgres_backend_single): ] create_tree = CreateTableExpression( dialect=dialect, - table="test_tf_tree_async", + table=Table(dialect, 'test_tf_tree_async'), columns=tree_columns, if_not_exists=True, ) @@ -406,7 +407,7 @@ async def async_tablefunc_env(async_postgres_backend_single): ] insert_tree = InsertExpression( dialect=dialect, - into="test_tf_tree_async", + into=Table(dialect, 'test_tf_tree_async'), columns=["node_name", "parent_id"], source=ValuesSource(dialect, tree_rows), ) @@ -418,7 +419,7 @@ async def async_tablefunc_env(async_postgres_backend_single): # Teardown: drop both tables using DropTableExpression drop_sales = DropTableExpression( dialect=dialect, - table="test_tf_sales_async", + table=Table(dialect, 'test_tf_sales_async'), if_exists=True, ) sql, params = drop_sales.to_sql() @@ -426,7 +427,7 @@ async def async_tablefunc_env(async_postgres_backend_single): drop_tree = DropTableExpression( dialect=dialect, - table="test_tf_tree_async", + table=Table(dialect, 'test_tf_tree_async'), if_exists=True, ) sql, params = drop_tree.to_sql() 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 69f2c7a6..f98e917c 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 @@ -27,9 +27,9 @@ DropTableExpression, InsertExpression, QueryExpression, - TableExpression, Column, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate @@ -66,14 +66,14 @@ def uuid_ossp_env(postgres_backend_single): # Clean up residual tables from previous runs for table in ["test_uuid_ossp", "test_uuid_explicit"]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() backend.execute(sql, params) # Setup: create test_uuid_ossp table (with DEFAULT uuid_generate_v4()) create_ossp = CreateTableExpression( dialect=dialect, - table="test_uuid_ossp", + table=Table(dialect, 'test_uuid_ossp'), columns=[ ColumnDefinition( dialect, name="id", @@ -96,7 +96,7 @@ def uuid_ossp_env(postgres_backend_single): # Insert data into test_uuid_ossp (id auto-generated) insert_ossp = InsertExpression( dialect=dialect, - into="test_uuid_ossp", + into=Table(dialect, 'test_uuid_ossp'), columns=["name"], source=ValuesSource( dialect, @@ -109,7 +109,7 @@ def uuid_ossp_env(postgres_backend_single): # Setup: create test_uuid_explicit table (no DEFAULT) create_explicit = CreateTableExpression( dialect=dialect, - table="test_uuid_explicit", + table=Table(dialect, 'test_uuid_explicit'), columns=[ ColumnDefinition( dialect, name="id", @@ -138,7 +138,7 @@ def uuid_ossp_env(postgres_backend_single): # Insert with the generated UUID insert_explicit = InsertExpression( dialect=dialect, - into="test_uuid_explicit", + into=Table(dialect, 'test_uuid_explicit'), columns=["id", "label"], source=ValuesSource( dialect, @@ -154,7 +154,7 @@ def uuid_ossp_env(postgres_backend_single): for table in ["test_uuid_ossp", "test_uuid_explicit"]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -205,7 +205,7 @@ def test_uuid_column_default(self, uuid_ossp_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "test_uuid_ossp"), + from_=Table(dialect, "test_uuid_ossp"), where=ComparisonPredicate( dialect, "=", Column(dialect, "name"), @@ -245,7 +245,7 @@ def test_uuid_column_explicit_insert(self, uuid_ossp_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "label")], - from_=TableExpression(dialect, "test_uuid_explicit"), + from_=Table(dialect, "test_uuid_explicit"), where=ComparisonPredicate( dialect, "=", Column(dialect, "label"), @@ -271,14 +271,14 @@ async def async_uuid_ossp_env(async_postgres_backend_single): # Clean up residual tables from previous runs for table in ["test_uuid_ossp_async", "test_uuid_explicit_async"]: - drop_expr = DropTableExpression(dialect=dialect, table=table, if_exists=True) + drop_expr = DropTableExpression(dialect=dialect, table=Table(dialect, table), if_exists=True) sql, params = drop_expr.to_sql() await backend.execute(sql, params) # Setup: create test_uuid_ossp_async table (with DEFAULT uuid_generate_v4()) create_ossp = CreateTableExpression( dialect=dialect, - table="test_uuid_ossp_async", + table=Table(dialect, 'test_uuid_ossp_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -301,7 +301,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): # Insert data into test_uuid_ossp_async (id auto-generated) insert_ossp = InsertExpression( dialect=dialect, - into="test_uuid_ossp_async", + into=Table(dialect, 'test_uuid_ossp_async'), columns=["name"], source=ValuesSource( dialect, @@ -314,7 +314,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): # Setup: create test_uuid_explicit_async table (no DEFAULT) create_explicit = CreateTableExpression( dialect=dialect, - table="test_uuid_explicit_async", + table=Table(dialect, 'test_uuid_explicit_async'), columns=[ ColumnDefinition( dialect, name="id", @@ -343,7 +343,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): # Insert with the generated UUID insert_explicit = InsertExpression( dialect=dialect, - into="test_uuid_explicit_async", + into=Table(dialect, 'test_uuid_explicit_async'), columns=["id", "label"], source=ValuesSource( dialect, @@ -359,7 +359,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): for table in ["test_uuid_ossp_async", "test_uuid_explicit_async"]: drop_expr = DropTableExpression( dialect=dialect, - table=table, + table=Table(dialect, table), if_exists=True, ) sql, params = drop_expr.to_sql() @@ -413,7 +413,7 @@ async def test_async_uuid_column_default(self, async_uuid_ossp_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "test_uuid_ossp_async"), + from_=Table(dialect, "test_uuid_ossp_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "name"), @@ -455,7 +455,7 @@ async def test_async_uuid_column_explicit_insert(self, async_uuid_ossp_env): query = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "label")], - from_=TableExpression(dialect, "test_uuid_explicit_async"), + from_=Table(dialect, "test_uuid_explicit_async"), where=ComparisonPredicate( dialect, "=", Column(dialect, "label"), 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 4bf23423..540a5247 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 @@ -35,6 +35,7 @@ from rhosocial.activerecord.backend.expression.statements import ( ColumnConstraint, ColumnConstraintType, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.mixins.dml.extended_statistics import ( PostgresExtendedStatisticsMixin, # noqa: F401 ) @@ -403,7 +404,7 @@ def test_create_and_add_check_fk_enforcement(self): fk = ForeignKeyConstraint( dialect, columns=["parent_id"], - foreign_key_table="people", + foreign_key_table=Table(dialect, 'people'), foreign_key_columns=["id"], enforced=True, ) @@ -439,7 +440,7 @@ def test_not_valid_is_limited_to_check_and_fk(self): constraint = ForeignKeyConstraint( dialect, columns=["parent_id"], - foreign_key_table="people", + foreign_key_table=Table(dialect, 'people'), foreign_key_columns=["id"], validation=ConstraintValidation.NOVALIDATE, ) @@ -531,7 +532,7 @@ def test_exclude_create_and_add(self): ) create = CreateTableExpression( dialect, - "ranges", + Table(dialect, 'ranges'), [ColumnDefinition(dialect, "range", IntegerType(dialect))], table_constraints=[exclude], ) @@ -557,7 +558,7 @@ def test_exclude_create_and_add(self): ) invalid_create = CreateTableExpression( dialect, - "invalid_checks", + Table(dialect, 'invalid_checks'), [ColumnDefinition(dialect, "id", IntegerType(dialect))], table_constraints=[not_valid_check], ) 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 28b909bb..20fcab0f 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 @@ -10,6 +10,7 @@ import pytest from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import MaterializedView, Table from rhosocial.activerecord.backend.expression import ( Column, FunctionCall, @@ -60,7 +61,7 @@ def test_basic_refresh(self, dialect): """Test basic REFRESH MATERIALIZED VIEW statement.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name="monthly_sales_summary", + view=MaterializedView(dialect, "monthly_sales_summary"), ) sql, params = expr.to_sql() assert sql == 'REFRESH MATERIALIZED VIEW "monthly_sales_summary"' @@ -70,8 +71,7 @@ def test_refresh_with_schema(self, dialect): """Test REFRESH MATERIALIZED VIEW with schema.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name="monthly_sales_summary", - schema="analytics", + view=MaterializedView(dialect, "monthly_sales_summary", schema_name="analytics"), ) sql, params = expr.to_sql() assert sql == 'REFRESH MATERIALIZED VIEW "analytics"."monthly_sales_summary"' @@ -82,7 +82,7 @@ def test_refresh_concurrently_pg13(self): dialect_pg93 = PostgresDialect(version=(9, 3, 0)) expr = PostgresRefreshMaterializedViewExpression( dialect=dialect_pg93, - name="monthly_sales_summary", + view=MaterializedView(dialect_pg93, "monthly_sales_summary"), concurrently=True, ) @@ -95,7 +95,7 @@ def test_refresh_concurrently_pg94(self, dialect): """Test CONCURRENTLY refresh with PG 9.4+.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name="monthly_sales_summary", + view=MaterializedView(dialect, "monthly_sales_summary"), concurrently=True, ) sql, params = expr.to_sql() @@ -107,7 +107,7 @@ def test_refresh_with_data_false_pg93(self): dialect_pg93 = PostgresDialect(version=(9, 3, 0)) expr = PostgresRefreshMaterializedViewExpression( dialect=dialect_pg93, - name="monthly_sales_summary", + view=MaterializedView(dialect_pg93, "monthly_sales_summary"), with_data=False, ) @@ -118,7 +118,7 @@ def test_refresh_with_data_false_pg94(self, dialect): """Test WITH NO DATA with PG 9.4+.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name="monthly_sales_summary", + view=MaterializedView(dialect, "monthly_sales_summary"), with_data=False, ) sql, params = expr.to_sql() @@ -129,7 +129,7 @@ def test_refresh_concurrently_and_with_data(self, dialect): """Test CONCURRENTLY with WITH NO DATA.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, - name="monthly_sales_summary", + view=MaterializedView(dialect, "monthly_sales_summary"), concurrently=True, with_data=False, ) @@ -151,20 +151,21 @@ def test_comment_on_table(self, dialect): expr = PostgresCommentExpression( dialect=dialect, object_type="TABLE", - object_name="users", + object=Table(dialect, "users"), comment="User accounts table", ) sql, params = expr.to_sql() assert "COMMENT ON TABLE" in sql - assert "users" in sql + assert '"users"' in sql assert params == ("User accounts table",) def test_comment_on_column(self, dialect): - """Test COMMENT ON COLUMN (dotted target quoted segment-by-segment).""" + """Test COMMENT ON COLUMN (column named beside the object it belongs to).""" expr = PostgresCommentExpression( dialect=dialect, object_type="COLUMN", - object_name="users.email", + object=Table(dialect, "users"), + column="email", comment="User email address", ) sql, params = expr.to_sql() @@ -177,7 +178,7 @@ def test_comment_on_index(self, dialect): expr = PostgresCommentExpression( dialect=dialect, object_type="INDEX", - object_name="users_email_idx", + object=Table(dialect, "users"), comment="Email index for users table", ) sql, params = expr.to_sql() @@ -188,7 +189,7 @@ def test_comment_on_view(self, dialect): expr = PostgresCommentExpression( dialect=dialect, object_type="VIEW", - object_name="user_stats", + object=Table(dialect, "user_stats"), comment="User statistics view", ) sql, params = expr.to_sql() @@ -199,7 +200,7 @@ def test_comment_on_schema(self, dialect): expr = PostgresCommentExpression( dialect=dialect, object_type="SCHEMA", - object_name="analytics", + object=Table(dialect, "analytics"), comment="Analytics schema", ) sql, params = expr.to_sql() @@ -210,7 +211,7 @@ def test_comment_on_function(self, dialect): expr = PostgresCommentExpression( dialect=dialect, object_type="FUNCTION", - object_name="calculate_total", + object=Table(dialect, "calculate_total"), comment="Calculate total amount", ) sql, params = expr.to_sql() @@ -221,7 +222,7 @@ def test_remove_comment(self, dialect): expr = PostgresCommentExpression( dialect=dialect, object_type="TABLE", - object_name="users", + object=Table(dialect, "users"), comment=None, ) sql, params = expr.to_sql() @@ -230,13 +231,12 @@ def test_remove_comment(self, dialect): assert params == () def test_comment_with_schema(self, dialect): - """Test comment on object with schema (dotted target quoted per segment).""" + """Test comment on an object in a schema (namespace comes off the object).""" expr = PostgresCommentExpression( dialect=dialect, object_type="TABLE", - object_name="public.users", + object=Table(dialect, "users", schema_name="public"), comment="Public users table", - schema="public", ) sql, params = expr.to_sql() assert '"public"."users"' in sql @@ -254,7 +254,7 @@ def test_create_range_partitioned_parent_table(self, dialect): """Test creating a RANGE-partitioned parent table.""" expr = CreateTableExpression( dialect=dialect, - table="events", + table=Table(dialect, 'events'), columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect)), ColumnDefinition( @@ -279,7 +279,7 @@ def test_create_list_partitioned_parent_table(self, dialect): """Test creating a LIST-partitioned parent table.""" expr = CreateTableExpression( dialect=dialect, - table="events", + table=Table(dialect, 'events'), columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect)), ColumnDefinition(dialect, "status", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), @@ -300,7 +300,7 @@ def test_create_hash_partitioned_parent_table_pg10(self): dialect = PostgresDialect(version=(10, 0, 0)) expr = CreateTableExpression( dialect=dialect, - table="events", + table=Table(dialect, 'events'), columns=[ ColumnDefinition( dialect, @@ -323,7 +323,7 @@ def test_create_hash_partitioned_parent_table_pg11(self): dialect = PostgresDialect(version=(11, 0, 0)) expr = CreateTableExpression( dialect=dialect, - table="events", + table=Table(dialect, 'events'), columns=[ ColumnDefinition( dialect, @@ -347,7 +347,7 @@ def test_create_partitioned_parent_table_pg9(self): dialect = PostgresDialect(version=(9, 6, 0)) expr = CreateTableExpression( dialect=dialect, - table="events", + table=Table(dialect, 'events'), columns=[ ColumnDefinition( dialect, @@ -398,7 +398,7 @@ def test_postgres_partition_clause_in_create_table(self, dialect): expr = CreateTableExpression( dialect=dialect, - table="events", + table=Table(dialect, 'events'), columns=[ ColumnDefinition( dialect, @@ -444,7 +444,7 @@ def test_multi_column_range_partitioned_parent_table(self, dialect): """Test creating a RANGE-partitioned parent table with multiple partition keys.""" expr = CreateTableExpression( dialect=dialect, - table="tenanted_events", + table=Table(dialect, 'tenanted_events'), columns=[ ColumnDefinition( dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_objects.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_objects.py index fa735898..fdc61252 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_objects.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_objects.py @@ -8,6 +8,7 @@ import pytest from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Domain, Type from rhosocial.activerecord.backend.dialect.exceptions import ( UnsupportedFeatureError, ) @@ -219,7 +220,7 @@ def test_create_domain_params_emit_one_constraint_key(self, dialect): def test_new_core_expression_rejects_raw_data_type(self, dialect): with pytest.raises(TypeError, match="DataType"): - CreateDomainExpression(dialect, "raw_domain", "INTEGER") + CreateDomainExpression(dialect, Domain(dialect, 'raw_domain'), "INTEGER") def test_all_domain_actions(self, dialect): value = DomainValueExpression(dialect) @@ -275,7 +276,7 @@ def test_all_domain_actions(self, dialect): def test_domain_actions_are_not_combinable(self, dialect): expression = AlterDomainExpression( dialect, - "amount", + Domain(dialect, 'amount'), [DropDomainDefaultAction(dialect), SetDomainNotNullAction(dialect)], ) with pytest.raises(UnsupportedFeatureError, match="multiple ALTER DOMAIN actions"): @@ -366,7 +367,7 @@ def test_composite_definition(self, dialect): ) sql, params = CreateTypeExpression( dialect, - "app.address", + Type(dialect, 'address', schema_name='app'), definition, ).to_sql() assert sql == ( @@ -380,7 +381,7 @@ def test_enum_definition_escapes_labels(self, dialect): dialect, ["ready", "O'Reilly", ""], ) - sql, params = CreateTypeExpression(dialect, "status", definition).to_sql() + sql, params = CreateTypeExpression(dialect, Type(dialect, 'status'), definition).to_sql() assert sql == "CREATE TYPE \"status\" AS ENUM ('ready', 'O''Reilly', '')" assert params == () @@ -392,7 +393,7 @@ def test_range_definition_with_multirange_name(self, dialect): canonical_function="public.canonical", multirange_type_name="app.span_multirange", ) - sql, params = CreateTypeExpression(dialect, "app.span", definition).to_sql() + sql, params = CreateTypeExpression(dialect, Type(dialect, 'span', schema_name='app'), definition).to_sql() assert sql == ( 'CREATE TYPE "app"."span" AS RANGE (SUBTYPE = INTEGER, ' 'SUBTYPE_OPCLASS = "public"."int_ops", CANONICAL = "public"."canonical", ' @@ -411,7 +412,7 @@ def test_range_multirange_name_version_gate(self): UnsupportedFeatureError, match="CREATE TYPE MULTIRANGE_TYPE_NAME", ): - CreateTypeExpression(dialect, "span", definition).to_sql() + CreateTypeExpression(dialect, Type(dialect, 'span'), definition).to_sql() def test_base_definition(self, dialect): definition = PostgresBaseTypeDefinition( @@ -424,7 +425,7 @@ def test_base_definition(self, dialect): default=0, element=IntegerType(dialect), ) - sql, params = CreateTypeExpression(dialect, "box", definition).to_sql() + sql, params = CreateTypeExpression(dialect, Type(dialect, 'box'), definition).to_sql() assert sql == ( 'CREATE TYPE "box" (INPUT = "public"."box_in", ' 'OUTPUT = "public"."box_out", INTERNALLENGTH = 16, PASSEDBYVALUE, ' @@ -442,7 +443,7 @@ def test_base_like_does_not_bypass_io_functions(self, dialect): def test_shell_definition(self, dialect): sql, params = CreateTypeExpression( dialect, - "public.pending_box", + Type(dialect, 'pending_box', schema_name='public'), PostgresShellTypeDefinition(dialect), ).to_sql() assert sql == 'CREATE TYPE "public"."pending_box"' @@ -453,14 +454,14 @@ def test_create_type_unsupported_clauses_fail_fast(self, dialect): with pytest.raises(UnsupportedFeatureError, match="CREATE TYPE IF NOT EXISTS"): CreateTypeExpression( dialect, - "status", + Type(dialect, 'status'), enum, if_not_exists=True, ).to_sql() with pytest.raises(UnsupportedFeatureError, match="CREATE OR REPLACE TYPE"): CreateTypeExpression( dialect, - "status", + Type(dialect, 'status'), enum, or_replace=True, ).to_sql() @@ -468,8 +469,7 @@ def test_create_type_unsupported_clauses_fail_fast(self, dialect): def test_drop_type_flags(self, dialect): sql, params = PostgresDropTypeExpression( dialect, - "status", - schema_name="app", + Type(dialect, 'status', schema_name="app"), if_exists=True, cascade=True, ).to_sql() @@ -477,7 +477,7 @@ def test_drop_type_flags(self, dialect): assert params == () expression = PostgresDropTypeExpression( dialect, - "status", + Type(dialect, 'status'), cascade=True, restrict=True, ) @@ -558,7 +558,7 @@ def test_all_type_actions(self, dialect): for action, expected in cases: sql, params = AlterTypeExpression( dialect, - "app.status", + Type(dialect, 'status', schema_name='app'), [action], ).to_sql() assert sql == f'ALTER TYPE "app"."status" {expected}' @@ -569,7 +569,7 @@ def test_composite_actions_can_be_combined(self, dialect): PostgresAddTypeAttributeAction(dialect, "active", IntegerType(dialect)), PostgresDropTypeAttributeAction(dialect, "legacy"), ] - sql, params = AlterTypeExpression(dialect, "app.address", actions).to_sql() + sql, params = AlterTypeExpression(dialect, Type(dialect, 'address', schema_name='app'), actions).to_sql() assert sql == ( 'ALTER TYPE "app"."address" ADD ATTRIBUTE "active" INTEGER, ' 'DROP ATTRIBUTE "legacy"' @@ -582,7 +582,7 @@ def test_mixed_type_actions_cannot_be_combined(self, dialect): PostgresRenameTypeAction(dialect, "new_address"), ] with pytest.raises(UnsupportedFeatureError, match="multiple ALTER TYPE actions"): - AlterTypeExpression(dialect, "app.address", actions).to_sql() + AlterTypeExpression(dialect, Type(dialect, 'address', schema_name='app'), actions).to_sql() def test_type_version_boundaries(self): enum = PostgresEnumTypeDefinition(PostgresDialect(version=(9, 6, 0)), ["a"]) @@ -594,12 +594,12 @@ def test_type_version_boundaries(self): with pytest.raises(UnsupportedFeatureError, match="RENAME VALUE"): AlterTypeExpression( PostgresDialect(version=(9, 6, 0)), - "status", + Type(PostgresDialect(version=(9, 6, 0)), 'status'), [rename], ).to_sql() sql, _ = AlterTypeExpression( PostgresDialect(version=(10, 0, 0)), - "status", + Type(PostgresDialect(version=(10, 0, 0)), 'status'), [PostgresRenameEnumValueAction( PostgresDialect(version=(10, 0, 0)), "a", @@ -610,7 +610,7 @@ def test_type_version_boundaries(self): with pytest.raises(UnsupportedFeatureError, match="SET properties"): AlterTypeExpression( PostgresDialect(version=(12, 0, 0)), - "box", + Type(PostgresDialect(version=(12, 0, 0)), 'box'), [PostgresSetTypePropertiesAction( PostgresDialect(version=(12, 0, 0)), {"STORAGE": "plain"}, @@ -629,9 +629,9 @@ def test_base_subscript_version_boundary(self): ) if version < (14, 0, 0): with pytest.raises(UnsupportedFeatureError, match="SUBSCRIPT"): - CreateTypeExpression(dialect, "box", definition).to_sql() + CreateTypeExpression(dialect, Type(dialect, 'box'), definition).to_sql() else: - sql, _ = CreateTypeExpression(dialect, "box", definition).to_sql() + sql, _ = CreateTypeExpression(dialect, Type(dialect, 'box'), definition).to_sql() assert 'SUBSCRIPT = "box_subscript"' in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_protocols.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_protocols.py index 47c23821..cc059302 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_protocols.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_protocols.py @@ -10,7 +10,15 @@ """ import pytest # noqa: F401 -from rhosocial.activerecord.backend.dialect import DomainSupport, UserDefinedTypeSupport +from rhosocial.activerecord.backend.dialect.protocols import ( + AlterDomainSupport, + AlterTypeSupport, + CreateDomainSupport, + CreateTypeSupport, + DropDomainSupport, + DropTypeSupport, + TypeObjectSupport, +) from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.dialect.mixins import DomainMixin, UserDefinedTypeMixin from rhosocial.activerecord.backend.expression.core import Literal @@ -660,20 +668,25 @@ def test_baseline_19(self): class TestPostgresTypeDomainProtocols: def test_protocol_derivation_and_runtime_support(self): - assert issubclass(PostgresTypeSupport, UserDefinedTypeSupport) - assert issubclass(PostgresDomainSupport, DomainSupport) + # Core split the former umbrella protocols into one per statement + # expression, so a backend protocol derives from each it restates. + for generic in (CreateTypeSupport, AlterTypeSupport, DropTypeSupport): + assert issubclass(PostgresTypeSupport, generic) + for generic in (CreateDomainSupport, AlterDomainSupport, DropDomainSupport): + assert issubclass(PostgresDomainSupport, generic) + # Naming a type is separate from declaring one, and both apply here. + assert issubclass(PostgresTypeSupport, TypeObjectSupport) dialect = PostgresDialect(version=(14, 0, 0)) assert isinstance(dialect, PostgresTypeSupport) - assert isinstance(dialect, UserDefinedTypeSupport) + assert isinstance(dialect, TypeObjectSupport) assert isinstance(dialect, PostgresDomainSupport) - assert isinstance(dialect, DomainSupport) def test_postgres_mixins_and_protocols_precede_core_mro(self): mro = PostgresDialect.__mro__ assert mro.index(PostgresTypeMixin) < mro.index(UserDefinedTypeMixin) assert mro.index(PostgresDomainMixin) < mro.index(DomainMixin) - assert mro.index(PostgresTypeSupport) < mro.index(UserDefinedTypeSupport) - assert mro.index(PostgresDomainSupport) < mro.index(DomainSupport) + assert mro.index(PostgresTypeSupport) < mro.index(CreateTypeSupport) + assert mro.index(PostgresDomainSupport) < mro.index(CreateDomainSupport) def test_dialect_initializes_core_validation_state(self) -> None: dialect = PostgresDialect(version=(14, 0, 0)) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_source_collection.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_source_collection.py index c3bbf98b..e52d06d8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_source_collection.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_source_collection.py @@ -20,6 +20,7 @@ ) from rhosocial.activerecord.backend.expression.types import IntegerType from rhosocial.activerecord.backend.expression.types import TextType +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresColumnOptions, @@ -97,7 +98,7 @@ TableConstraintType.FOREIGN_KEY, name="fk_order_tenant", columns=["tenant_id"], - foreign_key_table="tenant", + foreign_key_table=Table(DIALECT, "tenant"), foreign_key_columns=["id"], ), TableConstraint( @@ -379,7 +380,7 @@ def test_collected_table_options_render_with_postgres_dialect(): table_options = SyncDDLSource.table_options() expression = CreateTableExpression( DIALECT, - "ddl_source_orders", + Table(DIALECT, 'ddl_source_orders'), [ColumnDefinition(DIALECT, "id", IntegerType(DIALECT))], table_options=table_options[0], ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extensions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extensions.py index cad58216..56b21282 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extensions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_extensions.py @@ -113,13 +113,13 @@ def test_ltree_index_statement(self): """Test ltree index statement (DDL, still on mixin).""" dialect = PostgresDialect((14, 0, 0)) sql, params = dialect.format_ltree_index_statement('idx_path', 'categories', 'path') - assert "CREATE INDEX idx_path ON categories USING gist (path)" in sql + assert 'CREATE INDEX idx_path ON "categories" USING gist (path)' in sql def test_ltree_index_statement_with_schema(self): """Test ltree index statement with schema.""" dialect = PostgresDialect((14, 0, 0)) sql, params = dialect.format_ltree_index_statement('idx_path', 'categories', 'path', schema='public') - assert "ON public.categories" in sql + assert 'ON "public"."categories"' in sql def test_ltree_index_statement_btree(self): """Test ltree index statement with btree.""" 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 27909d03..5a0b5dbd 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 @@ -7,12 +7,13 @@ - Format CREATE INDEX with PostgreSQL-specific options - Format DROP INDEX with CONCURRENTLY - Format ALTER INDEX -- PostgreSQL-specific IndexSupport protocol methods +- PostgreSQL-specific IndexObjectSupport protocol methods - CreateIndexExpression / DropIndexExpression / PostgresAlterIndexExpression """ import pytest from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( AddIndex, DropIndex as DropIndexAction, @@ -422,7 +423,7 @@ def test_create_index_multiple_columns(self): class TestIndexSupportProtocol: - """Test IndexSupport protocol methods.""" + """Test IndexObjectSupport protocol methods.""" def test_supports_create_index(self): assert PostgresDialect().supports_create_index() is True @@ -565,8 +566,8 @@ def test_format_create_fulltext_index_statement_uses_gin(self): dialect = PostgresDialect() expr = CreateFulltextIndexExpression( dialect, - index_name="idx_ft", - table_name="articles", + index=Index(dialect, 'idx_ft'), + table=Table(dialect, 'articles'), columns=["body"], ) sql, params = expr.to_sql() @@ -580,8 +581,8 @@ def test_format_create_fulltext_index_statement_if_not_exists(self): dialect = PostgresDialect() expr = CreateFulltextIndexExpression( dialect, - index_name="idx_ft", - table_name="articles", + index=Index(dialect, 'idx_ft'), + table=Table(dialect, 'articles'), columns=["body"], if_not_exists=True, ) @@ -592,8 +593,8 @@ def test_format_create_fulltext_index_statement_multi_column(self): dialect = PostgresDialect() expr = CreateFulltextIndexExpression( dialect, - index_name="idx_ft", - table_name="articles", + index=Index(dialect, 'idx_ft'), + table=Table(dialect, 'articles'), columns=["title", "body"], ) sql, _ = expr.to_sql() @@ -605,8 +606,8 @@ def test_format_drop_fulltext_index_statement(self): dialect = PostgresDialect() expr = DropFulltextIndexExpression( dialect, - index_name="idx_ft", - table_name="articles", + index=Index(dialect, 'idx_ft'), + table=Table(dialect, 'articles'), ) sql, _ = expr.to_sql() assert sql.startswith("DROP INDEX") @@ -616,8 +617,8 @@ def test_format_drop_fulltext_index_statement_if_exists(self): dialect = PostgresDialect() expr = DropFulltextIndexExpression( dialect, - index_name="idx_ft", - table_name="articles", + index=Index(dialect, 'idx_ft'), + table=Table(dialect, 'articles'), if_exists=True, ) sql, _ = expr.to_sql() @@ -629,20 +630,20 @@ class TestCreateIndexExpression: def test_basic_create(self): d = PostgresDialect((15, 0, 0)) - expr = CreateIndexExpression(d, "idx_test", "t", ["a"]) + expr = CreateIndexExpression(d, Index(d, 'idx_test'), Table(d, 't'), ["a"]) sql, _ = expr.to_sql() assert sql == 'CREATE INDEX "idx_test" ON "t" ("a")' def test_unique(self): d = PostgresDialect((15, 0, 0)) - expr = CreateIndexExpression(d, "idx_u", "t", ["a"], unique=True) + expr = CreateIndexExpression(d, Index(d, 'idx_u'), Table(d, 't'), ["a"], unique=True) sql, _ = expr.to_sql() assert sql == 'CREATE UNIQUE INDEX "idx_u" ON "t" ("a")' def test_nulls_not_distinct(self): d = PostgresDialect((15, 0, 0)) expr = PostgresCreateIndexExpression( - d, "idx_u", "t", ["a"], unique=True, nulls_not_distinct=True + d, Index(d, 'idx_u'), Table(d, 't'), ["a"], unique=True, nulls_not_distinct=True ) sql, _ = expr.to_sql() assert sql == 'CREATE UNIQUE INDEX "idx_u" ON "t" ("a") NULLS NOT DISTINCT' @@ -650,7 +651,7 @@ def test_nulls_not_distinct(self): def test_nulls_not_distinct_pg14_raises(self): d = PostgresDialect((14, 0, 0)) expr = PostgresCreateIndexExpression( - d, "idx_u", "t", ["a"], unique=True, nulls_not_distinct=True + d, Index(d, 'idx_u'), Table(d, 't'), ["a"], unique=True, nulls_not_distinct=True ) with pytest.raises(ValueError, match="NULLS NOT DISTINCT requires PostgreSQL 15"): expr.to_sql() @@ -658,7 +659,7 @@ def test_nulls_not_distinct_pg14_raises(self): def test_nulls_not_distinct_non_unique_raises(self): d = PostgresDialect((15, 0, 0)) expr = PostgresCreateIndexExpression( - d, "idx_u", "t", ["a"], unique=False, nulls_not_distinct=True + d, Index(d, 'idx_u'), Table(d, 't'), ["a"], unique=False, nulls_not_distinct=True ) with pytest.raises(ValueError, match="NULLS NOT DISTINCT is only valid for UNIQUE"): expr.to_sql() @@ -666,7 +667,7 @@ def test_nulls_not_distinct_non_unique_raises(self): def test_concurrent_and_nulls_not_distinct_pg15_raises(self): d = PostgresDialect((15, 0, 0)) expr = PostgresCreateIndexExpression( - d, "idx_u", "t", ["a"], + d, Index(d, 'idx_u'), Table(d, 't'), ["a"], unique=True, concurrent=True, nulls_not_distinct=True, ) with pytest.raises(ValueError, match="CONCURRENTLY.*NULLS NOT DISTINCT.*PostgreSQL 16"): @@ -675,7 +676,7 @@ def test_concurrent_and_nulls_not_distinct_pg15_raises(self): def test_concurrent_and_nulls_not_distinct_pg16(self): d = PostgresDialect((16, 0, 0)) expr = PostgresCreateIndexExpression( - d, "idx_u", "t", ["a"], + d, Index(d, 'idx_u'), Table(d, 't'), ["a"], unique=True, concurrent=True, nulls_not_distinct=True, ) sql, _ = expr.to_sql() @@ -688,31 +689,31 @@ class TestDropIndexExpression: def test_basic_drop(self): d = PostgresDialect((18, 0, 0)) - expr = DropIndexExpression(d, "idx_test") + expr = DropIndexExpression(d, Index(d, 'idx_test')) sql, _ = expr.to_sql() assert sql == 'DROP INDEX "idx_test"' def test_drop_if_exists(self): d = PostgresDialect((18, 0, 0)) - expr = DropIndexExpression(d, "idx_test", if_exists=True) + expr = DropIndexExpression(d, Index(d, 'idx_test'), if_exists=True) sql, _ = expr.to_sql() assert sql == 'DROP INDEX IF EXISTS "idx_test"' def test_drop_concurrently(self): d = PostgresDialect((18, 0, 0)) - expr = DropIndexExpression(d, "idx_test", concurrent=True) + expr = DropIndexExpression(d, Index(d, 'idx_test'), concurrent=True) sql, _ = expr.to_sql() assert sql == 'DROP INDEX CONCURRENTLY "idx_test"' def test_drop_concurrently_pg17_raises(self): d = PostgresDialect((17, 0, 0)) - expr = DropIndexExpression(d, "idx_test", concurrent=True) + expr = DropIndexExpression(d, Index(d, 'idx_test'), concurrent=True) with pytest.raises(ValueError, match="DROP INDEX CONCURRENTLY requires PostgreSQL 18"): expr.to_sql() def test_drop_concurrently_if_exists(self): d = PostgresDialect((18, 0, 0)) - expr = DropIndexExpression(d, "idx_test", if_exists=True, concurrent=True) + expr = DropIndexExpression(d, Index(d, 'idx_test'), if_exists=True, concurrent=True) sql, _ = expr.to_sql() assert sql == 'DROP INDEX CONCURRENTLY IF EXISTS "idx_test"' @@ -821,7 +822,7 @@ def test_format_add_index_action_raises(self): def test_format_drop_index_action_raises(self): d = PostgresDialect() - drop = DropIndexAction(d, index_name="idx_test") + drop = DropIndexAction(d, index=Index(d, "idx_test")) with pytest.raises(UnsupportedFeatureError, match="ALTER TABLE DROP INDEX"): drop.to_sql() @@ -850,32 +851,32 @@ class TestCreateIndexExpressionAllOptions: def test_concurrent(self): d = PostgresDialect((11, 0, 0)) - expr = CreateIndexExpression(d, "idx_c", "t", ["a"], concurrent=True) + expr = CreateIndexExpression(d, Index(d, 'idx_c'), Table(d, 't'), ["a"], concurrent=True) sql, _ = expr.to_sql() assert "CONCURRENTLY" in sql def test_concurrent_pg10_raises(self): d = PostgresDialect((10, 0, 0)) - expr = CreateIndexExpression(d, "idx_c", "t", ["a"], concurrent=True) + expr = CreateIndexExpression(d, Index(d, 'idx_c'), Table(d, 't'), ["a"], concurrent=True) with pytest.raises(ValueError, match="CONCURRENTLY requires PostgreSQL 11"): expr.to_sql() def test_if_not_exists(self): d = PostgresDialect() - expr = CreateIndexExpression(d, "idx_t", "t", ["a"], if_not_exists=True) + expr = CreateIndexExpression(d, Index(d, 'idx_t'), Table(d, 't'), ["a"], if_not_exists=True) sql, _ = expr.to_sql() assert "IF NOT EXISTS" in sql def test_index_type(self): d = PostgresDialect() - expr = CreateIndexExpression(d, "idx_h", "t", ["a"], index_type="hash") + expr = CreateIndexExpression(d, Index(d, 'idx_h'), Table(d, 't'), ["a"], index_type="hash") sql, _ = expr.to_sql() assert "USING hash" in sql def test_include(self): d = PostgresDialect((12, 0, 0)) expr = CreateIndexExpression( - d, "idx_i", "t", ["a"], include=["b", "c"] + d, Index(d, 'idx_i'), Table(d, 't'), ["a"], include=["b", "c"] ) sql, _ = expr.to_sql() assert 'INCLUDE ("b", "c")' in sql @@ -883,7 +884,7 @@ def test_include(self): def test_tablespace(self): d = PostgresDialect() expr = CreateIndexExpression( - d, "idx_t", "t", ["a"], tablespace="fast_ts" + d, Index(d, 'idx_t'), Table(d, 't'), ["a"], tablespace="fast_ts" ) sql, _ = expr.to_sql() assert 'TABLESPACE "fast_ts"' in sql @@ -893,7 +894,7 @@ def test_where(self): from rhosocial.activerecord.backend.expression import Literal from rhosocial.activerecord.backend.expression import Column expr = CreateIndexExpression( - d, "idx_w", "t", ["a"], + d, Index(d, 'idx_w'), Table(d, 't'), ["a"], where=Column(d, "status") == Literal(d, 1), ) sql, params = expr.to_sql() @@ -904,7 +905,7 @@ def test_where(self): def test_with_options(self): d = PostgresDialect() expr = PostgresCreateIndexExpression( - d, "idx_w", "t", ["a"], + d, Index(d, 'idx_w'), Table(d, 't'), ["a"], with_options={"fillfactor": 70}, ) sql, _ = expr.to_sql() @@ -913,7 +914,7 @@ def test_with_options(self): def test_opclasses(self): d = PostgresDialect() expr = PostgresCreateIndexExpression( - d, "idx_o", "t", ["a", "b"], + d, Index(d, 'idx_o'), Table(d, 't'), ["a", "b"], opclasses={"a": "text_pattern_ops"}, ) sql, _ = expr.to_sql() @@ -1040,14 +1041,14 @@ 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 d = PostgresDialect((15, 0, 0)) - expr = CreateIndexExpression(d, "idx_e", "t", [Column(d, "a")]) + expr = CreateIndexExpression(d, Index(d, 'idx_e'), Table(d, 't'), [Column(d, "a")]) sql, _ = expr.to_sql() assert '"a"' in sql def test_include_gist_low_raises(self): d = PostgresDialect((11, 0, 0)) expr = CreateIndexExpression( - d, "idx_g", "t", ["a"], include=["b"], index_type="gist", + d, Index(d, 'idx_g'), Table(d, 't'), ["a"], include=["b"], index_type="gist", ) with pytest.raises(ValueError, match="INCLUDE for GiST"): expr.to_sql() @@ -1055,7 +1056,7 @@ def test_include_gist_low_raises(self): def test_include_spgist_low_raises(self): d = PostgresDialect((12, 0, 0)) expr = CreateIndexExpression( - d, "idx_s", "t", ["a"], include=["b"], index_type="spgist", + d, Index(d, 'idx_s'), Table(d, 't'), ["a"], include=["b"], index_type="spgist", ) with pytest.raises(ValueError, match="INCLUDE for SP-GiST"): expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py index f41e6286..09da602f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py @@ -10,6 +10,7 @@ import pytest from rhosocial.activerecord.backend.errors import DatabaseError +from rhosocial.activerecord.backend.expression.objects import MaterializedView from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresAlterMaterializedViewExpression, @@ -75,7 +76,7 @@ def test_with_no_data_is_unqueryable_until_refresh(self, mv_backend, daily_query """T-44.""" expression = PostgresCreateMaterializedViewExpression( dialect=mv_backend.dialect, - view_name="mv_sales_daily", + view=MaterializedView(mv_backend.dialect, "mv_sales_daily"), query=daily_query(), with_data=False, ) @@ -88,7 +89,7 @@ def test_with_no_data_is_unqueryable_until_refresh(self, mv_backend, daily_query assert "has not been populated" in str(exc.value) refresh = PostgresRefreshMaterializedViewExpression( - dialect=mv_backend.dialect, name="mv_sales_daily", with_data=True + dialect=mv_backend.dialect, view=MaterializedView(mv_backend.dialect, "mv_sales_daily"), with_data=True ) sql, params = refresh.to_sql() mv_backend.execute(sql, params) @@ -104,7 +105,7 @@ def test_refresh_picks_up_new_rows(self, mv_backend, create_summary_mv): "INSERT INTO mv_sales (product_id, amount, sale_date) VALUES (3, 10.00, '2024-02-01')" ) refresh = PostgresRefreshMaterializedViewExpression( - dialect=mv_backend.dialect, name="mv_sales_summary" + dialect=mv_backend.dialect, view=MaterializedView(mv_backend.dialect, "mv_sales_summary") ) sql, params = refresh.to_sql() mv_backend.execute(sql, params) @@ -115,7 +116,7 @@ def test_concurrent_refresh_requires_unique_index(self, mv_backend, create_summa """T-43: without a UNIQUE index the server must reject CONCURRENTLY.""" create_summary_mv() refresh = PostgresRefreshMaterializedViewExpression( - dialect=mv_backend.dialect, name="mv_sales_summary", concurrently=True + dialect=mv_backend.dialect, view=MaterializedView(mv_backend.dialect, "mv_sales_summary"), concurrently=True ) sql, params = refresh.to_sql() with pytest.raises(DatabaseError) as exc: @@ -136,8 +137,7 @@ def test_lifecycle_in_non_public_schema(self, mv_backend, create_summary_mv): """T-46.""" mv_backend.execute("CREATE SCHEMA IF NOT EXISTS mv_reporting", options=DDL) create_summary_mv( - view_name="sales_summary", - schema="mv_reporting", + view=MaterializedView(mv_backend.dialect, "sales_summary", schema_name="mv_reporting"), column_aliases=["product_id", "sale_count", "total_amount"], ) result = _query(mv_backend, "SELECT product_id FROM mv_reporting.sales_summary ORDER BY product_id") @@ -145,8 +145,7 @@ def test_lifecycle_in_non_public_schema(self, mv_backend, create_summary_mv): alter = PostgresAlterMaterializedViewExpression( dialect=mv_backend.dialect, - view_name="sales_summary", - schema="mv_reporting", + view=MaterializedView(mv_backend.dialect, "sales_summary", schema_name="mv_reporting"), actions=[PostgresSetMaterializedViewSchemaAction(mv_backend.dialect, "public")], ) sql, params = alter.to_sql() @@ -162,7 +161,7 @@ def test_rename(self, mv_backend, create_summary_mv): create_summary_mv() alter = PostgresAlterMaterializedViewExpression( dialect=mv_backend.dialect, - view_name="mv_sales_summary", + view=MaterializedView(mv_backend.dialect, "mv_sales_summary"), actions=[PostgresRenameMaterializedViewAction(mv_backend.dialect, "mv_sales_renamed")], ) sql, params = alter.to_sql() @@ -175,7 +174,7 @@ def test_set_storage_properties(self, mv_backend, create_summary_mv): create_summary_mv() alter = PostgresAlterMaterializedViewExpression( dialect=mv_backend.dialect, - view_name="mv_sales_summary", + view=MaterializedView(mv_backend.dialect, "mv_sales_summary"), actions=[ PostgresSetMaterializedViewPropertiesAction( mv_backend.dialect, {PostgresStorageParameter.FILLFACTOR: 85} @@ -190,7 +189,7 @@ def test_owner_to_is_accepted(self, mv_backend, create_summary_mv): create_summary_mv() alter = PostgresAlterMaterializedViewExpression( dialect=mv_backend.dialect, - view_name="mv_sales_summary", + view=MaterializedView(mv_backend.dialect, "mv_sales_summary"), actions=[PostgresChangeMaterializedViewOwnerAction(mv_backend.dialect, "CURRENT_USER")], ) sql, params = alter.to_sql() @@ -203,7 +202,7 @@ def test_list_reports_populated_flag(self, mv_backend, create_summary_mv, daily_ create_summary_mv() empty = PostgresCreateMaterializedViewExpression( dialect=mv_backend.dialect, - view_name="mv_sales_daily", + view=MaterializedView(mv_backend.dialect, "mv_sales_daily"), query=daily_query(), with_data=False, ) @@ -262,7 +261,10 @@ def test_table_list_filters_materialized_view(self, mv_backend, create_summary_m def test_drop_cascade(self, mv_backend, create_summary_mv): create_summary_mv() drop = PostgresDropMaterializedViewExpression( - dialect=mv_backend.dialect, view_name="mv_sales_summary", if_exists=True, cascade=True + dialect=mv_backend.dialect, + view=MaterializedView(mv_backend.dialect, "mv_sales_summary"), + if_exists=True, + cascade=True, ) sql, params = drop.to_sql() mv_backend.execute(sql, params, options=DDL) @@ -277,7 +279,7 @@ async def test_create_refresh_drop(self, async_mv_backend, summary_query): dialect = async_mv_backend.dialect create = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv_sales_summary", + view=MaterializedView(dialect, "mv_sales_summary"), query=summary_query(), column_aliases=["product_id", "sale_count", "total_amount"], ) @@ -293,7 +295,7 @@ async def test_create_refresh_drop(self, async_mv_backend, summary_query): "INSERT INTO mv_sales (product_id, amount, sale_date) VALUES (3, 10.00, '2024-02-01')" ) refresh = PostgresRefreshMaterializedViewExpression( - dialect=dialect, name="mv_sales_summary" + dialect=dialect, view=MaterializedView(dialect, "mv_sales_summary") ) sql, params = refresh.to_sql() await async_mv_backend.execute(sql, params) @@ -304,7 +306,10 @@ async def test_create_refresh_drop(self, async_mv_backend, summary_query): assert result.data[0]["count"] == 3 drop = PostgresDropMaterializedViewExpression( - dialect=dialect, view_name="mv_sales_summary", if_exists=True, cascade=True + dialect=dialect, + view=MaterializedView(dialect, "mv_sales_summary"), + if_exists=True, + cascade=True, ) sql, params = drop.to_sql() await async_mv_backend.execute(sql, params, options=DDL) @@ -313,7 +318,7 @@ async def test_list_materialized_views(self, async_mv_backend, summary_query): dialect = async_mv_backend.dialect create = PostgresCreateMaterializedViewExpression( dialect=dialect, - view_name="mv_sales_summary", + view=MaterializedView(dialect, "mv_sales_summary"), query=summary_query(), ) sql, params = create.to_sql() @@ -331,7 +336,10 @@ async def test_teardown_leaves_no_residue(self, async_mv_backend, daily_query): """T-51: fixture teardown must remove every object it created.""" dialect = async_mv_backend.dialect create = PostgresCreateMaterializedViewExpression( - dialect=dialect, view_name="mv_sales_daily", query=daily_query(), with_data=False + dialect=dialect, + view=MaterializedView(dialect, "mv_sales_daily"), + query=daily_query(), + with_data=False, ) sql, params = create.to_sql() await async_mv_backend.execute(sql, params, options=DDL) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_model_truncate.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_model_truncate.py index 2a91b48c..53fda6d7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_model_truncate.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_model_truncate.py @@ -1,6 +1,7 @@ # tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_model_truncate.py """PostgreSQL rendering tests for model-bound TRUNCATE expressions.""" +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements.ddl_truncate import ( TruncateExpression, ) @@ -13,13 +14,13 @@ class TestSyncModelTruncate: def test_truncate_expression_renders_postgres_options(self, postgres_backend): expression = TruncateExpression( postgres_backend.dialect, - TABLE, + Table(postgres_backend.dialect, TABLE), restart_identity=True, cascade=True, ) assert isinstance(expression, TruncateExpression) - assert expression.table_name == TABLE + assert expression.table.name == TABLE assert expression.restart_identity is True assert expression.cascade is True assert expression.to_sql() == ( @@ -35,13 +36,13 @@ async def test_truncate_expression_renders_postgres_options( ): expression = TruncateExpression( async_postgres_backend.dialect, - TABLE, + Table(async_postgres_backend.dialect, TABLE), restart_identity=True, cascade=True, ) assert isinstance(expression, TruncateExpression) - assert expression.table_name == TABLE + assert expression.table.name == TABLE assert expression.restart_identity is True assert expression.cascade is True assert expression.to_sql() == ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py index 3e9cb636..fdfbc16f 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py @@ -13,8 +13,8 @@ Literal, QueryExpression, RawSQLExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.serialization import deserialize, serialize from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend @@ -135,7 +135,7 @@ def select_query(dialect): return QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "users"), + from_=Table(dialect, "users"), where=Column(dialect, "id") > Literal(dialect, 3), ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py index 80ff0cc9..f95688a7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py @@ -3,7 +3,8 @@ import pytest -from rhosocial.activerecord.backend.expression import Column, Literal, QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression import Column, Literal, QueryExpression +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCopyFromExpression, PostgresCopyToExpression, @@ -39,7 +40,7 @@ def test_live_copy_round_trip_and_native_repack(postgres_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), where=Column(dialect, "id") > Literal(dialect, 1), ) assert postgres_backend.copy_to(PostgresCopyToExpression(dialect, query=query)) == b"2\tBob\n" @@ -103,7 +104,7 @@ async def test_live_async_copy_round_trip_and_native_repack(async_postgres_backe query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, TABLE_NAME), + from_=Table(dialect, TABLE_NAME), where=Column(dialect, "id") > Literal(dialect, 1), ) copied_query = await async_postgres_backend.copy_to(PostgresCopyToExpression(dialect, query=query)) 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 3ea2bb80..b18b9b95 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 @@ -17,17 +17,17 @@ from rhosocial.activerecord.backend.dialect.protocols import GraphTableSupport from rhosocial.activerecord.backend.expression import ( - GraphVertex, GraphEdge, GraphEdgeDirection, MatchClause, - GraphColumn, ColumnsClause, GraphTableExpression, - TablePropertiesClause, VertexTable, EdgeTable, - CreatePropertyGraphExpression, DropPropertyGraphExpression, - CreateTableExpression, DropTableExpression, ColumnDefinition, - ColumnConstraint, ColumnConstraintType, - InsertExpression, ValuesSource, Literal, - QueryExpression, WildcardExpression, - FunctionCall, TableExpression, - ExplainExpression, ExplainOptions, + AlterPropertyGraphExpression, ColumnsClause, ColumnConstraint, + ColumnConstraintType, ColumnDefinition, CreatePropertyGraphExpression, + CreateTableExpression, DropPropertyGraphExpression, DropTableExpression, + ExplainExpression, ExplainOptions, FunctionCall, GraphColumn, GraphEdge, + GraphEdgeDirection, GraphTableExpression, GraphVertex, InsertExpression, + Literal, MatchClause, QueryExpression, TablePropertiesClause, + ValuesSource, VertexTable, EdgeTable, WildcardExpression, ) +from rhosocial.activerecord.backend.expression.sources import NamedRelationRef +from rhosocial.activerecord.backend.expression.objects import EdgeTable as EdgeTableObject +from rhosocial.activerecord.backend.expression.objects import NodeTable, Table from rhosocial.activerecord.backend.expression.query_parts import ( WhereClause, OrderByClause, GroupByHavingClause, LimitOffsetClause, ) @@ -49,7 +49,7 @@ def social_data(postgres_backend): pytest.skip("PGQ not supported in this PostgreSQL version") for t in ("likes", "posts", "follows", "people"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) people_cols = [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), @@ -58,7 +58,7 @@ def social_data(postgres_backend): ColumnDefinition(dialect, "email", TextType(dialect=dialect)), ColumnDefinition(dialect, "city", TextType(dialect=dialect)), ] - backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'people'), people_cols).to_sql()) follows_cols = [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), @@ -71,7 +71,7 @@ def social_data(postgres_backend): foreign_key_reference=("people", ["id"]))]), ColumnDefinition(dialect, "since", TextType(dialect=dialect)), ] - backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'follows'), follows_cols).to_sql()) posts_cols = [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), @@ -82,7 +82,7 @@ def social_data(postgres_backend): ColumnDefinition(dialect, "content", TextType(dialect=dialect)), ColumnDefinition(dialect, "created_at", TextType(dialect=dialect)), ] - backend.execute(*CreateTableExpression(dialect, "posts", posts_cols).to_sql()) + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'posts'), posts_cols).to_sql()) likes_cols = [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), @@ -95,7 +95,7 @@ def social_data(postgres_backend): foreign_key_reference=("posts", ["id"]))]), ColumnDefinition(dialect, "created_at", TextType(dialect=dialect)), ] - backend.execute(*CreateTableExpression(dialect, "likes", likes_cols).to_sql()) + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'likes'), likes_cols).to_sql()) people_data = ValuesSource(dialect, [ [ @@ -129,7 +129,7 @@ def social_data(postgres_backend): Literal(dialect, "LA"), ], ]) - backend.execute(*InsertExpression(dialect, "people", source=people_data).to_sql()) + backend.execute(*InsertExpression(dialect, Table(dialect, "people"), source=people_data).to_sql()) follows_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2), Literal(dialect, "2024-01-01")], @@ -138,7 +138,7 @@ def social_data(postgres_backend): [Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, 1), Literal(dialect, "2024-04-01")], [Literal(dialect, 5), Literal(dialect, 3), Literal(dialect, 5), Literal(dialect, "2024-05-01")], ]) - backend.execute(*InsertExpression(dialect, "follows", source=follows_data).to_sql()) + backend.execute(*InsertExpression(dialect, Table(dialect, "follows"), source=follows_data).to_sql()) posts_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 2), Literal(dialect, "Hello world"), Literal(dialect, "2024-06-01")], @@ -146,7 +146,7 @@ def social_data(postgres_backend): [Literal(dialect, 3), Literal(dialect, 3), Literal(dialect, "Graph databases"), Literal(dialect, "2024-06-03")], [Literal(dialect, 4), Literal(dialect, 1), Literal(dialect, "My first post"), Literal(dialect, "2024-06-04")], ]) - backend.execute(*InsertExpression(dialect, "posts", source=posts_data).to_sql()) + backend.execute(*InsertExpression(dialect, Table(dialect, "posts"), source=posts_data).to_sql()) likes_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, "2024-06-02")], @@ -155,24 +155,24 @@ def social_data(postgres_backend): [Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, 4), Literal(dialect, "2024-06-05")], [Literal(dialect, 5), Literal(dialect, 5), Literal(dialect, 1), Literal(dialect, "2024-06-06")], ]) - backend.execute(*InsertExpression(dialect, "likes", source=likes_data).to_sql()) + backend.execute(*InsertExpression(dialect, Table(dialect, "likes"), source=likes_data).to_sql()) - vt_people = VertexTable(dialect, "people", + vt_people = VertexTable(dialect, NodeTable(dialect, 'people'), labels=["person"], properties=TablePropertiesClause(dialect, columns=["id", "name", "city"])) - vt_posts = VertexTable(dialect, "posts", + vt_posts = VertexTable(dialect, NodeTable(dialect, 'posts'), labels=["post"], properties=TablePropertiesClause(dialect, columns=["id", "content"])) - et_follows = EdgeTable(dialect, "follows", ["follower_id"], ["followed_id"], + et_follows = EdgeTable(dialect, EdgeTableObject(dialect, 'follows'), ["follower_id"], ["followed_id"], references_source=("people", ["id"]), references_destination=("people", ["id"]), labels=["follows"]) - et_posts = EdgeTable(dialect, "posts", ["author_id"], ["id"], + et_posts = EdgeTable(dialect, EdgeTableObject(dialect, 'posts'), ["author_id"], ["id"], references_source=("people", ["id"]), references_destination=("posts", ["id"]), labels=["authored"], alias="authored") - et_likes = EdgeTable(dialect, "likes", ["user_id"], ["post_id"], + et_likes = EdgeTable(dialect, EdgeTableObject(dialect, 'likes'), ["user_id"], ["post_id"], references_source=("people", ["id"]), references_destination=("posts", ["id"]), labels=["likes"]) @@ -183,14 +183,14 @@ def social_data(postgres_backend): backend.execute(*create_expr.to_sql()) except Exception as e: for t in ("likes", "posts", "follows", "people"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) raise e yield GRAPH_NAME backend.execute(*DropPropertyGraphExpression(dialect, GRAPH_NAME, if_exists=True).to_sql()) for t in ("likes", "posts", "follows", "people"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) class TestSocialGraph: @@ -203,14 +203,14 @@ def test_single_hop_followers(self, postgres_backend, social_data): a = GraphVertex( dialect, "a", - "person", + NodeTable(dialect, 'person'), where=WhereClause( dialect, condition=Column(dialect, "name", table="a") == Literal(dialect, "Alice"), ), ) - f = GraphEdge(dialect, "f", "follows", GraphEdgeDirection.RIGHT) - b = GraphVertex(dialect, "b", "person") + f = GraphEdge(dialect, "f", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + b = GraphVertex(dialect, "b", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, f, b) cols = ColumnsClause(dialect, GraphColumn("b", "name", "b_name")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") @@ -231,16 +231,16 @@ def test_two_hop_recommendation(self, postgres_backend, social_data): a = GraphVertex( dialect, "a", - "person", + NodeTable(dialect, 'person'), where=WhereClause( dialect, condition=Column(dialect, "name", table="a") == Literal(dialect, "Alice"), ), ) - f1 = GraphEdge(dialect, "f1", "follows", GraphEdgeDirection.RIGHT) - b = GraphVertex(dialect, "b", "person") - f2 = GraphEdge(dialect, "f2", "follows", GraphEdgeDirection.RIGHT) - c = GraphVertex(dialect, "c", "person") + f1 = GraphEdge(dialect, "f1", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + b = GraphVertex(dialect, "b", NodeTable(dialect, 'person')) + f2 = GraphEdge(dialect, "f2", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + c = GraphVertex(dialect, "c", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, f1, b, f2, c) cols = ColumnsClause(dialect, GraphColumn("c", "name", "c_name")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") @@ -261,16 +261,16 @@ def test_likes_on_posts(self, postgres_backend, social_data): a = GraphVertex( dialect, "a", - "person", + NodeTable(dialect, 'person'), where=WhereClause( dialect, condition=Column(dialect, "name", table="a") == Literal(dialect, "Alice"), ), ) - p = GraphEdge(dialect, "p", "authored", GraphEdgeDirection.RIGHT) - post = GraphVertex(dialect, "post", "post") - like_edge = GraphEdge(dialect, "l", "likes", GraphEdgeDirection.LEFT) - liker = GraphVertex(dialect, "liker", "person") + p = GraphEdge(dialect, "p", EdgeTable(dialect, EdgeTableObject(dialect, 'authored')), GraphEdgeDirection.RIGHT) + post = GraphVertex(dialect, "post", NodeTable(dialect, 'post')) + like_edge = GraphEdge(dialect, "l", EdgeTable(dialect, EdgeTableObject(dialect, 'likes')), GraphEdgeDirection.LEFT) + liker = GraphVertex(dialect, "liker", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, p, post, like_edge, liker) cols = ColumnsClause(dialect, GraphColumn("liker", "name", "liker_name")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") @@ -287,11 +287,11 @@ def test_likes_on_posts(self, postgres_backend, social_data): def test_graph_table_with_group_by(self, postgres_backend, social_data): """GRAPH_TABLE + GROUP BY.""" dialect = postgres_backend.dialect - a = GraphVertex(dialect, "a", "person") - p = GraphEdge(dialect, "p", "authored", GraphEdgeDirection.RIGHT) - post = GraphVertex(dialect, "post", "post") - like_edge = GraphEdge(dialect, "l", "likes", GraphEdgeDirection.LEFT) - liker = GraphVertex(dialect, "liker", "person") + a = GraphVertex(dialect, "a", NodeTable(dialect, 'person')) + p = GraphEdge(dialect, "p", EdgeTable(dialect, EdgeTableObject(dialect, 'authored')), GraphEdgeDirection.RIGHT) + post = GraphVertex(dialect, "post", NodeTable(dialect, 'post')) + like_edge = GraphEdge(dialect, "l", EdgeTable(dialect, EdgeTableObject(dialect, 'likes')), GraphEdgeDirection.LEFT) + liker = GraphVertex(dialect, "liker", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, p, post, like_edge, liker) cols = ColumnsClause(dialect, GraphColumn("a", "name", "author"), @@ -319,9 +319,9 @@ class TestCommerceGraph: def test_graph_table_combined_with_regular_sql(self, postgres_backend, social_data): """Composite: GRAPH_TABLE + JOIN + ORDER BY.""" dialect = postgres_backend.dialect - a = GraphVertex(dialect, "a", "person") - f = GraphEdge(dialect, "f", "follows", GraphEdgeDirection.RIGHT) - b = GraphVertex(dialect, "b", "person") + a = GraphVertex(dialect, "a", NodeTable(dialect, 'person')) + f = GraphEdge(dialect, "f", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + b = GraphVertex(dialect, "b", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, f, b) cols = ColumnsClause(dialect, GraphColumn("a", "name", "follower"), @@ -331,7 +331,7 @@ def test_graph_table_combined_with_regular_sql(self, postgres_backend, social_da join = JoinClause(dialect, left_table=gt, - right_table=TableExpression(dialect, "people", alias="p"), + right_table=NamedRelationRef(dialect, Table(dialect, "people"), alias="p"), join_type="INNER JOIN", condition=Column(dialect, "follower", "g") == Column(dialect, "name", "p")) @@ -351,14 +351,14 @@ def test_parameter_binding(self, postgres_backend, social_data): a = GraphVertex( dialect, "a", - "person", + NodeTable(dialect, 'person'), where=WhereClause( dialect, condition=Column(dialect, "name", table="a") == Literal(dialect, "Alice"), ), ) - f = GraphEdge(dialect, "f", "follows", GraphEdgeDirection.RIGHT) - b = GraphVertex(dialect, "b", "person") + f = GraphEdge(dialect, "f", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + b = GraphVertex(dialect, "b", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, f, b) cols = ColumnsClause(dialect, GraphColumn("b", "name", "b_name")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") @@ -384,7 +384,7 @@ async def async_social_data(self, async_postgres_backend): pytest.skip("PGQ not supported") for t in ("likes", "posts", "follows", "people"): - await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + await backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) people_cols = [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), @@ -393,7 +393,7 @@ async def async_social_data(self, async_postgres_backend): ColumnDefinition(dialect, "email", TextType(dialect=dialect)), ColumnDefinition(dialect, "city", TextType(dialect=dialect)), ] - await backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) + await backend.execute(*CreateTableExpression(dialect, Table(dialect, 'people'), people_cols).to_sql()) follows_cols = [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), @@ -406,21 +406,21 @@ async def async_social_data(self, async_postgres_backend): foreign_key_reference=("people", ["id"]))]), ColumnDefinition(dialect, "since", TextType(dialect=dialect)), ] - await backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) + await backend.execute(*CreateTableExpression(dialect, Table(dialect, 'follows'), follows_cols).to_sql()) people_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, "Alice"), Literal(dialect, "a@x.com"), Literal(dialect, "NYC")], [Literal(dialect, 2), Literal(dialect, "Bob"), Literal(dialect, "b@x.com"), Literal(dialect, "NYC")], ]) - await backend.execute(*InsertExpression(dialect, "people", source=people_data).to_sql()) + await backend.execute(*InsertExpression(dialect, Table(dialect, "people"), source=people_data).to_sql()) follows_data = ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2), Literal(dialect, "2024-01-01")], ]) - await backend.execute(*InsertExpression(dialect, "follows", source=follows_data).to_sql()) + await backend.execute(*InsertExpression(dialect, Table(dialect, "follows"), source=follows_data).to_sql()) - vt = VertexTable(dialect, "people", labels=["person"]) - et = EdgeTable(dialect, "follows", ["follower_id"], ["followed_id"], + vt = VertexTable(dialect, NodeTable(dialect, 'people'), labels=["person"]) + et = EdgeTable(dialect, EdgeTableObject(dialect, 'follows'), ["follower_id"], ["followed_id"], references_source=("people", ["id"]), references_destination=("people", ["id"]), labels=["follows"]) @@ -429,7 +429,7 @@ async def async_social_data(self, async_postgres_backend): yield GRAPH_NAME await backend.execute(*DropPropertyGraphExpression(dialect, GRAPH_NAME, if_exists=True).to_sql()) for t in ("follows", "people"): - await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + await backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) @pytest.mark.requires_protocol((GraphTableSupport, "supports_graph_table")) @pytest.mark.asyncio @@ -438,14 +438,14 @@ async def test_async_single_hop(self, async_postgres_backend, async_social_data) a = GraphVertex( dialect, "a", - "person", + NodeTable(dialect, 'person'), where=WhereClause( dialect, condition=Column(dialect, "name", table="a") == Literal(dialect, "Alice"), ), ) - f = GraphEdge(dialect, "f", "follows", GraphEdgeDirection.RIGHT) - b = GraphVertex(dialect, "b", "person") + f = GraphEdge(dialect, "f", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + b = GraphVertex(dialect, "b", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, f, b) cols = ColumnsClause(dialect, GraphColumn("b", "name", "b_name")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") @@ -465,9 +465,9 @@ class TestPGQExplain: @pytest.mark.requires_protocol((GraphTableSupport, "supports_graph_table")) def test_explain_graph_table(self, postgres_backend, social_data): dialect = postgres_backend.dialect - a = GraphVertex(dialect, "a", "person") - f = GraphEdge(dialect, "f", "follows", GraphEdgeDirection.RIGHT) - b = GraphVertex(dialect, "b", "person") + a = GraphVertex(dialect, "a", NodeTable(dialect, 'person')) + f = GraphEdge(dialect, "f", EdgeTable(dialect, EdgeTableObject(dialect, 'follows')), GraphEdgeDirection.RIGHT) + b = GraphVertex(dialect, "b", NodeTable(dialect, 'person')) match = MatchClause(dialect, a, f, b) cols = ColumnsClause(dialect, GraphColumn("b", "name", "b_name")) gt = GraphTableExpression(dialect, GRAPH_NAME, match, cols, alias="g") 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 6ce11a75..03bde4e2 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 @@ -19,7 +19,6 @@ ValuesSource, DropTableExpression, QueryExpression, - TableExpression, CTEExpression, WithQueryExpression, SetOperationExpression, @@ -28,6 +27,8 @@ ColumnConstraint, ColumnConstraintType, ) +from rhosocial.activerecord.backend.expression.sources import NamedRelationRef +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.types import ( IntegerType, VarCharType, @@ -50,22 +51,22 @@ def social_network_data(postgres_backend): dialect = backend.dialect for t in ("follows", "users"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) - backend.execute(*CreateTableExpression(dialect, "users", [ + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'users'), [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(length=100, dialect=dialect)), ]).to_sql()) - backend.execute(*CreateTableExpression(dialect, "follows", [ + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'follows'), [ 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"], + backend.execute(*InsertExpression(dialect, Table(dialect, "users"), columns=["id", "name"], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, "Alice")], [Literal(dialect, 2), Literal(dialect, "Bob")], @@ -74,7 +75,7 @@ def social_network_data(postgres_backend): [Literal(dialect, 5), Literal(dialect, "Eve")], ])).to_sql()) - backend.execute(*InsertExpression(dialect, "follows", columns=["id", "follower_id", "followed_id"], + backend.execute(*InsertExpression(dialect, Table(dialect, "follows"), columns=["id", "follower_id", "followed_id"], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], [Literal(dialect, 2), Literal(dialect, 2), Literal(dialect, 3)], @@ -85,7 +86,7 @@ def social_network_data(postgres_backend): yield for t in ("follows", "users"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) @pytest.fixture @@ -95,16 +96,16 @@ def aml_data(postgres_backend): dialect = backend.dialect for t in ("transactions", "accounts"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) - backend.execute(*CreateTableExpression(dialect, "accounts", [ + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'accounts'), [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), 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", [ + backend.execute(*CreateTableExpression(dialect, Table(dialect, 'transactions'), [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "source_account_id", IntegerType(dialect=dialect)), @@ -112,7 +113,7 @@ def aml_data(postgres_backend): ColumnDefinition(dialect, "amount", DecimalType(precision=12, scale=2, dialect=dialect)), ]).to_sql()) - backend.execute(*InsertExpression(dialect, "accounts", columns=["id", "account_holder", "account_type"], + backend.execute(*InsertExpression(dialect, Table(dialect, "accounts"), columns=["id", "account_holder", "account_type"], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, "Alice Smith"), Literal(dialect, "checking")], [Literal(dialect, 2), Literal(dialect, "Bob Corp"), Literal(dialect, "business")], @@ -121,7 +122,7 @@ def aml_data(postgres_backend): [Literal(dialect, 5), Literal(dialect, "Eve Holding"), Literal(dialect, "offshore")], ])).to_sql()) - backend.execute(*InsertExpression(dialect, "transactions", columns=[ + backend.execute(*InsertExpression(dialect, Table(dialect, "transactions"), columns=[ "id", "source_account_id", "target_account_id", "amount"], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 4), Literal(dialect, 3), Literal(dialect, 500000)], @@ -133,7 +134,7 @@ def aml_data(postgres_backend): yield for t in ("transactions", "accounts"): - backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) class TestSocialNetworkTraversal: @@ -148,18 +149,18 @@ def _build_traversal_cte(self, dialect): Column(dialect, "name"), Literal(dialect, 0).as_("depth"), ], - from_=TableExpression(dialect, "users"), + from_=Table(dialect, "users"), where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), ) recursive_join = JoinClause( dialect=dialect, - left_table=TableExpression(dialect, "traversal", alias="t"), - right_table=TableExpression(dialect, "follows", alias="f"), + left_table=NamedRelationRef(dialect, Table(dialect, "traversal"), alias="t"), + right_table=NamedRelationRef(dialect, Table(dialect, "follows"), alias="f"), join_type="INNER JOIN", condition=Column(dialect, "id", table="t") == Column(dialect, "follower_id", table="f"), ).inner_join( - right_table=TableExpression(dialect, "users", alias="u"), + right_table=NamedRelationRef(dialect, Table(dialect, "users"), alias="u"), condition=Column(dialect, "followed_id", table="f") == Column(dialect, "id", table="u"), ) @@ -200,7 +201,7 @@ def test_friend_recommendation_depth_1_to_3(self, postgres_backend, social_netwo Column(dialect, "name"), Column(dialect, "depth"), ], - from_=TableExpression(dialect, "traversal"), + from_=Table(dialect, "traversal"), where=WhereClause(dialect, condition=BetweenPredicate( dialect, Column(dialect, "depth"), Literal(dialect, 1), Literal(dialect, 3), )), @@ -235,7 +236,7 @@ def test_friend_recommendation_depth_2(self, postgres_backend, social_network_da Column(dialect, "name"), Column(dialect, "depth"), ], - from_=TableExpression(dialect, "traversal"), + from_=Table(dialect, "traversal"), where=WhereClause(dialect, condition=Column(dialect, "depth") == Literal(dialect, 2)), order_by=OrderByClause(dialect, [Column(dialect, "name")]), ) @@ -265,7 +266,7 @@ def test_exclude_start_node(self, postgres_backend, social_network_data): Column(dialect, "name"), Column(dialect, "depth"), ], - from_=TableExpression(dialect, "traversal"), + from_=Table(dialect, "traversal"), where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 1)), order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "name")]), ) @@ -291,8 +292,8 @@ def _build_aml_cte(self, dialect): """Build recursive CTE expression for fund tracing.""" base_join = JoinClause( dialect=dialect, - left_table=TableExpression(dialect, "transactions", alias="tx"), - right_table=TableExpression(dialect, "accounts", alias="a"), + left_table=NamedRelationRef(dialect, Table(dialect, "transactions"), alias="tx"), + right_table=NamedRelationRef(dialect, Table(dialect, "accounts"), alias="a"), join_type="INNER JOIN", condition=Column(dialect, "source_account_id", table="tx") == Column(dialect, "id", table="a"), ) @@ -316,12 +317,12 @@ def _build_aml_cte(self, dialect): recursive_join = JoinClause( dialect=dialect, - left_table=TableExpression(dialect, "fund_trace", alias="tr"), - right_table=TableExpression(dialect, "transactions", alias="tx"), + left_table=NamedRelationRef(dialect, Table(dialect, "fund_trace"), alias="tr"), + right_table=NamedRelationRef(dialect, Table(dialect, "transactions"), alias="tx"), join_type="INNER JOIN", condition=Column(dialect, "target_account_id", table="tx") == Column(dialect, "id", table="tr"), ).inner_join( - right_table=TableExpression(dialect, "accounts", alias="a"), + right_table=NamedRelationRef(dialect, Table(dialect, "accounts"), alias="a"), condition=Column(dialect, "source_account_id", table="tx") == Column(dialect, "id", table="a"), ) @@ -364,7 +365,7 @@ def test_fund_trace_chain(self, postgres_backend, aml_data): Column(dialect, "account_holder"), Column(dialect, "depth"), ], - from_=TableExpression(dialect, "fund_trace"), + from_=Table(dialect, "fund_trace"), where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 1)), order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "id")]), ) @@ -398,7 +399,7 @@ def test_fund_trace_depth_filter(self, postgres_backend, aml_data): Column(dialect, "account_holder"), Column(dialect, "depth"), ], - from_=TableExpression(dialect, "fund_trace"), + from_=Table(dialect, "fund_trace"), where=WhereClause(dialect, condition=Column(dialect, "depth") >= Literal(dialect, 2)), order_by=OrderByClause(dialect, [Column(dialect, "depth"), Column(dialect, "id")]), ) @@ -428,7 +429,7 @@ def test_fund_trace_aggregation(self, postgres_backend, aml_data): Column(dialect, "depth"), FunctionCall(dialect, "SUM", Column(dialect, "amount"), alias="total"), ], - from_=TableExpression(dialect, "fund_trace"), + from_=Table(dialect, "fund_trace"), group_by_having=GroupByHavingClause( dialect, group_by=[Column(dialect, "depth")] ), @@ -462,28 +463,28 @@ async def async_social_network_data(self, async_postgres_backend): dialect = backend.dialect for t in ("follows", "users"): - await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + await backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) - await backend.execute(*CreateTableExpression(dialect, "users", [ + await backend.execute(*CreateTableExpression(dialect, Table(dialect, 'users'), [ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=[ ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)]), ColumnDefinition(dialect, "name", VarCharType(length=100, dialect=dialect)), ]).to_sql()) - await backend.execute(*CreateTableExpression(dialect, "follows", [ + await backend.execute(*CreateTableExpression(dialect, Table(dialect, 'follows'), [ 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"], + await backend.execute(*InsertExpression(dialect, Table(dialect, "users"), columns=["id", "name"], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, "Alice")], [Literal(dialect, 2), Literal(dialect, "Bob")], ])).to_sql()) - await backend.execute(*InsertExpression(dialect, "follows", columns=["id", "follower_id", "followed_id"], + await backend.execute(*InsertExpression(dialect, Table(dialect, "follows"), columns=["id", "follower_id", "followed_id"], source=ValuesSource(dialect, [ [Literal(dialect, 1), Literal(dialect, 1), Literal(dialect, 2)], ])).to_sql()) @@ -491,7 +492,7 @@ async def async_social_network_data(self, async_postgres_backend): yield for t in ("follows", "users"): - await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) + await backend.execute(*DropTableExpression(dialect, Table(dialect, t), if_exists=True, cascade=True).to_sql()) @pytest.mark.asyncio async def test_async_single_hop(self, async_postgres_backend, async_social_network_data): @@ -500,7 +501,7 @@ async def test_async_single_hop(self, async_postgres_backend, async_social_netwo base = QueryExpression( dialect=dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Literal(dialect, 0).as_("depth")], - from_=TableExpression(dialect, "users"), + from_=Table(dialect, "users"), where=WhereClause(dialect, condition=Column(dialect, "name") == Literal(dialect, "Alice")), ) result = base.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_schema_namespace.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_schema_namespace.py index 14adb4f0..c31fe85a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_schema_namespace.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_schema_namespace.py @@ -5,7 +5,7 @@ Covers: - PostgresDialect schema capability declarations - Expression/Dialect layer: CreateSchemaExpression, DropSchemaExpression, - Column/TableExpression/WildcardExpression with schema_name + Column/Table/WildcardExpression with schema_name - Backend layer: CREATE/DROP schema, table operations in custom schema, introspection with schema - Async Backend layer: same operations with async backend @@ -13,9 +13,11 @@ import pytest from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect -from rhosocial.activerecord.backend.expression.core import ( - Column, TableExpression, WildcardExpression, +from rhosocial.activerecord.backend.expression import ( + Column, WildcardExpression, ) +from rhosocial.activerecord.backend.expression.objects import Schema, Table +from rhosocial.activerecord.backend.expression.sources import NamedRelationRef from rhosocial.activerecord.backend.expression.statements.ddl_schema import ( CreateSchemaExpression, DropSchemaExpression, @@ -56,14 +58,16 @@ def test_column_with_schema_name(self, dialect): assert params == () def test_table_expression_with_schema(self, dialect): - table = TableExpression(dialect, "users", schema_name="app") + table = Table(dialect, "users", schema_name="app") sql, params = table.to_sql() assert sql == '"app"."users"' assert params == () def test_table_expression_with_schema_and_alias(self, dialect): - table = TableExpression(dialect, "users", schema_name="app", alias="u") - sql, params = table.to_sql() + source = NamedRelationRef( + dialect, Table(dialect, "users", schema_name="app"), alias="u" + ) + sql, params = source.to_sql() assert '"app"."users"' in sql assert '"u"' in sql @@ -74,26 +78,26 @@ def test_wildcard_with_schema(self, dialect): assert params == () def test_create_schema_expression(self, dialect): - expr = CreateSchemaExpression(dialect, schema_name="app") + expr = CreateSchemaExpression(dialect, schema=Schema(dialect, 'app')) sql, params = expr.to_sql() assert sql == 'CREATE SCHEMA "app"' assert params == () def test_drop_schema_expression(self, dialect): - expr = DropSchemaExpression(dialect, schema_name="app") + expr = DropSchemaExpression(dialect, schema=Schema(dialect, 'app')) sql, params = expr.to_sql() assert sql == 'DROP SCHEMA "app"' assert params == () def test_create_schema_if_not_exists(self, dialect): - expr = CreateSchemaExpression(dialect, schema_name="app", if_not_exists=True) + expr = CreateSchemaExpression(dialect, schema=Schema(dialect, 'app'), if_not_exists=True) sql, params = expr.to_sql() assert "IF NOT EXISTS" in sql assert '"app"' in sql def test_drop_schema_if_exists_cascade(self, dialect): expr = DropSchemaExpression( - dialect, schema_name="app", if_exists=True, cascade=True, + dialect, schema=Schema(dialect, 'app'), if_exists=True, cascade=True, ) sql, params = expr.to_sql() assert "IF EXISTS" in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_set_operation_and_truncate.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_set_operation_and_truncate.py index e704c65d..1b9e8f96 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_set_operation_and_truncate.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_set_operation_and_truncate.py @@ -2,6 +2,7 @@ """Tests for SetOperation and Truncate protocol support in PostgreSQL dialect.""" import pytest # noqa: F401 from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.dialect.protocols import ( SetOperationSupport, TruncateSupport ) @@ -93,7 +94,7 @@ def test_format_truncate_statement_basic(self): from rhosocial.activerecord.backend.expression.statements import TruncateExpression dialect = PostgresDialect() - expr = TruncateExpression(dialect, table_name="users") + expr = TruncateExpression(dialect, table=Table(dialect, 'users')) sql, params = expr.to_sql() assert sql == 'TRUNCATE TABLE "users"' @@ -104,7 +105,7 @@ def test_format_truncate_statement_with_restart_identity(self): from rhosocial.activerecord.backend.expression.statements import TruncateExpression dialect = PostgresDialect(version=(9, 0, 0)) - expr = TruncateExpression(dialect, table_name="users", restart_identity=True) + expr = TruncateExpression(dialect, table=Table(dialect, 'users'), restart_identity=True) sql, params = expr.to_sql() assert sql == 'TRUNCATE TABLE "users" RESTART IDENTITY' @@ -115,7 +116,7 @@ def test_format_truncate_statement_with_cascade(self): from rhosocial.activerecord.backend.expression.statements import TruncateExpression dialect = PostgresDialect() - expr = TruncateExpression(dialect, table_name="orders", cascade=True) + expr = TruncateExpression(dialect, table=Table(dialect, 'orders'), cascade=True) sql, params = expr.to_sql() assert sql == 'TRUNCATE TABLE "orders" CASCADE' @@ -128,7 +129,7 @@ def test_format_truncate_statement_with_restart_identity_and_cascade(self): dialect = PostgresDialect(version=(9, 0, 0)) expr = TruncateExpression( dialect, - table_name="orders", + table=Table(dialect, 'orders'), restart_identity=True, cascade=True ) @@ -145,7 +146,7 @@ def test_format_truncate_statement_restart_identity_unsupported_version(self): # PostgreSQL 8.3 does not support RESTART IDENTITY dialect = PostgresDialect(version=(8, 3, 0)) - expr = TruncateExpression(dialect, table_name="users", restart_identity=True) + expr = TruncateExpression(dialect, table=Table(dialect, 'users'), restart_identity=True) # RESTART IDENTITY should raise UnsupportedFeatureError on unsupported version with pytest.raises(UnsupportedFeatureError, match="RESTART IDENTITY"): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_table.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_table.py index 409aaf45..7ce126ea 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_table.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_table.py @@ -7,6 +7,7 @@ ColumnDefinition, CreateTableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.types import IntegerType from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.mixins.ddl_table import PostgresTableMixin @@ -73,7 +74,7 @@ class TestPostgresTableDDLDeclarations: def _expression(dialect, table="child", *, inherits=None, tablespace=None): return CreateTableExpression( dialect, - table, + Table(dialect, table), [ColumnDefinition(dialect, "id", IntegerType(dialect))], inherits=inherits, tablespace=tablespace, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py index 4e080def..a5c4abd0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_trigger.py @@ -10,6 +10,7 @@ from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.mixins.ddl.trigger import PostgresTriggerMixin +from rhosocial.activerecord.backend.expression.objects import Function, Table, Trigger from rhosocial.activerecord.backend.expression.statements import ( CreateTriggerExpression, DropTriggerExpression, @@ -81,11 +82,11 @@ def test_create_trigger_basic_before(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="update_timestamp", - table_name="users", + trigger=Trigger(dialect, 'update_timestamp'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.UPDATE], - function_name="update_updated_at_column" + function=Function(dialect, 'update_updated_at_column') ) sql, params = expr.to_sql() assert 'CREATE TRIGGER "update_timestamp"' in sql @@ -98,11 +99,11 @@ def test_create_trigger_basic_after(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="log_insert", - table_name="users", + trigger=Trigger(dialect, 'log_insert'), + table=Table(dialect, 'users'), timing=TriggerTiming.AFTER, events=[TriggerEvent.INSERT], - function_name="log_user_insert" + function=Function(dialect, 'log_user_insert') ) sql, params = expr.to_sql() assert "AFTER INSERT" in sql @@ -112,11 +113,11 @@ def test_create_trigger_multiple_events(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="audit_trigger", - table_name="users", + trigger=Trigger(dialect, 'audit_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.AFTER, events=[TriggerEvent.INSERT, TriggerEvent.UPDATE, TriggerEvent.DELETE], - function_name="audit_function" + function=Function(dialect, 'audit_function') ) sql, params = expr.to_sql() assert "INSERT OR UPDATE OR DELETE" in sql @@ -126,12 +127,12 @@ def test_create_trigger_update_of(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="check_status", - table_name="orders", + trigger=Trigger(dialect, 'check_status'), + table=Table(dialect, 'orders'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.UPDATE], update_columns=["status", "updated_at"], - function_name="validate_status_change" + function=Function(dialect, 'validate_status_change') ) sql, params = expr.to_sql() assert 'UPDATE OF "status", "updated_at"' in sql @@ -141,11 +142,11 @@ def test_create_trigger_with_level_row(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="test_trigger", - table_name="users", + trigger=Trigger(dialect, 'test_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.UPDATE], - function_name="test_func", + function=Function(dialect, 'test_func'), level=TriggerLevel.ROW ) sql, params = expr.to_sql() @@ -156,11 +157,11 @@ def test_create_trigger_with_level_statement(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="test_trigger", - table_name="users", + trigger=Trigger(dialect, 'test_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.AFTER, events=[TriggerEvent.INSERT], - function_name="test_func", + function=Function(dialect, 'test_func'), level=TriggerLevel.STATEMENT ) sql, params = expr.to_sql() @@ -171,11 +172,11 @@ def test_create_trigger_if_not_exists(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="test_trigger", - table_name="users", + trigger=Trigger(dialect, 'test_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.UPDATE], - function_name="test_func", + function=Function(dialect, 'test_func'), if_not_exists=True ) sql, params = expr.to_sql() @@ -186,11 +187,11 @@ def test_create_trigger_with_referencing_pg10(self): dialect = PostgresDialect((10, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="test_trigger", - table_name="users", + trigger=Trigger(dialect, 'test_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.AFTER, events=[TriggerEvent.UPDATE], - function_name="test_func", + function=Function(dialect, 'test_func'), referencing="OLD TABLE AS old NEW TABLE AS new" ) sql, params = expr.to_sql() @@ -202,11 +203,11 @@ def test_create_trigger_no_level(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="simple_trigger", - table_name="users", + trigger=Trigger(dialect, 'simple_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.INSERT], - function_name="simple_func", + function=Function(dialect, 'simple_func'), level=None, ) sql, params = expr.to_sql() @@ -219,11 +220,11 @@ def test_create_trigger_with_condition(self): condition = Column(dialect, "status") == Literal(dialect, "ACTIVE") expr = CreateTriggerExpression( dialect, - trigger_name="check_status", - table_name="users", + trigger=Trigger(dialect, 'check_status'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.UPDATE], - function_name="validate_status", + function=Function(dialect, 'validate_status'), condition=condition, ) sql, params = expr.to_sql() @@ -236,11 +237,11 @@ def test_create_trigger_instead_of(self): dialect = PostgresDialect((14, 0, 0)) expr = CreateTriggerExpression( dialect, - trigger_name="view_trigger", - table_name="user_view", + trigger=Trigger(dialect, 'view_trigger'), + table=Table(dialect, 'user_view'), timing=TriggerTiming.INSTEAD_OF, events=[TriggerEvent.INSERT], - function_name="handle_view_insert" + function=Function(dialect, 'handle_view_insert') ) sql, params = expr.to_sql() assert "INSTEAD OF INSERT" in sql @@ -254,7 +255,7 @@ def test_drop_trigger_basic(self): dialect = PostgresDialect((14, 0, 0)) expr = DropTriggerExpression( dialect, - trigger_name="test_trigger" + trigger=Trigger(dialect, 'test_trigger') ) sql, params = expr.to_sql() assert 'DROP TRIGGER "test_trigger"' in sql @@ -264,8 +265,8 @@ def test_drop_trigger_with_table(self): dialect = PostgresDialect((14, 0, 0)) expr = DropTriggerExpression( dialect, - trigger_name="test_trigger", - table_name="users" + trigger=Trigger(dialect, 'test_trigger'), + table=Table(dialect, 'users') ) sql, params = expr.to_sql() assert 'ON "users"' in sql @@ -275,7 +276,7 @@ def test_drop_trigger_if_exists(self): dialect = PostgresDialect((14, 0, 0)) expr = DropTriggerExpression( dialect, - trigger_name="test_trigger", + trigger=Trigger(dialect, 'test_trigger'), if_exists=True ) sql, params = expr.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py index 3ce72c2f..6e8c218e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py @@ -9,13 +9,14 @@ from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # noqa: F401 from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect # noqa: F401 from rhosocial.activerecord.backend.expression import ( - Column, Literal, FunctionCall, TableExpression, QueryExpression, # noqa: F401 - CreateViewExpression, DropViewExpression, - CreateMaterializedViewExpression, DropMaterializedViewExpression, - RefreshMaterializedViewExpression, CreateTableExpression, DropTableExpression, # noqa: F401 - InsertExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, # noqa: F401 - TableConstraint, TableConstraintType, ForeignKeyConstraint, ValuesSource # noqa: F401 + Column, ColumnConstraint, ColumnConstraintType, ColumnDefinition, + CreateMaterializedViewExpression, CreateTableExpression, CreateViewExpression, + DropMaterializedViewExpression, DropTableExpression, DropViewExpression, + ForeignKeyConstraint, FunctionCall, InsertExpression, Literal, + QueryExpression, RefreshMaterializedViewExpression, TableConstraint, + TableConstraintType, ValuesSource, ) +from rhosocial.activerecord.backend.expression.objects import MaterializedView, Table, View from rhosocial.activerecord.backend.expression.operators import RawSQLPredicate, RawSQLExpression # noqa: F401 from rhosocial.activerecord.backend.expression.query_parts import GroupByHavingClause, WhereClause # noqa: F401 from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError # noqa: F401 @@ -124,12 +125,12 @@ def test_create_view_basic(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "email")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_view = CreateViewExpression( dialect, - view_name="user_view", + view=View(dialect, 'user_view'), query=query ) @@ -159,13 +160,13 @@ def test_create_view_with_where(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users"), + from_=Table(dialect, "users"), where=WhereClause(dialect, condition=RawSQLPredicate(dialect, '"status" = \'active\'')) ) create_view = CreateViewExpression( dialect, - view_name="active_users", + view=View(dialect, 'active_users'), query=query ) @@ -192,12 +193,12 @@ def test_create_or_replace_view(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_view = CreateViewExpression( dialect, - view_name="user_view", + view=View(dialect, 'user_view'), query=query ) @@ -209,12 +210,12 @@ def test_create_or_replace_view(self, postgres_view_backend): query2 = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_view2 = CreateViewExpression( dialect, - view_name="user_view", + view=View(dialect, 'user_view'), query=query2, replace=True ) @@ -231,12 +232,12 @@ def test_drop_view(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_view = CreateViewExpression( dialect, - view_name="user_view", + view=View(dialect, 'user_view'), query=query ) @@ -246,7 +247,7 @@ def test_drop_view(self, postgres_view_backend): drop_view = DropViewExpression( dialect, - view_name="user_view" + view=View(dialect, 'user_view') ) sql, params = drop_view.to_sql() @@ -260,7 +261,7 @@ def test_drop_view_if_exists(self, postgres_view_backend): drop_view = DropViewExpression( dialect, - view_name="nonexistent_view", + view=View(dialect, 'nonexistent_view'), if_exists=True ) @@ -276,12 +277,12 @@ def test_drop_view_cascade(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_view = CreateViewExpression( dialect, - view_name="user_view", + view=View(dialect, 'user_view'), query=query ) @@ -291,7 +292,7 @@ def test_drop_view_cascade(self, postgres_view_backend): drop_view = DropViewExpression( dialect, - view_name="user_view", + view=View(dialect, 'user_view'), cascade=True ) @@ -311,12 +312,12 @@ def test_create_materialized_view_basic(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name"), Column(dialect, "email")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query ) @@ -344,12 +345,12 @@ def test_create_materialized_view_with_data(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query, with_data=True ) @@ -374,12 +375,12 @@ def test_create_materialized_view_with_no_data(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query, with_data=False ) @@ -396,12 +397,12 @@ def test_drop_materialized_view(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query ) @@ -411,7 +412,7 @@ def test_drop_materialized_view(self, postgres_view_backend): drop_mv = DropMaterializedViewExpression( dialect, - view_name="test_mv" + view=MaterializedView(dialect, 'test_mv') ) sql, params = drop_mv.to_sql() @@ -425,7 +426,7 @@ def test_drop_materialized_view_if_exists(self, postgres_view_backend): drop_mv = DropMaterializedViewExpression( dialect, - view_name="nonexistent_mv", + view=MaterializedView(dialect, 'nonexistent_mv'), if_exists=True ) @@ -441,12 +442,12 @@ def test_drop_materialized_view_cascade(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query ) @@ -456,7 +457,7 @@ def test_drop_materialized_view_cascade(self, postgres_view_backend): drop_mv = DropMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), cascade=True ) @@ -472,12 +473,12 @@ def test_refresh_materialized_view(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query ) @@ -487,7 +488,7 @@ def test_refresh_materialized_view(self, postgres_view_backend): refresh_mv = RefreshMaterializedViewExpression( dialect, - view_name="test_mv" + view=MaterializedView(dialect, 'test_mv') ) sql, params = refresh_mv.to_sql() @@ -502,12 +503,12 @@ def test_refresh_materialized_view_concurrently(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query ) @@ -524,7 +525,7 @@ def test_refresh_materialized_view_concurrently(self, postgres_view_backend): refresh_mv = RefreshMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), concurrent=True ) @@ -540,12 +541,12 @@ def test_refresh_materialized_view_with_data(self, postgres_view_backend): query = QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "name")], - from_=TableExpression(dialect, "users") + from_=Table(dialect, "users") ) create_mv = CreateMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), query=query, with_data=False ) @@ -556,7 +557,7 @@ def test_refresh_materialized_view_with_data(self, postgres_view_backend): refresh_mv = RefreshMaterializedViewExpression( dialect, - view_name="test_mv", + view=MaterializedView(dialect, 'test_mv'), with_data=True ) 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 50dd237e..c0676280 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 @@ -19,6 +19,7 @@ BooleanType, IntegerType, TextType, TimestampType, VarCharType, ) from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Table @pytest.fixture @@ -32,7 +33,7 @@ def _pk(dialect): def _build_table(dialect): return CreateTableExpression( - dialect=dialect, table="test_tbl", + dialect=dialect, table=Table(dialect, 'test_tbl'), columns=[ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=_pk(dialect)), ColumnDefinition(dialect, "name", TextType(dialect=dialect), @@ -60,7 +61,7 @@ def test_all_standard_types_compile(self, dialect): def test_boolean_default_true(self, dialect): expr = CreateTableExpression( - dialect=dialect, table="bool_test", + dialect=dialect, table=Table(dialect, 'bool_test'), columns=[ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=_pk(dialect)), ColumnDefinition(dialect, "flag", BooleanType(dialect=dialect), @@ -73,7 +74,7 @@ def test_boolean_default_true(self, dialect): def test_timestamp_type(self, dialect): expr = CreateTableExpression( - dialect=dialect, table="ts_test", + dialect=dialect, table=Table(dialect, 'ts_test'), columns=[ ColumnDefinition(dialect, "id", IntegerType(dialect=dialect), constraints=_pk(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 9b483a2a..c7a3018b 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 @@ -10,6 +10,7 @@ form ``CREATE TABLE t (LIKE src [INCLUDING ...])``. """ from rhosocial.activerecord.backend.expression import CreateTableLikeExpression +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect @@ -19,7 +20,11 @@ class TestPostgreSQLCreateTableLike: def test_basic_like_syntax(self): """Test basic CREATE TABLE ... LIKE syntax.""" dialect = PostgresDialect() - expr = CreateTableLikeExpression(dialect, table="users_copy", like_table="users") + expr = CreateTableLikeExpression( + dialect, + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), + ) sql, params = expr.to_sql() assert sql == 'CREATE TABLE "users_copy" (LIKE "users")' @@ -30,8 +35,8 @@ def test_like_with_including_defaults(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={'including': ['defaults']}, ) sql, params = expr.to_sql() @@ -44,8 +49,8 @@ def test_like_with_including_constraints(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={'including': ['constraints']}, ) sql, params = expr.to_sql() @@ -58,8 +63,8 @@ def test_like_with_including_indexes(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={'including': ['indexes']}, ) sql, params = expr.to_sql() @@ -72,8 +77,8 @@ def test_like_with_multiple_including_options(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={'including': ['defaults', 'constraints', 'indexes']}, ) sql, params = expr.to_sql() @@ -89,8 +94,8 @@ def test_like_with_including_and_excluding_options(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={ 'including': ['defaults', 'constraints'], 'excluding': ['indexes', 'comments'], @@ -110,8 +115,8 @@ def test_like_with_list_format_options(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options=['defaults', 'constraints'], ) sql, params = expr.to_sql() @@ -127,8 +132,8 @@ def test_like_with_tuple_format_options(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options=[ ('INCLUDING', 'DEFAULTS'), ('EXCLUDING', 'INDEXES'), @@ -147,8 +152,8 @@ def test_like_with_lowercase_option_names(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={'including': ['comments', 'defaults']}, ) sql, params = expr.to_sql() @@ -164,8 +169,8 @@ def test_like_with_if_not_exists(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), if_not_exists=True, ) sql, params = expr.to_sql() @@ -178,8 +183,8 @@ def test_like_with_temporary(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="temp_users", - like_table="users", + table=Table(dialect, "temp_users"), + like_table=Table(dialect, "users"), temporary=True, ) sql, params = expr.to_sql() @@ -192,8 +197,8 @@ def test_like_with_schema_qualified_table(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table=('public', 'users'), + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users", schema_name="public"), ) sql, params = expr.to_sql() @@ -205,8 +210,8 @@ def test_like_with_temporary_and_if_not_exists(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="temp_users_copy", - like_table=('public', 'users'), + table=Table(dialect, "temp_users_copy"), + like_table=Table(dialect, "users", schema_name="public"), temporary=True, if_not_exists=True, ) @@ -223,8 +228,8 @@ def test_like_with_all_option(self): dialect = PostgresDialect() expr = CreateTableLikeExpression( dialect, - table="users_copy", - like_table="users", + table=Table(dialect, "users_copy"), + like_table=Table(dialect, "users"), like_options={'including': ['all']}, ) sql, params = expr.to_sql() 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 15c816cf..fc9acf57 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 @@ -1,80 +1,1340 @@ # tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py """ -Functional serialization coverage for PostgreSQL expression classes. +Functional serialization coverage: every registered expression class must +round-trip losslessly through all three encodings (dict / JSON string / XML). -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. +For each expression class that can be constructed: + 1. dict round-trip : deserialize(serialize(e)).get_params() == e.get_params() + 2. JSON round-trip : deserialize_json(serialize_json(e)).get_params() == ... + 3. XML round-trip : deserialize_xml(serialize_xml(e)).get_params() == ... + 4. SQL consistency : classified, never swallowed -- see below. + +Why this matrix collects two packages, not one +============================================= + +``rhosocial.activerecord.backend.impl.postgres.expression`` holds the classes this +backend defines. ``rhosocial.activerecord.backend.expression`` holds core's, and +core's statements are exactly the ones a backend override consumes: when a +PostgreSQL statement formats a core ``Table`` or a core ``CommentOnExpression``, +the defect that matters is in *this* dialect's reading of a core object. That is +why ``mixins/ddl/comment.py`` could read a field core had removed and stay green: +core's own classes were outside the only matrix in this repository, so a +PostgreSQL-side formatter misreading them was unobservable. + +The matrix therefore walks both packages with ``collect_expression_classes``. It +deliberately does not read ``ExpressionRegistry._registry``: that dict is +process-global and grows as sibling test modules import other backends, so a +registry-based matrix covers a different class set depending on import order. +Walking the packages is deterministic. + +Why ``to_sql()`` is classified rather than caught +================================================ + +This matrix used to call the testsuite's ``sql_consistent``, whose body is:: + + try: + expected = instance.to_sql() + except Exception: + return + +so every render failure was a green tick and the three SQL comparisons never +ran. Measured on this backend before the rewrite -- walk the PostgreSQL +expression package only, build each class once with ``make_instance``, render it +once, and bucket the outcome (``scripts/expr_matrix_probe.py`` does exactly that): +of **168** classes discovered, **125** reached a SQL comparison, **20** raised and +were swallowed, and **23** could not be built and became ``pytest.skip``. So 43 of +168 -- a quarter -- were never asserted at all, and every one of them reported +green. + +Each outcome is now named and asserted: + +* **renders** -- all three encodings must restore byte-identical SQL *and* + byte-identical bind parameters. +* ``UnsupportedFeatureError`` -- this dialect does not model the feature. + Asserted as exactly that type, so a different error cannot hide behind it. +* a member of :data:`LEGITIMATE_NON_RENDERS` -- a class that cannot render for a + reason belonging to its own tree or to PostgreSQL's feature set. Each entry + pins the exception type *and* a message fragment, so a class that started + failing for a different reason fails here instead of staying quietly green. +* **anything else** -- a failure naming the class and the exception. + +Why some pins are themselves ``UnsupportedFeatureError`` +========================================================= + +``to_sql()`` reports a missing formatter as ``UnsupportedFeatureError``, so the +two buckets above can overlap: a class can be unrenderable for a reason that is +about PostgreSQL rather than about its own tree. ``TableSource``, +``PivotExpression`` and ``UnpivotExpression`` are such classes, and they are +still named in :data:`LEGITIMATE_NON_RENDERS` on purpose. The bare type is always +allowed, so the type alone asserts nothing about *which* formatter is missing; +the entry adds the message fragment, and that fragment names the one formatter +the class dispatches on. Deleting the entry would have made the test pass by +saying less. + +And what happens when a class cannot be constructed +================================================== + +``make_instance(...) is None`` becomes a skip, but only for a class named in +:data:`UNCONSTRUCTIBLE`, and ``test_unconstructible_list_is_exact`` pins that +tuple in both directions. A class that starts needing an exemption fails CI +instead of turning into a skip, and a stale entry fails too. """ +import inspect +from typing import Dict + import pytest +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression import graph as graph_mod +from rhosocial.activerecord.backend.expression.advanced_functions import ( + CaseExpression, + WindowClause, + WindowDefinition, + WindowSpecification, +) +from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.expression.collation import CollateExpression +from rhosocial.activerecord.backend.expression.core import Column, Literal +from rhosocial.activerecord.backend.expression.datetime import ( + TemporalOptionsExpression, +) +from rhosocial.activerecord.backend.expression.objects import ( + Database, + EdgeTable as EdgeTableObject, + Function, + MaterializedView, + NodeTable, + PropertyGraph, + Table, + Trigger, +) +from rhosocial.activerecord.backend.expression.predicates import ( + ComparisonPredicate, + ILIKEExpression as CoreILIKEExpression, +) +from rhosocial.activerecord.backend.expression.query_parts import JoinClause +from rhosocial.activerecord.backend.expression.serialization import ( + ExpressionRegistry, + deserialize, + deserialize_json, + deserialize_xml, + serialize, + serialize_json, + serialize_xml, +) +from rhosocial.activerecord.backend.expression.sources import NamedRelationRef +from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( + AddColumn, + AddTableConstraint, +) +from rhosocial.activerecord.backend.expression.statements.ddl_database import ( + AlterDatabaseAction, + AlterDatabaseExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_domain import ( + AddDomainCheckAction, + DomainCheckConstraint, + RenameDomainAction, +) +from rhosocial.activerecord.backend.expression.statements.ddl_partition import ( + PartitionStrategy, +) +from rhosocial.activerecord.backend.expression.statements.ddl_table import ( + ColumnConstraint, + ColumnConstraintType, + ColumnDefinition, + ForeignKeyConstraint, + ReferencesClause, + TableConstraint, + TableConstraintType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_trigger import ( + CreateTriggerExpression, + TriggerEvent, + TriggerTiming, +) +from rhosocial.activerecord.backend.expression.statements.dml import ( + MergeAction, + MergeActionType, + MergeExpression, +) +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + EnumType, + IntegerType, + VarCharType, +) +from rhosocial.activerecord.backend.expression.xml import ( + XMLAttribute, + XMLAttributesExpression, + XMLConcatExpression, + XMLForestExpression, + XMLForestItem, + XMLTableColumn, + XMLTableExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.copy import ( + PostgresCopyToExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.column import ( + PostgresColumnDefinition, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.constraint import ( + PostgresAlterConstraint, + PostgresValidateConstraint, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.domain import ( + PostgresAddDomainCheckAction, + PostgresAlterDomainExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.exclude_constraint import ( + PostgresExcludeConstraint, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.index import ( + PostgresAlterIndexActionType, + PostgresAlterIndexExpression, + PostgresReindexExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.mv import ( + PostgresAlterMaterializedViewExpression, + PostgresResetMaterializedViewPropertiesAction, + PostgresSetMaterializedViewPropertiesAction, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.partition import ( + PostgresAttachPartitionExpression, + PostgresCreatePartitionExpression, + PostgresPartitionClause, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.pg_partman import ( + PostgresPgPartmanUpdateConfigExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.policy import ( + PostgresAlterPolicyExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.publication import ( + PostgresCreatePublicationExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.rls_config import ( + PostgresAlterTableRlsExpression, + RlsConfigurationMode, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.table_settings import ( + LoggingMode, + PostgresAlterTableSettingsExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.type import ( + CreateEnumTypeExpression, + EnumValuesExpression, + PostgresBaseTypeDefinition, + PostgresCompositeTypeAttribute, + PostgresCompositeTypeDefinition, + PostgresCreateEnumTypeExpression, + PostgresEnumTypeDefinition, + PostgresSetTypePropertiesAction, +) +from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import ( + PostgresEnumType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ilike import ( + ILIKEExpression as PostgresILIKEExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, +) from rhosocial.activerecord.testsuite.utils.expression import ( collect_expression_classes, make_instance, register_all, register_special_constructor, - roundtrip_expression, - sql_consistent, ) + +def assert_params_equal(a, b, path="params"): + """Deep compare two get_params() dicts, treating nested BaseExpression + instances as structurally equal when their get_params() match.""" + if isinstance(a, BaseExpression) and isinstance(b, BaseExpression): + assert_params_equal(a.get_params(), b.get_params(), path + ".") + return + assert type(a) is type(b) or ( + isinstance(a, (list, tuple, dict)) and isinstance(b, (list, tuple, dict)) + ), f"{path}: type mismatch {type(a).__name__} vs {type(b).__name__}" + if isinstance(a, dict): + assert set(a) == set(b), f"{path}: keys differ {set(a) ^ set(b)}" + for k in a: + assert_params_equal(a[k], b[k], f"{path}.{k}") + return + if isinstance(a, (list, tuple)): + assert len(a) == len(b), f"{path}: length differ" + for i, (x, y) in enumerate(zip(a, b)): + assert_params_equal(x, y, f"{path}[{i}]") + return + assert a == b, f"{path}: {a!r} != {b!r}" + + PG_EXPR_PKG = "rhosocial.activerecord.backend.impl.postgres.expression" +CORE_EXPR_PKG = "rhosocial.activerecord.backend.expression" -CLASSES = collect_expression_classes(PG_EXPR_PKG) -register_all(CLASSES) +def _collect_matrix_classes(): + """Every concrete expression class this backend can be asked to render. -def _register_postgres_specials(): - from rhosocial.activerecord.backend.expression.core import Column, Literal + Two packages, walked rather than read out of the registry -- see the module + docstring. ``ExpressionRegistry._auto_register_builtins`` is still called + because deserialisation needs the core classes registered; that is a separate + concern from deciding which classes to test. + """ + ExpressionRegistry._auto_register_builtins() + collected: Dict[str, type] = {} + for package in (PG_EXPR_PKG, CORE_EXPR_PKG): + collected.update(collect_expression_classes(package)) + by_identity: Dict[int, str] = {} + for fqn, cls in sorted(collected.items()): + by_identity.setdefault(id(cls), fqn) + return { + fqn: cls + for cls in collected.values() + if not inspect.isabstract(cls) + for fqn in [by_identity[id(cls)]] + } - def for_update(d): - from rhosocial.activerecord.backend.impl.postgres.expression.locking import ( - PostgresForUpdateExpression, - ) - return PostgresForUpdateExpression(d, columns=[Column(dialect, d, "id")]) - def advisory(d): - from rhosocial.activerecord.backend.impl.postgres.expression.advisory.lock import ( - PostgresAdvisoryLockExpression, +REGISTERED = _collect_matrix_classes() +register_all(REGISTERED) + + +# --------------------------------------------------------------------------- +# Special constructors: a real value where the introspective guess is a lie +# --------------------------------------------------------------------------- +# +# ``make_instance`` reads each required parameter's annotation and guesses: +# ``"x"`` for a string, ``[]`` for a list, ``IntegerType()`` for a type. That is +# right for a name and wrong for every parameter that wants a catalogue object -- +# a Table, an Index, an enum member -- because a bare ``"x"`` is not one, and the +# formatter now refuses it by name. It is also wrong for the containers that +# require at least one member (CASE, WINDOW, JOIN, EXCLUDE), for the predicates +# that must render without bind parameters (a DOMAIN CHECK is DDL), and for the +# handful of strings that are really enums spelled as a union of an enum class +# and ``str`` -- ``constraint_type``, ``action_type`` -- where the guess cannot +# know which member is legal. +# +# Every registration below replaces a guess that would otherwise have produced an +# instance the PostgreSQL dialect cannot render. Suffixes are spelled relative to +# the expression package because ``make_instance`` matches with ``str.endswith`` +# and several modules export identically named classes. + + +def _table_obj(dialect, name="t"): + """A table with a bare name and no namespace.""" + return Table(dialect, name) + + +def _column_predicate(dialect): + """A predicate comparing two columns, so it renders with no bind parameters.""" + return ComparisonPredicate(dialect, "=", Column(dialect, "a"), Column(dialect, "b")) + + +def _integer_column(dialect, name="col"): + """A column definition carrying a *dialect-bound* type. + + The binding is load-bearing, not cosmetic: ``to_sql()`` dispatches on the + type through its own dialect, so an unbound ``IntegerType()`` raises + ``ValueError: ... has no dialect bound`` the moment anything renders it. + """ + return ColumnDefinition(dialect, name, IntegerType(dialect)) + + +def _primary_key_constraint(dialect): + """A table-level PRIMARY KEY, shared by three guesses that need one.""" + return TableConstraint( + dialect, TableConstraintType.PRIMARY_KEY, name="c", columns=["a"] + ) + + +def _domain_check(dialect): + """A DOMAIN CHECK constraint, which renders with no bind parameters. + + A predicate comparing a column to a literal would have to become a bind + parameter, and DDL carries none -- so the check compares two columns. + """ + return DomainCheckConstraint(dialect, _column_predicate(dialect), name="chk") + + +def _merge_action(dialect): + """One MERGE ``WHEN MATCHED`` arm.""" + return MergeAction( + dialect, + MergeActionType.UPDATE, + {"a": Literal(dialect, 1)}, + _column_predicate(dialect), + "matched", + ) + + +def _alter_constraint(dialect): + """An ALTER CONSTRAINT action carrying the one required enum member. + + ``constraint_type`` is typed ``ColumnConstraintType | TableConstraintType | + str``, so the introspective constructor cannot know which member is legal -- + and only CHECK and FOREIGN KEY are accepted for this statement, so most + guesses are refused by name. PostgreSQL's own four subclasses and core's three + share this shape, so one constructor is registered under each name: + ``make_instance`` picks by ``str.endswith``, so a single shared suffix would + also shadow unrelated classes. + """ + return PostgresAlterConstraint( + dialect, + constraint_name="c", + name="c", + constraint_type=TableConstraintType.CHECK, + ) + + +def _validate_constraint(dialect): + """A VALIDATE CONSTRAINT action, which needs only a name. + + The guess leaves the name ``None`` and the class refuses a nameless + constraint. + """ + return PostgresValidateConstraint( + dialect, constraint_name="c", name="c" + ) + + +#: The ``constraint_type`` family: PostgreSQL's four ALTER CONSTRAINT classes and +#: core's three. All seven need ``_alter_constraint``. +ALTER_CONSTRAINT_SUFFIXES = ( + "ddl.constraint.PostgresAlterConstraint", + "ddl.constraint.PostgresAlterConstraintAction", + "ddl.constraint.PostgresAlterConstraintExpression", + "ddl.constraint.PostgresAlterTableConstraint", + "statements.ddl_alter.AlterConstraint", + "statements.ddl_alter.AlterConstraintAction", + "statements.ddl_alter.AlterTableConstraint", +) + +#: The VALIDATE CONSTRAINT family, same shape, same reason. Seven classes. +VALIDATE_CONSTRAINT_SUFFIXES = ( + "ddl.constraint.PostgresValidateConstraint", + "ddl.constraint.PostgresValidateConstraintAction", + "ddl.constraint.PostgresValidateConstraintExpression", + "ddl.constraint.PostgresValidateTableConstraint", + "statements.ddl_alter.ValidateConstraint", + "statements.ddl_alter.ValidateConstraintAction", + "statements.ddl_alter.ValidateTableConstraint", +) + + +def register_specials(): + """Replace every introspective guess that would cost a real assertion.""" + # -- the FROM side ------------------------------------------------------ + register_special_constructor( + "sources.relation.NamedRelationRef", + lambda d: NamedRelationRef(d, _table_obj(d)), + ) + register_special_constructor( + "query_parts.JoinClause", + lambda d: JoinClause( + d, + left_table=NamedRelationRef(d, _table_obj(d)), + right_table=NamedRelationRef(d, Table(d, "other")), + condition=_column_predicate(d), + ), + ) + + # -- core objects that must reach a formatter as objects --------------- + # ``to_sql`` dispatches through the object's own ``format_method``, so a + # plain string in one of these slots was rendered by whatever formatter the + # string happened to name -- or refused. Either way the statement was never + # the one the class describes. + register_special_constructor( + "statements.ddl_alter.AddColumn", + lambda d: AddColumn(d, _integer_column(d)), + ) + register_special_constructor( + "statements.ddl_alter.AddTableConstraint", + lambda d: AddTableConstraint(d, _primary_key_constraint(d)), + ) + register_special_constructor( + "statements.ddl_table.ColumnConstraint", + lambda d: ColumnConstraint(d, ColumnConstraintType.NOT_NULL, name="c"), + ) + register_special_constructor( + "statements.ddl_table.TableConstraint", + lambda d: _primary_key_constraint(d), + ) + register_special_constructor( + "statements.ddl_table.ForeignKeyConstraint", + lambda d: ForeignKeyConstraint( + d, + columns=["a"], + foreign_key_table=Table(d, "other"), + foreign_key_columns=["b"], + name="fk", + ), + ) + register_special_constructor( + "statements.ddl_table.ReferencesClause", + lambda d: ReferencesClause(d, Table(d, "other"), ["b"]), + ) + register_special_constructor( + "statements.ddl_database.AlterDatabaseExpression", + lambda d: AlterDatabaseExpression( + d, + Database(d, "db"), + action=AlterDatabaseAction.RENAME_TO, + target="renamed_db", + ), + ) + register_special_constructor( + "statements.ddl_trigger.CreateTriggerExpression", + lambda d: CreateTriggerExpression( + d, + trigger=Trigger(d, "trg"), + table=_table_obj(d), + timing=TriggerTiming.BEFORE, + events=[TriggerEvent.INSERT], + function=Function(d, "fn"), + ), + ) + + # -- DDL checks render with no bind parameters ------------------------- + # A DOMAIN CHECK is DDL. A predicate comparing a column to a literal would + # have to become a bind parameter, and DDL carries none, so both the check + # itself and the action that adds it need a column-to-column predicate. + register_special_constructor( + "statements.ddl_domain.DomainCheckConstraint", + lambda d: _domain_check(d), + ) + register_special_constructor( + "statements.ddl_domain.AddDomainCheckAction", + lambda d: AddDomainCheckAction(d, _domain_check(d)), + ) + + # -- the constraint families -------------------------------------------- + # ``constraint_type`` is a union of an enum class and ``str``, so the guess + # cannot know which member is legal; the VALIDATE family needs only a name. + for suffix in ALTER_CONSTRAINT_SUFFIXES: + register_special_constructor(suffix, lambda d: _alter_constraint(d)) + for suffix in VALIDATE_CONSTRAINT_SUFFIXES: + register_special_constructor(suffix, lambda d: _validate_constraint(d)) + + # -- PostgreSQL statements whose required slot is a validated enum ----- + # ``partition_type`` is checked against {RANGE, LIST, HASH}; ``target_type`` + # against the REINDEX targets; ``mode`` against the RLS modes. The guess + # supplies a bare string and the formatter refuses it. + register_special_constructor( + "ddl.partition.PostgresCreatePartitionExpression", + lambda d: PostgresCreatePartitionExpression( + d, + partition_name="p1", + parent_table="t", + partition_type="LIST", + partition_values={"default": True}, + ), + ) + register_special_constructor( + "ddl.partition.PostgresAttachPartitionExpression", + lambda d: PostgresAttachPartitionExpression( + d, + partition_name="p1", + parent_table="t", + partition_type="LIST", + partition_values={"default": True}, + ), + ) + register_special_constructor( + "ddl.partition.PostgresPartitionClause", + lambda d: PostgresPartitionClause( + d, PartitionStrategy.RANGE, [Column(d, "a")] + ), + ) + register_special_constructor( + "ddl.index.PostgresAlterIndexExpression", + lambda d: PostgresAlterIndexExpression( + d, + index_name="i", + action_type=PostgresAlterIndexActionType.RENAME_TO, + new_name="j", + ), + ) + register_special_constructor( + "ddl.index.PostgresReindexExpression", + lambda d: PostgresReindexExpression(d, target_type="INDEX", name="i"), + ) + register_special_constructor( + "ddl.rls_config.PostgresAlterTableRlsExpression", + lambda d: PostgresAlterTableRlsExpression( + d, table_name="t", mode=RlsConfigurationMode.ENABLE + ), + ) + register_special_constructor( + "ddl.table_settings.PostgresAlterTableSettingsExpression", + lambda d: PostgresAlterTableSettingsExpression( + d, table_name="t", mode=LoggingMode.LOGGED + ), + ) + + # -- PostgreSQL statements whose required slot is a non-empty member ---- + # Each of these refuses to be a statement without the member its name says it + # modifies, so the introspective ``[]`` is refused by design. + register_special_constructor( + "ddl.exclude_constraint.PostgresExcludeConstraint", + lambda d: PostgresExcludeConstraint(d, name="ex", elements=[(Column(d, "a"), "=")]), + ) + register_special_constructor( + "ddl.pg_partman.PostgresPgPartmanUpdateConfigExpression", + lambda d: PostgresPgPartmanUpdateConfigExpression( + d, parent_table="t", retention="7 days" + ), + ) + register_special_constructor( + "ddl.policy.PostgresAlterPolicyExpression", + lambda d: PostgresAlterPolicyExpression( + d, name="p", table_name="t", new_name="q" + ), + ) + register_special_constructor( + "ddl.publication.PostgresCreatePublicationExpression", + lambda d: PostgresCreatePublicationExpression( + d, name="pub", tables=["t"] + ), + ) + + # -- PostgreSQL DDL type definitions ------------------------------------ + # ``values`` / ``labels`` / ``properties`` are keyword-only behind defaulted + # positionals, so the introspective constructor skips them and the definition + # declares nothing. + register_special_constructor( + "ddl.type.PostgresCreateEnumTypeExpression", + lambda d: PostgresCreateEnumTypeExpression(d, name="mood", values=["sad", "ok"]), + ) + register_special_constructor( + "ddl.type.CreateEnumTypeExpression", + lambda d: CreateEnumTypeExpression(d, name="mood", values=["sad", "ok"]), + ) + register_special_constructor( + "ddl.type.EnumValuesExpression", + lambda d: EnumValuesExpression(d, values=["sad", "ok"]), + ) + register_special_constructor( + "ddl.type.PostgresEnumTypeDefinition", + lambda d: PostgresEnumTypeDefinition(d, labels=["sad", "ok"]), + ) + register_special_constructor( + "ddl.type.PostgresSetTypePropertiesAction", + lambda d: PostgresSetTypePropertiesAction(d, properties={"STORAGE": "plain"}), + ) + # A base type is a shell type: it needs the two I/O functions and everything + # else is optional, so the introspective constructor supplies nothing at all + # because ``dialect`` is the only non-defaulted parameter. + register_special_constructor( + "ddl.type.PostgresBaseTypeDefinition", + lambda d: PostgresBaseTypeDefinition( + d, input_function="int4in", output_function="int4out", internallength=4 + ), + ) + register_special_constructor( + "ddl.type.PostgresCompositeTypeDefinition", + lambda d: PostgresCompositeTypeDefinition( + d, + attributes=[ + PostgresCompositeTypeAttribute("a", IntegerType(d)) + ], + ), + ) + register_special_constructor( + "enum_.PostgresEnumType", + lambda d: PostgresEnumType(d, name="mood", values=["sad", "ok"]), + ) + + # -- columns, COPY, materialized views --------------------------------- + # A PostgreSQL column definition's ``data_type`` is required and untyped in + # the annotation, so the guess supplies nothing for it. + register_special_constructor( + "ddl.column.PostgresColumnDefinition", + lambda d: PostgresColumnDefinition(d, "c", IntegerType(d)), + ) + # COPY needs a source: either a query or a table. With neither, there is no + # statement. + register_special_constructor( + "copy.PostgresCopyToExpression", + lambda d: PostgresCopyToExpression(d, table_name="t"), + ) + register_special_constructor( + "ddl.mv.PostgresSetMaterializedViewPropertiesAction", + lambda d: PostgresSetMaterializedViewPropertiesAction( + d, properties={"autovacuum_enabled": "true"} + ), + ) + register_special_constructor( + "ddl.mv.PostgresResetMaterializedViewPropertiesAction", + lambda d: PostgresResetMaterializedViewPropertiesAction(d, parameters=["fillfactor"]), + ) + register_special_constructor( + "ddl.mv.PostgresAlterMaterializedViewExpression", + lambda d: PostgresAlterMaterializedViewExpression( + d, + view=MaterializedView(d, "mv"), + actions=[PostgresSetMaterializedViewPropertiesAction( + d, properties={"fillfactor": "70"} + )], + ), + ) + register_special_constructor( + "ddl.domain.PostgresAddDomainCheckAction", + lambda d: PostgresAddDomainCheckAction(d, _domain_check(d)), + ) + register_special_constructor( + "ddl.domain.PostgresAlterDomainExpression", + lambda d: PostgresAlterDomainExpression( + d, name="dom", action=RenameDomainAction(d, "dom2"), + ), + ) + + # -- expressions that need at least one member -------------------------- + register_special_constructor( + "advanced_functions.CaseExpression", + lambda d: CaseExpression( + d, + cases=[(_column_predicate(d), Literal(d, 1))], + else_result=Literal(d, 0), + ), + ) + register_special_constructor( + "advanced_functions.WindowSpecification", + lambda d: WindowSpecification(d, partition_by=["a"]), + ) + register_special_constructor( + "advanced_functions.WindowDefinition", + lambda d: WindowDefinition(d, "w", WindowSpecification(d, partition_by=["a"])), + ) + register_special_constructor( + "advanced_functions.WindowClause", + lambda d: WindowClause( + d, [WindowDefinition(d, "w", WindowSpecification(d, partition_by=["a"]))] + ), + ) + # An empty options dict is refused by the formatter, so a time-travel clause + # needs an actual option. + register_special_constructor( + "datetime.TemporalOptionsExpression", + lambda d: TemporalOptionsExpression(d, {"as_of": "2020-01-01"}), + ) + + # -- predicates whose column slot is an expression ---------------------- + # ``column`` is annotated ``Any`` -- it is genuinely either a column or a + # computed value -- so the guess supplies a string and the formatter calls + # ``.to_sql()`` on it. Core and PostgreSQL spell ILIKE as two classes with the + # same parameter shape, so both need the answer. + register_special_constructor( + "predicates.ILIKEExpression", + lambda d: CoreILIKEExpression(d, Column(d, "a"), "x%"), + ) + register_special_constructor( + "ilike.ILIKEExpression", + lambda d: PostgresILIKEExpression(d, Column(d, "a"), "x%"), + ) + + # -- COLLATE needs a collation PostgreSQL actually has ------------------- + # The formatter validates the name against the backend whitelist, and the + # guess's ``"x"`` is not on it. ``und-x-icu`` needs PostgreSQL 10+, so ``C`` is + # the one that is legal at every version. + register_special_constructor( + "collation.CollateExpression", + lambda d: CollateExpression(d, Column(d, "a"), "C"), + ) + + # -- MERGE -------------------------------------------------------------- + register_special_constructor( + "statements.dml.MergeAction", + lambda d: _merge_action(d), + ) + register_special_constructor( + "statements.dml.MergeExpression", + lambda d: MergeExpression( + d, + target_table=_table_obj(d), + source=NamedRelationRef(d, Table(d, "src")), + on_condition=_column_predicate(d), + when_matched=[_merge_action(d)], + ), + ) + + # -- SQL/XML ------------------------------------------------------------ + register_special_constructor( + "xml.XMLAttributesExpression", + lambda d: XMLAttributesExpression(d, [XMLAttribute(Literal(d, "v"), "a")]), + ) + register_special_constructor( + "xml.XMLForestExpression", + lambda d: XMLForestExpression(d, [XMLForestItem(Literal(d, "v"), "a")]), + ) + register_special_constructor( + "xml.XMLConcatExpression", + lambda d: XMLConcatExpression(d, [Literal(d, "a"), Literal(d, "b")]), + ) + register_special_constructor( + "xml.XMLTableExpression", + lambda d: XMLTableExpression( + d, + row_pattern=Literal(d, ""), + columns=[XMLTableColumn("a", "int")], + ), + ) + + # -- property graphs ---------------------------------------------------- + def node_table(d): + return NodeTable(d, "people") + + def edge_table(d): + return EdgeTableObject(d, "knows") + + def path_pattern(d): + return graph_mod.PathPattern( + d, graph_mod.GraphVertex(d, "n", node_table(d)) ) - return PostgresAdvisoryLockExpression(d, key=1) - register_special_constructor("locking.PostgresForUpdateExpression", for_update) - register_special_constructor("advisory.lock.PostgresAdvisoryLockExpression", advisory) + register_special_constructor( + "graph.GraphVertex", lambda d: graph_mod.GraphVertex(d, "n", node_table(d)) + ) + register_special_constructor( + "graph.GraphEdge", lambda d: graph_mod.GraphEdge(d, "e", edge_table(d)) + ) + register_special_constructor( + "graph.VertexTable", + lambda d: graph_mod.VertexTable(d, node_table(d), key_columns=["id"]), + ) + register_special_constructor( + "graph.EdgeTable", + lambda d: graph_mod.EdgeTable(d, edge_table(d), ["src"], ["dst"]), + ) + register_special_constructor( + "graph.QuantifiedPath", + lambda d: graph_mod.QuantifiedPath( + d, graph_mod.GraphEdge(d, "e", edge_table(d)), min_repeats=1, max_repeats=3 + ), + ) + register_special_constructor("graph.PathPattern", path_pattern) + register_special_constructor( + "graph.MatchClause", lambda d: graph_mod.MatchClause(d, path_pattern(d)) + ) + register_special_constructor( + "graph.ColumnsClause", + lambda d: graph_mod.ColumnsClause(d, graph_mod.GraphColumn("n", "id")), + ) + register_special_constructor( + "graph.GraphTableExpression", + lambda d: graph_mod.GraphTableExpression( + d, + graph=PropertyGraph(d, "g"), + match=graph_mod.MatchClause(d, path_pattern(d)), + columns=graph_mod.ColumnsClause(d, graph_mod.GraphColumn("n", "id")), + ), + ) + register_special_constructor( + "graph.CreatePropertyGraphExpression", + lambda d: graph_mod.CreatePropertyGraphExpression( + d, graph=PropertyGraph(d, "g"), + vertex_tables=[graph_mod.VertexTable(d, node_table(d))], + ), + ) + # The formatter accepts "add"/"drop" against "vertex tables"/"edge tables"/ + # "tables"; anything else is refused. + register_special_constructor( + "graph.AlterPropertyGraphExpression", + lambda d: graph_mod.AlterPropertyGraphExpression( + d, + graph=PropertyGraph(d, "g"), + action="add", + target="vertex tables", + vertex_tables=[graph_mod.VertexTable(d, node_table(d))], + ), + ) + register_special_constructor( + "graph.DropPropertyGraphExpression", + lambda d: graph_mod.DropPropertyGraphExpression(d, graph=PropertyGraph(d, "g")), + ) + + # -- types -------------------------------------------------------------- + # The element type must be bound to the dialect, because ``to_sql`` dispatches + # on it through its own dialect. + register_special_constructor( + "types.array.ArrayType", lambda d: ArrayType(d, VarCharType(d, 10)) + ) + register_special_constructor( + "types.PostgresArrayType", lambda d: PostgresArrayType(d, VarCharType(d, 10)) + ) + register_special_constructor( + "types.enum_.EnumType", lambda d: EnumType(d, values=["sad", "ok"]) + ) + + +register_specials() -_register_postgres_specials() +# --------------------------------------------------------------------------- +# Lists that cannot grow or shrink silently +# --------------------------------------------------------------------------- +#: Classes the generic introspective constructor cannot build. +#: +#: Each entry is a real coverage gap, named here so it is visible rather than +#: lost. ``test_unconstructible_list_is_exact`` pins the tuple in both +#: directions, so a class that gains a constructor fails here until this entry is +#: removed, and a class that starts failing to build fails here too -- neither can +#: become a quiet skip. +#: +#: Registering a constructor is how an entry is retired. +#: +#: Sorted, because the integrity test compares this against a sorted tuple of what +#: it observes. +UNCONSTRUCTIBLE = ( + # Every class that gets here is a real coverage gap: the matrix cannot assert + # anything about a statement it cannot build, and the skip it becomes looks + # exactly like the skip a legitimately unsupported statement earns. Register a + # constructor for any of them; that is the way to retire an entry. + # + # There are none left. That is not a claim about the classes -- it is what + # ``test_unconstructible_list_is_exact`` observes right now, and it is pinned + # in both directions so the first class that needs an exemption fails CI. +) + +#: Classes that construct but cannot render, for a reason belonging to their own +#: tree or to PostgreSQL's feature set rather than to a defect. Each entry pins +#: the exception type and a message fragment, so a class that starts failing for +#: a *different* reason fails here. +#: +#: Three entries pin ``UnsupportedFeatureError`` itself, the type +#: :func:`assert_sql_roundtrip_classified` allows unconditionally; what earns them +#: a place here is the fragment, which names the specific formatter that is +#: missing. See the module docstring for why they were not simply deleted. +_NO_FORMATTER = "does not declare its dialect formatting method name" + + +def _no_such_formatter(formatter_name: str) -> str: + """The message fragment ``to_sql()`` reports when the dialect lacks a formatter. + + ``BaseExpression.to_sql()`` resolves ``format_method`` against the bound + dialect and, when the name resolves to nothing, raises + ``UnsupportedFeatureError`` naming the method -- the same way every other + capability gap in the tree is reported, and the exception both migration + runners already catch. That lookup used to let the missing attribute escape + as a bare ``AttributeError``, which is a statement about Python's object + model rather than about the engine, and so said nothing about *why* the + statement cannot be rendered. + + Taking the formatter name as an argument keeps each pin naming the one + method its class actually dispatches on. A shared fragment would let + ``TableSource``, ``PivotExpression`` and ``UnpivotExpression`` start failing + for each other's reason and still pass. + """ + return f"does not support the '{formatter_name}' statement" + +LEGITIMATE_NON_RENDERS = { + # ---- a marker class that exists in order to refuse ------------------ + # ``_UnsupportedDomainAction`` is constructed precisely so an ALTER DOMAIN + # action PostgreSQL cannot express gets a message naming the action. The + # ValueError is the designed behaviour, not a defect -- and it is the only + # thing this class does. + "rhosocial.activerecord.backend.impl.postgres.expression.ddl.domain." + "_UnsupportedDomainAction": ( + ValueError, + "Unsupported ALTER DOMAIN action", + ), + + # ---- a core type PostgreSQL spells another way ------------------------ + # Core's generic ENUM type is for engines that dispatch on the name ``enum``. + # PostgreSQL has no such type -- ``CREATE TYPE ... AS ENUM`` goes through + # ``PostgresEnumType`` -- so there is no ``format_data_type_enum`` to reach. + "rhosocial.activerecord.backend.expression.types.enum_.EnumType": ( + TypeError, + "does not support the generic type 'enum'", + ), + + # ---- bases that name an expression category, not a renderable thing ---- + # Each of these is a base class that deliberately declares no + # ``format_method``, so ``to_sql()`` reports that there is nothing to + # dispatch. They are not ``inspect.isabstract`` -- they are concrete enough to + # construct -- so the walk keeps them, and each is pinned to its exact message + # so a base that started rendering fails here instead of passing quietly. + "rhosocial.activerecord.backend.expression.bases.SQLPredicate": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.bases.SQLValueExpression": ( + NotImplementedError, + _NO_FORMATTER, + ), + # The roots of the object tree. Each concrete object overrides + # ``format_method`` with its own ``format__object``; the base names only + # what every catalogue object has in common. + "rhosocial.activerecord.backend.expression.objects.base.SchemaObject": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.objects.relation.RelationObject": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.objects.routine.RoutineObject": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.objects.type_.TypeObject": ( + NotImplementedError, + _NO_FORMATTER, + ), + # Expression-category bases whose concrete members each name their own + # formatter: an ALTER TABLE action, an INSERT row source, a temporal value, an + # introspection query. + "rhosocial.activerecord.backend.expression.statements.ddl_alter.AlterTableAction": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.statements.dml.InsertDataSource": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.transaction.TransactionExpression": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.datetime._TemporalValueExpression": ( + NotImplementedError, + _NO_FORMATTER, + ), + "rhosocial.activerecord.backend.expression.introspection.IntrospectionExpression": ( + NotImplementedError, + _NO_FORMATTER, + ), + # The root of the type tree. Every concrete type declares its own generic + # ``name``, which is what ``format_data_type`` dispatches on; the root declares + # none, so there is nothing to dispatch. TypeError, not + # UnsupportedFeatureError, because the class is incomplete rather than the + # dialect being unable. + "rhosocial.activerecord.backend.expression.types._base.DataType": ( + TypeError, + "does not declare a valid generic type name", + ), + + # ---- the root of the row-source tree --------------------------------- + # The root of the row-source tree. No concrete source renders through + # ``format_table_source``; each overrides ``format_method``. Rendering the base + # would mean inventing SQL for an object that carries nothing but an alias. + # + # Pinned as a dialect capability gap rather than as the ``AttributeError`` the + # lookup used to leak, because that is what it is: PostgreSQL declares no + # ``format_table_source`` and is not going to. ``UnsupportedFeatureError`` is + # also what :func:`assert_sql_roundtrip_classified` treats as always allowed, + # so this entry now adds the message fragment on top of the type rather than + # substituting for it -- which is why it stays here instead of being deleted. + "rhosocial.activerecord.backend.expression.sources.base.TableSource": ( + UnsupportedFeatureError, + _no_such_formatter("format_table_source"), + ), + + # ---- core types PostgreSQL has no formatter for ---------------------- + # Core defines these two generic types for engines that spell them VARBINARY + # or BINARY. PostgreSQL has no such type -- bytea is modelled separately -- so + # there is no ``format_data_type_binary`` to dispatch to. A dialect gap, not a + # defect: the dialect is saying it cannot render them. + "rhosocial.activerecord.backend.expression.types.binary.BinaryType": ( + TypeError, + "does not support the generic type 'binary'", + ), + "rhosocial.activerecord.backend.expression.types.binary.VarBinaryType": ( + TypeError, + "does not support the generic type 'varbinary'", + ), + + # ---- SQL standard pivot, which is not PostgreSQL syntax -------------- + # Core models the SQL:2003 PIVOT / UNPIVOT for engines that spell it that way. + # PostgreSQL has no PIVOT keyword, so the dialect declares no + # ``format_pivot_expression``. Typing PIVOT is done with a ``CASE`` / + # ``generate_series`` plan, not with this expression, so the absence is a fact + # about the engine rather than a gap in this backend. Each entry names its own + # formatter so the two cannot pass for one another. + "rhosocial.activerecord.backend.expression.pivot.PivotExpression": ( + UnsupportedFeatureError, + _no_such_formatter("format_pivot_expression"), + ), + "rhosocial.activerecord.backend.expression.pivot.UnpivotExpression": ( + UnsupportedFeatureError, + _no_such_formatter("format_unpivot_expression"), + ), +} + + +def _channels_that_lose_params(fqn, instance, dialect): + """The encodings whose round-trip does not restore ``get_params()``. + + Returns ``{channel: why}``. An empty dict means the class round-trips + everywhere, which every class in the matrix must do -- there is no exemption + list, because there has never been a reason for one. + """ + original = instance.get_params() + lost = {} + for channel, decoded in ( + ("dict", deserialize(serialize(instance), dialect)), + ("json", deserialize_json(serialize_json(instance), dialect)), + ("xml", deserialize_xml(serialize_xml(instance), dialect)), + ): + try: + assert_params_equal(decoded.get_params(), original, fqn) + except AssertionError as exc: + lost[channel] = str(exc).splitlines()[0] + return lost + + +# --------------------------------------------------------------------------- +# The local SQL assertion: classify the outcome instead of swallowing it +# --------------------------------------------------------------------------- + + +def assert_sql_roundtrip_classified(fqn, instance, dialect): + """Assert an expression's SQL survives the round-trip, or say precisely why not. + + Four outcomes, each asserted: + + * **renders** -- every encoding that preserved the parameters must restore + byte-identical SQL *and* byte-identical bind parameters. + * ``UnsupportedFeatureError`` -- the dialect does not model the feature. + Asserted as exactly that type, so a formatter raising it for an unrelated + reason is still visible as that type rather than as a pass. Checked first, + before :data:`LEGITIMATE_NON_RENDERS`, so a class pinned with that same + type is classified here and its fragment is enforced by + ``test_pinned_non_render_really_does_not_render`` instead. + * a member of :data:`LEGITIMATE_NON_RENDERS` -- unrenderable by design, + asserted as its exact type *and* message fragment. + * **anything else** -- a failure naming the class and the exception. + + Returns a short string naming the branch taken. + + Raises: + AssertionError: On a round-trip mismatch, on an unexpected exception type, + or when a class's rendering outcome changed. + """ + try: + expected_sql, expected_params = instance.to_sql() + except UnsupportedFeatureError as exc: + assert type(exc) is UnsupportedFeatureError, fqn + return "unsupported" + except Exception as exc: + if fqn not in LEGITIMATE_NON_RENDERS: + raise AssertionError( + f"{fqn}: to_sql() raised {type(exc).__name__}, which is neither a " + f"render nor a classified non-render, and this is a defect.\n" + f" UnsupportedFeatureError means the dialect lacks the feature and " + f"is always allowed.\n" + f" A class that cannot render for a reason belonging to its own " + f"tree or to PostgreSQL's feature set belongs in " + f"LEGITIMATE_NON_RENDERS.\n" + f" Exception: {exc}" + ) from exc + expected_type, fragment = LEGITIMATE_NON_RENDERS[fqn] + assert type(exc) is expected_type, ( + f"{fqn}: LEGITIMATE_NON_RENDERS pins this class as a legitimate " + f"non-render raising {expected_type.__name__}, but it raised " + f"{type(exc).__name__}: {exc}" + ) + assert fragment in str(exc), ( + f"{fqn}: expected {expected_type.__name__} and was expected to say " + f"{fragment!r}, but it said: {exc}" + ) + return "non-render" + + for channel, decoded in ( + ("dict", deserialize(serialize(instance), dialect)), + ("json", deserialize_json(serialize_json(instance), dialect)), + ("xml", deserialize_xml(serialize_xml(instance), dialect)), + ): + decoded_sql, decoded_params = decoded.to_sql() + assert decoded_sql == expected_sql, ( + f"{fqn}: {channel} round-trip changed the SQL.\n" + f" original: {expected_sql!r}\n" + f" {channel}: {decoded_sql!r}" + ) + assert decoded_params == expected_params, ( + f"{fqn}: {channel} round-trip changed the bind parameters.\n" + f" original: {expected_params!r}\n" + f" {channel}: {decoded_params!r}" + ) + return "rendered" -@pytest.fixture(params=[fqn for fqn in sorted(CLASSES)], ids=sorted(CLASSES)) -def postgres_expr_case(request, postgres_dialect): + +@pytest.fixture( + params=[fqn for fqn in sorted(REGISTERED)], ids=sorted(REGISTERED) +) +def expr_case(request, postgres_dialect): fqn = request.param - cls = CLASSES[fqn] + cls = REGISTERED[fqn] instance, source = make_instance(cls, postgres_dialect) if instance is None: - pytest.skip(f"{fqn}: {source}") + assert fqn in UNCONSTRUCTIBLE, ( + f"{fqn} cannot be built by the generic constructor ({source}) and is " + f"not in UNCONSTRUCTIBLE. Either register a special constructor for " + f"it or add it to the tuple with a reason -- do not let it disappear " + f"into a skip." + ) + pytest.skip( + f"{fqn}: pinned in UNCONSTRUCTIBLE, cannot be constructed ({source})" + ) return fqn, instance -class TestPostgresExpressionRoundtrip: - """All constructible PostgreSQL expression classes round-trip losslessly.""" +class TestExpressionRoundtripAll: + """All constructible expression classes round-trip through all encodings.""" - def test_get_params_roundtrip(self, postgres_expr_case, postgres_dialect): - fqn, instance = postgres_expr_case - roundtrip_expression(fqn, instance, postgres_dialect) + def test_get_params_roundtrip_across_encodings(self, expr_case, postgres_dialect): + fqn, instance = expr_case + lost = _channels_that_lose_params(fqn, instance, postgres_dialect) + assert not lost, ( + f"{fqn} does not survive the round-trip through {sorted(lost)}.\n" + + "\n".join(f" {channel}: {why}" for channel, why in lost.items()) + + "\nThe serialiser encodes dataclasses field by field and BaseExpression " + "nodes structurally. A payload that is neither is written out as null, " + "so a round-trip silently drops it. Make it a dataclass -- the " + "constructor signature is what get_params() reads, so it does not have " + "to change." + ) - def test_to_sql_consistent(self, postgres_expr_case, postgres_dialect): - fqn, instance = postgres_expr_case - sql_consistent(fqn, instance, postgres_dialect) + def test_to_sql_roundtrip_classified(self, expr_case, postgres_dialect): + """A render must survive the round-trip; a non-render must be classified.""" + fqn, instance = expr_case + assert_sql_roundtrip_classified(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 +class TestMatrixIntegrity: + """Guards on the matrix and its lists, so neither can quietly change.""" - 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 + def test_unconstructible_list_is_exact(self, postgres_dialect): + """Pin the unconstructible tuple against what the constructor really skips. + + Two directions are checked. A class named here that now builds has gained a + constructor and the entry is stale; a class that fails to build without + being named would become a silent skip. Both fail here. + """ + dialect = postgres_dialect + actual = tuple( + sorted( + fqn + for fqn in REGISTERED + if make_instance(REGISTERED[fqn], dialect)[0] is None + ) + ) + assert actual == tuple(sorted(UNCONSTRUCTIBLE)), ( + "the set of expression classes the generic constructor cannot build " + "changed.\n" + f" now skipped but not named: " + f"{sorted(set(actual) - set(UNCONSTRUCTIBLE))}\n" + f" named but now built: " + f"{sorted(set(UNCONSTRUCTIBLE) - set(actual))}\n" + "Each new entry needs a reason in the comment above UNCONSTRUCTIBLE." + ) + + def test_unconstructible_entries_are_real_classes(self): + """Every entry names a class that was actually collected. + + A typo in the tuple would otherwise exempt nothing while still reading as + a deliberate decision. + """ + unknown = set(UNCONSTRUCTIBLE) - set(REGISTERED) + assert not unknown, ( + f"UNCONSTRUCTIBLE names classes that were not registered: {sorted(unknown)}" + ) + + def test_legitimate_non_renders_are_real_classes(self): + """Every pinned non-render names a class that was actually collected.""" + unknown = set(LEGITIMATE_NON_RENDERS) - set(REGISTERED) + assert not unknown, ( + f"LEGITIMATE_NON_RENDERS names classes that were not registered: " + f"{sorted(unknown)}" + ) + + def test_pinned_non_render_really_does_not_render(self, postgres_dialect): + """Each pinned entry still raises what it claims, for the stated reason. + + Without this, an entry could sit in the dict for a class that renders + perfectly well, and the matrix would be asserting nothing about it. + """ + for fqn, (expected_type, fragment) in LEGITIMATE_NON_RENDERS.items(): + instance, source = make_instance(REGISTERED[fqn], postgres_dialect) + assert instance is not None, ( + f"{fqn} is pinned as a non-render but could not be constructed " + f"({source})" + ) + with pytest.raises(expected_type) as exc_info: + instance.to_sql() + assert fragment in str(exc_info.value), ( + f"{fqn}: expected the message to mention {fragment!r}, got: " + f"{exc_info.value}" + ) + + def test_matrix_covers_both_packages_completely(self): + """The matrix covers every concrete class both packages define. + + Re-walked here rather than trusting the module-level collection, so a class + that appeared after import is caught. The package walk is used rather than + the registry because the registry also holds whatever backends other test + modules happened to import. + """ + ExpressionRegistry._auto_register_builtins() + expected = set(_collect_matrix_classes()) + stray = [ + fqn + for fqn in REGISTERED + if not fqn.startswith((f"{PG_EXPR_PKG}.", f"{CORE_EXPR_PKG}.")) + ] + assert not stray, f"classes outside the two packages are in the matrix: {stray}" + assert expected == set(REGISTERED), ( + "the set of classes the two packages define changed after collection.\n" + f" now defined but not covered: {sorted(expected - set(REGISTERED))}\n" + f" covered but no longer defined: {sorted(set(REGISTERED) - expected)}" + ) + + def test_matrix_collects_core_expressions_too(self): + """Core's own expression classes are in the matrix, not only ours. + + This is the specific gap that let ``mixins/ddl/comment.py`` read a field + core had removed and stay green: core's classes were outside the only + matrix in this repository. Removing this assertion would restore that + blindness silently. + """ + core_classes = [ + fqn for fqn in REGISTERED if fqn.startswith(f"{CORE_EXPR_PKG}.") + ] + assert core_classes, ( + "the matrix collects no core expression classes; core statements " + "rendered by this dialect would go unasserted" + ) + + def test_every_covered_class_is_registered_for_deserialization(self): + """A class in the matrix can be found again when deserializing. + + Deserialisation looks the class up by name, so a class the matrix renders + but the registry cannot resolve would round-trip into the wrong thing or + nothing at all. + """ + ExpressionRegistry._auto_register_builtins() + unresolved = sorted(set(REGISTERED) - set(ExpressionRegistry._registry)) + assert not unresolved, ( + f"the matrix covers classes the registry cannot resolve: {unresolved}" + ) + + def test_coverage_report(self, postgres_dialect): + """Surface what the matrix covers, so coverage stays transparent.""" + ExpressionRegistry._auto_register_builtins() + constructible = [ + fqn + for fqn in REGISTERED + if make_instance(REGISTERED[fqn], postgres_dialect)[0] is not None + ] + assert constructible + print( + f"\nexpression matrix: {len(constructible)} constructible, " + f"{len(UNCONSTRUCTIBLE)} pinned-unconstructible, " + f"{len(REGISTERED)} registered" + ) + for fqn in UNCONSTRUCTIBLE: + print(f" not constructible: {fqn}") \ No newline at end of file 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 1feedbf9..7fa02c07 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 @@ -20,10 +20,10 @@ LogicalPredicate, QueryExpression, RawSQLExpression, - TableExpression, UpdateExpression, ValuesSource, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import PartitionClause, PartitionStrategy from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresAttachPartitionExpression, @@ -55,14 +55,14 @@ def _drop_table_expression(dialect, table_name: str, *, schema: str | None = None): - table = TableExpression(dialect, table_name, schema_name=schema) if schema else table_name + table = Table(dialect, table_name, schema_name=schema) return DropTableExpression(dialect=dialect, table=table, if_exists=True, cascade=True) def _create_list_parent_expression(dialect): return CreateTableExpression( dialect=dialect, - table=LIST_PARENT, + table=Table(dialect, LIST_PARENT), columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "status", TextType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), @@ -90,7 +90,7 @@ def _create_list_partition_sql(dialect, partition_name: str, values: Iterable[st def _create_hash_parent_expression(dialect): return CreateTableExpression( dialect=dialect, - table=HASH_PARENT, + table=Table(dialect, HASH_PARENT), columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), ColumnDefinition(dialect, "bucket", BigIntType(dialect=dialect), constraints=[ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)]), @@ -118,7 +118,7 @@ def _create_hash_partition_sql(dialect, partition_name: str, remainder: int): def _create_range_parent_expression(dialect): return CreateTableExpression( dialect=dialect, - table=RANGE_PARENT, + table=Table(dialect, RANGE_PARENT), columns=[ 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)]), @@ -146,7 +146,7 @@ def _create_range_partition_sql(dialect, partition_name: str, start: str, end: s def _insert_rows_expression(dialect, table_name: str, columns: list[str], rows: list[list[Any]]): return InsertExpression( dialect=dialect, - into=table_name, + into=Table(dialect, table_name), columns=columns, source=ValuesSource( dialect, @@ -162,7 +162,7 @@ def _select_tableoid_payload_expression(dialect, table_name: str): RawSQLExpression(dialect, "tableoid::regclass::text AS partition_name"), Column(dialect, "payload"), ], - from_=TableExpression(dialect, table_name), + from_=Table(dialect, table_name), ) @@ -320,7 +320,7 @@ def test_default_attach_and_detach_workflow(self, postgres_backend, postgres_ran # Create a table with matching structure for DEFAULT attachment create_default = CreateTableExpression( dialect=dialect, - table=default_partition_name, + table=Table(dialect, default_partition_name), columns=[ 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)]), @@ -380,7 +380,7 @@ def test_metadata_include_partitions_false_and_schema_filter(self, postgres_back for schema in (SCHEMA_A, SCHEMA_B): parent = CreateTableExpression( dialect=dialect, - table=TableExpression(dialect, SCHEMA_PARENT, schema_name=schema), + table=Table(dialect, SCHEMA_PARENT, schema_name=schema), columns=[ 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)]), @@ -434,7 +434,7 @@ def test_partition_key_update_moves_row_between_partitions( ) update = UpdateExpression( dialect=dialect, - table=RANGE_PARENT, + table=Table(dialect, RANGE_PARENT), assignments={"created_at": Literal(dialect, "2026-02-15")}, where=Column(dialect, "id") == Literal(dialect, 1), ) @@ -447,7 +447,7 @@ def test_partition_key_update_moves_row_between_partitions( invalid_update = UpdateExpression( dialect=dialect, - table=RANGE_PARENT, + table=Table(dialect, RANGE_PARENT), assignments={"created_at": Literal(dialect, "2026-03-15")}, where=Column(dialect, "id") == Literal(dialect, 1), ) @@ -472,7 +472,7 @@ def test_json_explain_pruning_reports_target_partition( query = QueryExpression( dialect, select=[Column(dialect, "payload")], - from_=TableExpression(dialect, RANGE_PARENT), + from_=Table(dialect, RANGE_PARENT), where=LogicalPredicate( dialect, "AND", @@ -559,7 +559,7 @@ async def test_partition_key_update_moves_row_between_partitions( ) update = UpdateExpression( dialect=dialect, - table=RANGE_PARENT, + table=Table(dialect, RANGE_PARENT), assignments={"created_at": Literal(dialect, "2026-02-15")}, where=Column(dialect, "id") == Literal(dialect, 1), ) 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 7e34d2ec..407f1633 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 @@ -19,10 +19,10 @@ InsertExpression, Literal, QueryExpression, - TableExpression, ValuesSource, WildcardExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.statements import ( ExplainExpression, ExplainFormat, @@ -59,13 +59,13 @@ def _iter_partitions(): def _drop_table_sql(dialect, table_name: str): - return DropTableExpression(dialect=dialect, table=table_name, if_exists=True).to_sql() + return DropTableExpression(dialect=dialect, table=Table(dialect, table_name), if_exists=True).to_sql() def _create_parent_table_expression(dialect): return CreateTableExpression( dialect=dialect, - table=PARTITION_EXPLAIN_TABLE, + table=Table(dialect, PARTITION_EXPLAIN_TABLE), if_not_exists=False, columns=[ ColumnDefinition(dialect, "id", BigIntType(dialect=dialect), @@ -109,7 +109,7 @@ def _seed_rows_expression(dialect): ] return InsertExpression( dialect=dialect, - into=PARTITION_EXPLAIN_TABLE, + into=Table(dialect, PARTITION_EXPLAIN_TABLE), columns=["id", "created_at", "tenant_id", "payload"], source=ValuesSource( dialect, @@ -122,7 +122,7 @@ def _range_query_expression(dialect, start, end): return QueryExpression( dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, PARTITION_EXPLAIN_TABLE), + from_=Table(dialect, PARTITION_EXPLAIN_TABLE), where=(Column(dialect, "created_at") >= Literal(dialect, start)) & (Column(dialect, "created_at") < Literal(dialect, end)), ) @@ -132,7 +132,7 @@ def _full_scan_query_expression(dialect): return QueryExpression( dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, PARTITION_EXPLAIN_TABLE), + from_=Table(dialect, PARTITION_EXPLAIN_TABLE), ) 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 f6fb2d27..d5a7113a 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 @@ -23,10 +23,10 @@ OrderByClause, QualifiedIdentifierExpression, QueryExpression, - TableExpression, ValuesSource, WildcardExpression, ) +from rhosocial.activerecord.backend.expression.objects import Index, Table from rhosocial.activerecord.backend.expression.statements import ( ColumnDefinition, CreateTableExpression, @@ -81,7 +81,7 @@ def _qualified(table_name: str) -> str: def _create_partitioned_parent_sql(dialect, table_name: str): expr = CreateTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), columns=[ ColumnDefinition( dialect, @@ -135,7 +135,7 @@ def _create_default_partition_sql(dialect, partition_name: str): def _delete_partition_events_for_range_expression(dialect, start, end): return DeleteExpression( dialect=dialect, - tables="ar_partition_events", + tables=Table(dialect, 'ar_partition_events'), where=(Column(dialect, "created_at") >= Literal(dialect, start)) & (Column(dialect, "created_at") < Literal(dialect, end)), ) @@ -144,7 +144,7 @@ def _delete_partition_events_for_range_expression(dialect, start, end): def _insert_partition_events_expression(dialect, rows): return InsertExpression( dialect=dialect, - into="ar_partition_events", + into=Table(dialect, 'ar_partition_events'), columns=["id", "created_at", "payload"], source=ValuesSource( dialect, @@ -176,7 +176,7 @@ def _attach_partition_sql(dialect, partition_name: str, from_value: str, to_valu def _drop_table_sql(dialect, table_name: str): expr = DropTableExpression( dialect=dialect, - table=table_name, + table=Table(dialect, table_name), if_exists=True, cascade=True, ) @@ -186,7 +186,7 @@ def _drop_table_sql(dialect, table_name: str): def _truncate_table_sql(dialect, table_name: str): expr = TruncateExpression( dialect=dialect, - table_name=table_name, + table=Table(dialect, table_name), ) return expr.to_sql() @@ -219,7 +219,7 @@ def _partition_metadata_sql(dialect, table_name: str): def _create_production_parent_sql(dialect): expr = CreateTableExpression( dialect=dialect, - table=PRODUCTION_PARTITION_TABLE, + table=Table(dialect, PRODUCTION_PARTITION_TABLE), columns=[ ColumnDefinition( dialect, @@ -303,8 +303,8 @@ def _attach_production_partition_sql(dialect, partition_name: str, from_value: s def _create_production_index_sql(dialect, table_name: str): expr = CreateIndexExpression( dialect, - f"idx_{table_name}_tenant_created_at", - table_name, + Index(dialect, f'idx_{table_name}_tenant_created_at'), + Table(dialect, table_name), ["tenant_id", "created_at"], if_not_exists=True, ) @@ -314,7 +314,7 @@ def _create_production_index_sql(dialect, table_name: str): def _insert_production_events_expression(dialect, rows): return InsertExpression( dialect=dialect, - into=PRODUCTION_PARTITION_TABLE, + into=Table(dialect, PRODUCTION_PARTITION_TABLE), columns=["id", "created_at", "tenant_id", "payload"], source=ValuesSource( dialect, @@ -337,7 +337,7 @@ def _select_production_payloads_expression(dialect, start, end, *, tenant_id: Op return QueryExpression( dialect, select=[Column(dialect, "payload")], - from_=TableExpression(dialect, PRODUCTION_PARTITION_TABLE), + from_=Table(dialect, PRODUCTION_PARTITION_TABLE), where=predicate, order_by=OrderByClause(dialect, [(Column(dialect, "id"), "ASC")]), ) @@ -347,7 +347,7 @@ def _production_range_query_expression(dialect, start, end, *, tenant_id: int): return QueryExpression( dialect, select=[WildcardExpression(dialect)], - from_=TableExpression(dialect, PRODUCTION_PARTITION_TABLE), + from_=Table(dialect, PRODUCTION_PARTITION_TABLE), where=LogicalPredicate( dialect, "AND", @@ -362,7 +362,7 @@ def _select_count_expression(dialect, table_name: str): return QueryExpression( dialect, select=[FunctionCall(dialect, "COUNT", WildcardExpression(dialect)).as_("count")], - from_=TableExpression(dialect, table_name), + from_=Table(dialect, table_name), ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_database_ddl.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_database_ddl.py index ff0800b9..c2fe9cae 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_database_ddl.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_database_ddl.py @@ -1,6 +1,7 @@ # tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_database_ddl.py """Explicit PostgresDialect database DDL capability + rendering tests.""" +from rhosocial.activerecord.backend.expression.objects import Database from rhosocial.activerecord.backend.expression.statements.ddl_database import ( CreateDatabaseExpression, DropDatabaseExpression, @@ -19,11 +20,13 @@ def test_database_capabilities(): def test_create_database_renders(): - sql, params = CreateDatabaseExpression(_dialect(), database_name="app").to_sql() + dialect = _dialect() + sql, params = CreateDatabaseExpression(dialect, Database(dialect, "app")).to_sql() assert "CREATE DATABASE" in sql assert params == () def test_drop_database_renders(): - sql, _ = DropDatabaseExpression(_dialect(), database_name="app").to_sql() + dialect = _dialect() + sql, _ = DropDatabaseExpression(dialect, Database(dialect, "app")).to_sql() assert "DROP DATABASE" in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py index eb689c87..18986a57 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py @@ -13,8 +13,8 @@ Literal, LogicalPredicate, QueryExpression, - TableExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression.functions import ( date_add, date_diff, @@ -150,7 +150,7 @@ def _datetime_expression_query(dialect): dialect, "minute", Column(dialect, "started_at"), Column(dialect, "ended_at") ).as_("duration_minutes"), ], - from_=TableExpression(dialect, "explain_temporal_events"), + from_=Table(dialect, "explain_temporal_events"), where=_category_created_filter(dialect), order_by=OrderByClause( dialect, @@ -178,7 +178,7 @@ def test_created_at_range_explain_returns_temporal_plan( QueryExpression( dialect, select=[Column(dialect, "id"), Column(dialect, "created_at")], - from_=TableExpression(dialect, "explain_temporal_events"), + from_=Table(dialect, "explain_temporal_events"), where=_range_filter( dialect, "created_at", @@ -208,7 +208,7 @@ def test_datetime_range_index_is_available_when_seqscan_disabled( Column(dialect, "category"), Column(dialect, "created_at"), ], - from_=TableExpression(dialect, "explain_temporal_events"), + from_=Table(dialect, "explain_temporal_events"), where=_category_created_filter(dialect), ) ) @@ -260,7 +260,7 @@ async def test_datetime_range_index_is_available_when_seqscan_disabled( Column(dialect, "category"), Column(dialect, "created_at"), ], - from_=TableExpression(dialect, "explain_temporal_events"), + from_=Table(dialect, "explain_temporal_events"), where=_category_created_filter(dialect), ) ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_ddl_improvements.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_ddl_improvements.py index f06ed4d7..00a9576c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_ddl_improvements.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_ddl_improvements.py @@ -5,11 +5,11 @@ from rhosocial.activerecord.backend.expression import ( Column, - TableExpression, QueryExpression, CreateViewExpression, DropViewExpression, ) +from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError 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 3ffd1890..ae1096ae 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 @@ -26,6 +26,7 @@ from rhosocial.activerecord.backend.expression.core import CastExpression from rhosocial.activerecord.backend.impl.postgres.expression.ddl import PartitionValue from typing import Tuple, Any # noqa: F401 +from rhosocial.activerecord.backend.expression.objects import Function, Table, Trigger @pytest.fixture @@ -366,11 +367,11 @@ def test_trigger_function_name_quoted(self, dialect): expr = CreateTriggerExpression( dialect=dialect, - trigger_name="my_trigger", - table_name="users", + trigger=Trigger(dialect, 'my_trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.INSERT], - function_name="my_function", + function=Function(dialect, 'my_function'), ) sql, params = expr.to_sql() @@ -387,11 +388,11 @@ def test_trigger_function_name_special_chars_quoted(self, dialect): expr = CreateTriggerExpression( dialect=dialect, - trigger_name="trigger", - table_name="users", + trigger=Trigger(dialect, 'trigger'), + table=Table(dialect, 'users'), timing=TriggerTiming.BEFORE, events=[TriggerEvent.INSERT], - function_name="Function With Spaces", + function=Function(dialect, 'Function With Spaces'), ) sql, params = expr.to_sql() 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 379709ed..b38f1b62 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 @@ -105,27 +105,70 @@ def get_postgres_protocols(): dialect_protocols.WildcardSupport, dialect_protocols.JoinSupport, dialect_protocols.SetOperationSupport, - dialect_protocols.ViewSupport, - dialect_protocols.SchemaSupport, - dialect_protocols.IndexSupport, - dialect_protocols.SequenceSupport, - dialect_protocols.TableSupport, + # Named objects. Core split naming out of the DDL umbrella protocols, + # so each kind a PostgreSQL statement can name is listed on its own. + dialect_protocols.NamespaceSupport, + dialect_protocols.TableObjectSupport, + dialect_protocols.ViewObjectSupport, + dialect_protocols.MaterializedViewObjectSupport, + dialect_protocols.ForeignTableObjectSupport, + dialect_protocols.IndexObjectSupport, + dialect_protocols.SequenceObjectSupport, + dialect_protocols.TriggerObjectSupport, + dialect_protocols.TypeObjectSupport, + dialect_protocols.RoutineObjectSupport, + # Per-statement DDL protocols. One per statement expression: PostgreSQL + # implements each of these, so it declares each. + dialect_protocols.CreateTableSupport, + dialect_protocols.DropTableSupport, + dialect_protocols.AlterTableSupport, + dialect_protocols.CreateIndexSupport, + dialect_protocols.DropIndexSupport, + dialect_protocols.CreateViewSupport, + dialect_protocols.DropViewSupport, + dialect_protocols.MaterializedViewSupport, + dialect_protocols.CreateSequenceSupport, + dialect_protocols.DropSequenceSupport, + dialect_protocols.AlterSequenceSupport, + dialect_protocols.CreateTriggerSupport, + dialect_protocols.DropTriggerSupport, + dialect_protocols.CreateTypeSupport, + dialect_protocols.AlterTypeSupport, + dialect_protocols.DropTypeSupport, + dialect_protocols.CreateDomainSupport, + dialect_protocols.AlterDomainSupport, + dialect_protocols.DropDomainSupport, + dialect_protocols.CreateSchemaSupport, + dialect_protocols.DropSchemaSupport, + dialect_protocols.CreateDatabaseSupport, + dialect_protocols.DropDatabaseSupport, + dialect_protocols.AlterDatabaseSupport, + dialect_protocols.CreateRoutineSupport, + dialect_protocols.DropRoutineSupport, dialect_protocols.PartitionSupport, dialect_protocols.ConstraintSupport, dialect_protocols.TruncateSupport, dialect_protocols.IntrospectionSupport, dialect_protocols.TransactionControlSupport, dialect_protocols.SQLFunctionSupport, - # Generic protocols Postgres also satisfies (previously omitted). + # Generic protocols Postgres also satisfies. dialect_protocols.AlterTableModifierSupport, dialect_protocols.DDLTypeSupport, - dialect_protocols.UserDefinedTypeSupport, - dialect_protocols.DomainSupport, dialect_protocols.GraphTableSupport, dialect_protocols.ILIKESupport, - dialect_protocols.TriggerSupport, dialect_protocols.AutoIncrementSupport, dialect_protocols.GeneratedColumnSupport, + # Statement forms PostgreSQL implements through its mixins, so the + # partition test below can classify them rather than leaving them + # unclassified. + dialect_protocols.CreateTableAsSupport, + dialect_protocols.CreateTableCloneSupport, + dialect_protocols.CreateTableLikeSupport, + dialect_protocols.CreateTableUsingTemplateSupport, + dialect_protocols.DateTimeSupport, + dialect_protocols.DqlOrderSupport, + dialect_protocols.FulltextIndexSupport, + dialect_protocols.SynonymObjectSupport, ] postgres_mro = postgres_dialect.PostgresDialect.__mro__ @@ -167,11 +210,9 @@ def test_implements_protocol(self, dialect, protocol): # conscious decision (move to POSTGRES_PROTOCOLS or revert). POSTGRES_NOT_IMPLEMENTED = [ # --- Intentional non-support --- - # The generic DatabaseSupport protocol is not composed by PostgresDialect. - dialect_protocols.DatabaseSupport, - # Postgres exposes routine DDL through its own PostgresRoutineSupport - # protocol rather than the generic SQL/PSM FunctionSupport. - dialect_protocols.FunctionSupport, + # PIVOT is not a PostgreSQL statement form. A dialect that reports support + # has to render one, and nothing here does. + dialect_protocols.PivotSupport, ] @@ -207,7 +248,12 @@ def test_positive_and_negative_lists_partition_all_protocols(self): all_protos = set(get_all_generic_protocols()) if dialect_protocols.DDLTypeSupport is dialect_protocols.DataTypeSupport: all_protos.discard("DDLTypeSupport") - positive = {p.__name__ for p in POSTGRES_PROTOCOLS if p.__module__ == dialect_protocols.__name__} + positive = { + p.__name__ + for p in POSTGRES_PROTOCOLS + if p.__module__ == dialect_protocols.__name__ + or p.__module__.startswith(dialect_protocols.__name__ + ".") + } negative = {p.__name__ for p in POSTGRES_NOT_IMPLEMENTED} overlap = positive & negative @@ -229,28 +275,41 @@ def test_no_interface_overlap_between_protocols(self): proto.__name__: get_all_protocol_methods(proto) for proto in POSTGRES_PROTOCOLS } + class_map = {proto.__name__: proto for proto in POSTGRES_PROTOCOLS} for name, members in member_map.items(): assert len(members) > 0, f"Protocol {name} has no members defined" excluded_overlaps = { # Generic -> Postgres derivation pairs (expected overlap) - ('IndexSupport', 'PostgresIndexSupport'), - ('PostgresIndexSupport', 'IndexSupport'), - ('TableSupport', 'PostgresTableSupport'), - ('PostgresTableSupport', 'TableSupport'), + ('NamespaceSupport', 'PostgresNamespaceSupport'), + ('PostgresNamespaceSupport', 'NamespaceSupport'), + ('IndexObjectSupport', 'PostgresIndexSupport'), + ('PostgresIndexSupport', 'IndexObjectSupport'), + ('TableObjectSupport', 'PostgresTableSupport'), + ('PostgresTableSupport', 'TableObjectSupport'), ('PartitionSupport', 'PostgresPartitionSupport'), ('PostgresPartitionSupport', 'PartitionSupport'), ('ConstraintSupport', 'PostgresConstraintSupport'), ('PostgresConstraintSupport', 'ConstraintSupport'), ('LockingSupport', 'PostgresLockingSupport'), ('PostgresLockingSupport', 'LockingSupport'), - ('TriggerSupport', 'PostgresTriggerSupport'), - ('PostgresTriggerSupport', 'TriggerSupport'), - ('UserDefinedTypeSupport', 'PostgresTypeSupport'), - ('PostgresTypeSupport', 'UserDefinedTypeSupport'), - ('DomainSupport', 'PostgresDomainSupport'), - ('PostgresDomainSupport', 'DomainSupport'), + ('TriggerObjectSupport', 'PostgresTriggerSupport'), + ('PostgresTriggerSupport', 'TriggerObjectSupport'), + ('CreateTypeSupport', 'PostgresTypeSupport'), + ('PostgresTypeSupport', 'CreateTypeSupport'), + ('AlterTypeSupport', 'PostgresTypeSupport'), + ('PostgresTypeSupport', 'AlterTypeSupport'), + ('DropTypeSupport', 'PostgresTypeSupport'), + ('PostgresTypeSupport', 'DropTypeSupport'), + ('TypeObjectSupport', 'PostgresTypeSupport'), + ('PostgresTypeSupport', 'TypeObjectSupport'), + ('CreateDomainSupport', 'PostgresDomainSupport'), + ('PostgresDomainSupport', 'CreateDomainSupport'), + ('AlterDomainSupport', 'PostgresDomainSupport'), + ('PostgresDomainSupport', 'AlterDomainSupport'), + ('DropDomainSupport', 'PostgresDomainSupport'), + ('PostgresDomainSupport', 'DropDomainSupport'), # SQLXMLSupport aggregates standard SQL/XML capability-family protocols ('SQLXMLSupport', 'SQLXMLParsingSupport'), ('SQLXMLParsingSupport', 'SQLXMLSupport'), @@ -304,15 +363,36 @@ def test_no_interface_overlap_between_protocols(self): ('PostgresJoinSupport', 'JoinSupport'), ('SetOperationSupport', 'PostgresSetOperationSupport'), ('PostgresSetOperationSupport', 'SetOperationSupport'), - ('ViewSupport', 'PostgresViewSupport'), - ('PostgresViewSupport', 'ViewSupport'), + ('ViewObjectSupport', 'PostgresViewSupport'), + ('PostgresViewSupport', 'ViewObjectSupport'), # PG materialized-view protocol restates the generic view formatter - ('ViewSupport', 'PostgresMaterializedViewSupport'), - ('PostgresMaterializedViewSupport', 'ViewSupport'), - ('SchemaSupport', 'PostgresSchemaSupport'), - ('PostgresSchemaSupport', 'SchemaSupport'), - ('SequenceSupport', 'PostgresSequenceSupport'), - ('PostgresSequenceSupport', 'SequenceSupport'), + ('MaterializedViewObjectSupport', 'PostgresMaterializedViewSupport'), + ('PostgresMaterializedViewSupport', 'MaterializedViewObjectSupport'), + ('MaterializedViewSupport', 'PostgresMaterializedViewSupport'), + ('PostgresMaterializedViewSupport', 'MaterializedViewSupport'), + # PG protocols restate the generic statement DDL they override. + # Core split the former umbrella protocols per statement, so the + # restatement is now against each half rather than one protocol. + ('CreateTableSupport', 'PostgresTableSupport'), + ('PostgresTableSupport', 'CreateTableSupport'), + ('CreateTableLikeSupport', 'PostgresTableSupport'), + ('PostgresTableSupport', 'CreateTableLikeSupport'), + ('CreateViewSupport', 'PostgresViewSupport'), + ('PostgresViewSupport', 'CreateViewSupport'), + ('DropViewSupport', 'PostgresViewSupport'), + ('PostgresViewSupport', 'DropViewSupport'), + ('CreateSequenceSupport', 'PostgresSequenceSupport'), + ('PostgresSequenceSupport', 'CreateSequenceSupport'), + ('DropSequenceSupport', 'PostgresSequenceSupport'), + ('PostgresSequenceSupport', 'DropSequenceSupport'), + ('CreateTriggerSupport', 'PostgresTriggerSupport'), + ('PostgresTriggerSupport', 'CreateTriggerSupport'), + ('DropTriggerSupport', 'PostgresTriggerSupport'), + ('PostgresTriggerSupport', 'DropTriggerSupport'), + ('CreateSchemaSupport', 'PostgresSchemaSupport'), + ('PostgresSchemaSupport', 'CreateSchemaSupport'), + ('DropSchemaSupport', 'PostgresSchemaSupport'), + ('PostgresSchemaSupport', 'DropSchemaSupport'), ('TruncateSupport', 'PostgresTruncateSupport'), ('PostgresTruncateSupport', 'TruncateSupport'), ('TransactionControlSupport', 'PostgresTransactionSupport'), @@ -345,12 +425,37 @@ def test_no_interface_overlap_between_protocols(self): ('PostgresCommentSupport', 'CommentSupport'), } + # NamespaceSupport is the base of every object protocol, so all of them + # inherit its five namespace switches. That is the shape of the tree -- + # naming questions are asked once and answered once -- so an overlap + # that is entirely NamespaceSupport's members is not two protocols + # competing for the same method. Anything else still is. + namespace_members = get_all_protocol_methods(dialect_protocols.NamespaceSupport) + + # A protocol that derives another inherits its members, so the two share + # those names by construction rather than by accident. NamespaceSupport + # is the base of every object protocol and Create/Drop/AlterTableSupport + # and friends are the bases of the PostgreSQL protocols that extend them, + # which is why each is listed explicitly below; PostgresTypeSupport and + # PostgresDomainSupport carry the same relationship for Type/Domain. + # Anything that is not an inheritance relationship still competes for the + # same method name, and that is what this test is for. + inheritance_pairs = set() + for name, proto in class_map.items(): + for base in getattr(proto, "__mro__", ())[1:]: + base_name = base.__name__ + if base_name in class_map and base_name != name: + inheritance_pairs.add((name, base_name)) + inheritance_pairs.add((base_name, name)) + violations = [] for (name_a, members_a), (name_b, members_b) in combinations(member_map.items(), 2): if (name_a, name_b) in excluded_overlaps: continue + if (name_a, name_b) in inheritance_pairs: + continue overlap = members_a & members_b - if overlap: + if overlap and not overlap <= namespace_members: violations.append(f"{name_a} ∩ {name_b} = {overlap}") assert not violations, ( @@ -367,14 +472,20 @@ class TestPostgresProtocolDerivation: """ PROTOCOL_DERIVATIONS = [ - ("PostgresTableSupport", "TableSupport"), + ("PostgresTableSupport", "TableObjectSupport"), ("PostgresPartitionSupport", "PartitionSupport"), - ("PostgresIndexSupport", "IndexSupport"), + ("PostgresIndexSupport", "IndexObjectSupport"), ("PostgresLockingSupport", "LockingSupport"), - ("PostgresTriggerSupport", "TriggerSupport"), + ("PostgresTriggerSupport", "TriggerObjectSupport"), ("PostgresConstraintSupport", "ConstraintSupport"), - ("PostgresTypeSupport", "UserDefinedTypeSupport"), - ("PostgresDomainSupport", "DomainSupport"), + ("PostgresNamespaceSupport", "NamespaceSupport"), + ("PostgresTypeSupport", "CreateTypeSupport"), + ("PostgresTypeSupport", "AlterTypeSupport"), + ("PostgresTypeSupport", "DropTypeSupport"), + ("PostgresTypeSupport", "TypeObjectSupport"), + ("PostgresDomainSupport", "CreateDomainSupport"), + ("PostgresDomainSupport", "AlterDomainSupport"), + ("PostgresDomainSupport", "DropDomainSupport"), ] @pytest.mark.parametrize("pg_name,generic_name", PROTOCOL_DERIVATIONS) @@ -520,6 +631,11 @@ def test_expression_delegates_to_dialect(self, expr_name, format_method): (postgres_protocols.PostgresJoinSupport, postgres_mixins.PostgresJoinMixin), (postgres_protocols.PostgresTruncateSupport, postgres_mixins.PostgresTruncateMixin), (postgres_protocols.PostgresSchemaSupport, postgres_mixins.PostgresSchemaMixin), + # The naming side. Paired with the one mixin that answers it, so a switch + # added to the protocol without an implementation -- or an override added to + # the mixin without a protocol declaration -- fails here rather than at + # render time. + (postgres_protocols.PostgresNamespaceSupport, postgres_mixins.PostgresNamespaceMixin), (postgres_protocols.PostgresSequenceSupport, postgres_mixins.PostgresSequenceMixin), (postgres_protocols.PostgresTransactionSupport, postgres_mixins.PostgresTransactionMixin), (postgres_protocols.PostgresViewSupport, postgres_mixins.PostgresViewMixin), @@ -585,10 +701,11 @@ class TestProtocolMethodSignatureConformance: ('OrderedSetAggregationSupport', 'format_ordered_set_aggregation'), # QualifyClauseSupport: Mixin uses expr instead of clause ('QualifyClauseSupport', 'format_qualify_clause'), - # ViewSupport: Materialized view methods use expr instead of named params - ('ViewSupport', 'format_create_materialized_view_statement'), - ('ViewSupport', 'format_drop_materialized_view_statement'), - ('ViewSupport', 'format_refresh_materialized_view_statement'), + # MaterializedViewSupport: the PG mixin methods take expr, and the PG + # override carries the CONCURRENTLY gate the generic one lacks. + ('MaterializedViewSupport', 'format_create_materialized_view_statement'), + ('MaterializedViewSupport', 'format_drop_materialized_view_statement'), + ('MaterializedViewSupport', 'format_refresh_materialized_view_statement'), # ExplainSupport: Mixin uses different signature ('ExplainSupport', 'format_explain_statement'), # ILIKESupport: Mixin uses expr-based signature instead of named params From 877cebe43984469c083c1daaac3c6ce93e6b354e Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 6 Oct 2026 20:08:27 +0800 Subject: [PATCH 2/8] fix(postgres): declare the sequence probes core now gates on Core commit 9dc18ab made all three sequence DDL formatters consult supports_sequence() first and unconditionally, and added four option probes (supports_sequence_start, _increment, _minvalue, _maxvalue). PostgreSQL's sequence mixin never defined supports_sequence -- core had never asked for it -- so the master switch fell back to core's default False and CREATE SEQUENCE, DROP SEQUENCE and ALTER SEQUENCE all raised UnsupportedFeatureError. A full-support dialect had stopped rendering the statement it supports. The mixin now answers every probe those formatters consult. supports_sequence, supports_create_sequence, supports_drop_sequence, supports_alter_sequence, supports_sequence_if_not_exists, supports_sequence_if_exists, and the option probes start, increment, minvalue, maxvalue, cycle, cache and owned_by all return True. supports_sequence_order returns False: the PostgreSQL CREATE SEQUENCE synopsis has no ORDER / NO ORDER clause -- that spelling is Oracle and SQL Server -- and the live server rejects it with a syntax error, so the formatter must refuse it rather than emit a clause the engine cannot parse. PostgresSequenceSupport restates the same switches, so the protocol and the mixin agree method for method. It derives from core's CreateSequenceSupport, DropSequenceSupport and AlterSequenceSupport, which is how the non-overlap guard in the protocol conformance test recognises the restatement as the deliberate overlap it is. The dialect's three redundant direct protocol bases are dropped for the same reason: a base may not precede the subclass that restates it. The three DDL formatters remain core's; this backend only answers the capability questions they ask. --- .../backend/impl/postgres/dialect.py | 10 ++- .../backend/impl/postgres/mixins/sequence.py | 61 +++++++++++++++++- .../impl/postgres/protocols/sequence.py | 62 +++++++++++++++++-- 3 files changed, 120 insertions(+), 13 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index b6cec6af..06aa02d1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -74,7 +74,6 @@ from rhosocial.activerecord.backend.dialect.protocols import ( AlterDatabaseSupport, AlterDomainSupport, - AlterSequenceSupport, AlterTableSupport, AlterTypeSupport, CreateDatabaseSupport, @@ -82,7 +81,6 @@ CreateIndexSupport, CreateRoutineSupport, CreateSchemaSupport, - CreateSequenceSupport, CreateTableSupport, CreateTriggerSupport, CreateTypeSupport, @@ -92,7 +90,6 @@ DropIndexSupport, DropRoutineSupport, DropSchemaSupport, - DropSequenceSupport, DropTableSupport, DropTriggerSupport, DropTypeSupport, @@ -580,9 +577,10 @@ class PostgresDialect( CreateIndexSupport, DropIndexSupport, FulltextIndexSupport, - CreateSequenceSupport, - DropSequenceSupport, - AlterSequenceSupport, + # CREATE/DROP/ALTER SEQUENCE are declared once, on PostgresSequenceSupport, + # which derives from the three core per-statement protocols. Listing them + # here as well would put a base before the subclass that restates it and + # break C3; the dialect still satisfies each through that derivation. CreateTriggerSupport, DropTriggerSupport, CreateTypeSupport, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py index a71928ef..426984d9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py @@ -1,15 +1,72 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py -"""PostgreSQL sequence feature support implementation.""" +"""PostgreSQL sequence feature support implementation. + +PostgreSQL is a full-support sequence dialect. Its ``CREATE SEQUENCE`` synopsis +takes ``IF NOT EXISTS``, ``INCREMENT BY``, ``MINVALUE``, ``MAXVALUE``, +``CYCLE``, ``START WITH``, ``CACHE`` and ``OWNED BY``; the one option core's +``CreateSequenceExpression`` carries that PostgreSQL has no clause for is +``ORDER``, which is Oracle/SQL Server syntax and is absent from the synopsis. +The probes below restate that, so the core formatters -- which consult +:meth:`supports_sequence` first and each option probe before its clause -- can +render everything the server accepts and refuse only ``ORDER``. + +These probes mirror +:class:`~...protocols.sequence.PostgresSequenceSupport`. The protocol and this +mixin are held in step by ``TestProtocolMixinForwardCoverage`` and +``TestProtocolMixinReverseCoverage`` in ``test_postgres_protocol_conformance``, +which fail if a switch is declared in one and not the other. +""" class PostgresSequenceMixin: """PostgreSQL sequence override implementation. - All features are native, using version number for detection. + Every form below has been accepted since PostgreSQL 9.6, this backend's + baseline, so the answers are version-independent and the ``version`` the + dialect carries does not enter into them. """ + def supports_sequence(self) -> bool: + return True + def supports_create_sequence(self) -> bool: return True def supports_drop_sequence(self) -> bool: return True + + def supports_alter_sequence(self) -> bool: + return True + + def supports_sequence_if_not_exists(self) -> bool: + return True + + def supports_sequence_if_exists(self) -> bool: + return True + + def supports_sequence_start(self) -> bool: + return True + + def supports_sequence_increment(self) -> bool: + return True + + def supports_sequence_minvalue(self) -> bool: + return True + + def supports_sequence_maxvalue(self) -> bool: + return True + + def supports_sequence_cycle(self) -> bool: + return True + + def supports_sequence_cache(self) -> bool: + return True + + def supports_sequence_order(self) -> bool: + # ORDER / NO ORDER is Oracle/SQL Server syntax. The PostgreSQL + # CREATE SEQUENCE synopsis has no such clause, so this is the one + # sequence option the dialect must refuse. + return False + + def supports_sequence_owned_by(self) -> bool: + return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py index ce507088..69f8874a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py @@ -1,13 +1,65 @@ # src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py -"""PostgreSQL sequence feature support protocol.""" +"""PostgreSQL sequence feature support protocol. + +Declared here, implemented by +:class:`~...mixins.sequence.PostgresSequenceMixin`. PostgreSQL is a +full-support sequence dialect, so this restates every capability switch the +core sequence protocols declare and answers them True -- except +``supports_sequence_order``, because the ``CREATE SEQUENCE`` synopsis has no +``ORDER`` / ``NO ORDER`` clause. The one-for-one mirror is enforced by the +forward/reverse coverage tests in ``test_postgres_protocol_conformance``. +""" from typing import Protocol, runtime_checkable +from rhosocial.activerecord.backend.dialect.protocols import ( + AlterSequenceSupport, + CreateSequenceSupport, + DropSequenceSupport, +) + @runtime_checkable -class PostgresSequenceSupport(Protocol): - """PostgreSQL sequence feature support protocol.""" +class PostgresSequenceSupport( + CreateSequenceSupport, DropSequenceSupport, AlterSequenceSupport, Protocol +): + """PostgreSQL sequence feature support protocol. + + Derives from the three core per-statement sequence protocols -- core split + the former umbrella into CREATE / DROP / ALTER -- so the interface it + restates is inherited rather than merely duplicated. The non-overlap guard + in ``test_postgres_protocol_conformance`` reads that inheritance as the + deliberate overlap it is; a standalone protocol declaring the same switches + would look like an accidental collision instead. + + The bodies below restate each capability question so this protocol mirrors + :class:`PostgresSequenceMixin` method for method. + """ + + def supports_sequence(self) -> bool: ... + + def supports_create_sequence(self) -> bool: ... + + def supports_drop_sequence(self) -> bool: ... + + def supports_alter_sequence(self) -> bool: ... + + def supports_sequence_if_not_exists(self) -> bool: ... + + def supports_sequence_if_exists(self) -> bool: ... + + def supports_sequence_start(self) -> bool: ... + + def supports_sequence_increment(self) -> bool: ... + + def supports_sequence_minvalue(self) -> bool: ... + + def supports_sequence_maxvalue(self) -> bool: ... + + def supports_sequence_cycle(self) -> bool: ... + + def supports_sequence_cache(self) -> bool: ... - def supports_create_sequence(self)-> bool: ... + def supports_sequence_order(self) -> bool: ... - def supports_drop_sequence(self)-> bool: ... + def supports_sequence_owned_by(self) -> bool: ... From 1887630a835902fd75508459a176edf42160ab3b Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 6 Oct 2026 22:28:40 +0800 Subject: [PATCH 3/8] fix(postgres): declare the ALTER SEQUENCE START probe explicitly Core's format_alter_sequence_statement gated AlterSequenceExpression.start on supports_sequence_start(), the probe for the CREATE SEQUENCE ... START WITH clause. Those are two different clauses, and True for one does not imply True for the other. PostgreSQL accepts START on ALTER -- it records the start value that future RESTART calls use, while RESTART WITH sets the current value -- but Oracle, SQL Server, Firebird and Snowflake all reject it. Reusing the CREATE probe for the ALTER formatter would therefore let those four dialects emit SQL their server rejects. Add supports_alter_sequence_start() to PostgresSequenceMixin, answering True because the live server accepts the clause, and declare it on PostgresSequenceSupport so the protocol keeps mirroring the mixin. The default for the new probe is False, which is the safe side: a dialect that accepts the clause must say so explicitly, as this one does. --- .../backend/impl/postgres/mixins/sequence.py | 18 ++++++++++++++++++ .../impl/postgres/protocols/sequence.py | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py index 426984d9..60bd9a84 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py @@ -38,6 +38,24 @@ def supports_drop_sequence(self) -> bool: def supports_alter_sequence(self) -> bool: return True + def supports_alter_sequence_start(self) -> bool: + """Whether ``ALTER SEQUENCE ... START WITH`` is supported. + + This is a different clause from the ``CREATE SEQUENCE ... START WITH`` + one :meth:`supports_sequence_start` describes, so ``True`` there does + not imply ``True`` here and the two are not interchangeable. PostgreSQL + accepts both: ``ALTER SEQUENCE ... START WITH n`` records the start + value future ``RESTART`` calls use, while ``RESTART WITH n`` sets the + current value. + + The default is ``False``, which is the safe side: Oracle, SQL Server, + Firebird and Snowflake all reject ``START`` on ``ALTER SEQUENCE``, and + a probe answering ``True`` by default would let them emit SQL their + server rejects. A dialect that does accept the clause must therefore + say so explicitly, as this one does. + """ + return True + def supports_sequence_if_not_exists(self) -> bool: return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py index 69f8874a..9506200a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py @@ -44,6 +44,8 @@ def supports_drop_sequence(self) -> bool: ... def supports_alter_sequence(self) -> bool: ... + def supports_alter_sequence_start(self) -> bool: ... + def supports_sequence_if_not_exists(self) -> bool: ... def supports_sequence_if_exists(self) -> bool: ... From 0ea4d39e22de99ed07479fdd9b09a6dd0244e29d Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 7 Oct 2026 03:37:21 +0800 Subject: [PATCH 4/8] feat(postgres): declare the identity probes and gate identity DDL on them Core made the identity and auto-increment nodes fail-closed: IdentityClause consults supports_identity_column() plus one probe per option, and AutoIncrementClause consults supports_auto_increment_column(). PostgreSQL declared none of the new probes -- the dialect did not mix in the identity mixin at all -- so core's formatters refused every identity clause. Declare the measured boundary: supports_identity_column() is True from PostgreSQL 10 on, because PostgreSQL 9 rejects the clause with `syntax error at or near "GENERATED"` (measured) while 10 through 19 all accept it. The six option probes -- generation ALWAYS, start, increment, minvalue, maxvalue, cycle -- answer True: they are part of the same synopsis. supports_auto_increment_column() answers False: PostgreSQL has no AUTO_INCREMENT spelling, and the probe is declared explicitly rather than inherited from the fail-closed default. The legacy is_auto_increment constraint now routes through the gated identity formatter, so PostgreSQL 9 refuses an identity request instead of rendering a GENERATED clause its server rejects. There is no fallback to SERIAL: the SERIAL type family stays available for users who ask for it explicitly, and it keeps executing on every version. PostgresIdentitySupport and PostgresAutoIncrementSupport mirror the two new mixins; the protocol conformance test pairs them with the existing forward/reverse coverage. The new execution-confirmation suite renders and executes each clause against a live server: accepted where the probe says True, NOT_RENDERED on PostgreSQL 9 where it says False, with a rejected sentinel and an accepted control before each verdict. The PG9 identity example now uses the supported IdentityAttribute path instead of a keyword ColumnDefinition never accepted. --- .../backend/impl/postgres/dialect.py | 8 + .../schema_diff/postgres10_identity.py | 10 +- .../backend/impl/postgres/mixins/__init__.py | 4 + .../impl/postgres/mixins/auto_increment.py | 28 ++ .../backend/impl/postgres/mixins/ddl_table.py | 13 +- .../impl/postgres/mixins/generated_column.py | 7 - .../impl/postgres/mixins/identity_column.py | 67 +++++ .../impl/postgres/protocols/__init__.py | 7 + .../impl/postgres/protocols/auto_increment.py | 30 +++ .../postgres/protocols/identity_column.py | 43 +++ .../backend/test_auto_increment_ddl.py | 44 ++- .../backend/test_identity_execution.py | 255 ++++++++++++++++++ .../test_postgres_protocol_conformance.py | 7 +- 13 files changed, 509 insertions(+), 14 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/auto_increment.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/protocols/auto_increment.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 06aa02d1..d98c04dc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -245,6 +245,8 @@ PostgresDQLMixin, PostgresJSONMixin, PostgresGeneratedColumnMixin, + PostgresIdentityColumnMixin, + PostgresAutoIncrementMixin, PostgresExpressionMixin, PostgresFunctionMixin, ) @@ -289,6 +291,8 @@ PostgresSchemaSupport, PostgresNamespaceSupport, PostgresSequenceSupport, + PostgresIdentitySupport, + PostgresAutoIncrementSupport, PostgresTransactionSupport, PostgresViewSupport, PostgresXMLSupport, @@ -379,6 +383,8 @@ class PostgresDialect( PostgresDQLMixin, PostgresJSONMixin, PostgresGeneratedColumnMixin, + PostgresIdentityColumnMixin, + PostgresAutoIncrementMixin, PostgresExpressionMixin, PostgresFunctionMixin, # Per-feature mixins @@ -633,6 +639,8 @@ class PostgresDialect( PostgresSchemaSupport, PostgresNamespaceSupport, PostgresSequenceSupport, + PostgresIdentitySupport, + PostgresAutoIncrementSupport, PostgresTransactionSupport, PostgresViewSupport, PostgresXMLSupport, 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 6414b36a..10fc36eb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py @@ -17,9 +17,9 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os +from rhosocial.activerecord.base import IdentityAttribute from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig -from rhosocial.activerecord.backend.expression.statements import IdentityClause from rhosocial.activerecord.backend.expression.objects import Table from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, @@ -63,14 +63,16 @@ backend.execute(sql, params) # PostgreSQL 10+: GENERATED AS IDENTITY -# Note: IdentityClause(generation='ALWAYS') triggers GENERATED ALWAYS AS IDENTITY -# in the PostgreSQL dialect's format_column_definition +# Note: IdentityAttribute(generation='ALWAYS') triggers +# GENERATED ALWAYS AS IDENTITY through the dialect's gated identity formatter; +# on PostgreSQL 9 that formatter refuses instead of rendering a clause the +# server rejects, and the SERIAL path above stays the PG9-compatible one. try: pg10_table = CreateTableExpression( dialect=dialect, table=Table(dialect, 'pg10_style'), columns=[ ColumnDefinition(dialect, "id", IntegerType(dialect), constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)], - identity_clause=IdentityClause(dialect, generation="ALWAYS")), + attributes=[IdentityAttribute(generation="ALWAYS")]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), ] ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py index 54aa8647..c9dff656 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py @@ -19,6 +19,8 @@ from .dql import PostgresDQLMixin from .json import PostgresJSONMixin from .generated_column import PostgresGeneratedColumnMixin +from .identity_column import PostgresIdentityColumnMixin +from .auto_increment import PostgresAutoIncrementMixin from .expression import PostgresExpressionMixin from .function import PostgresFunctionMixin @@ -138,6 +140,8 @@ "PostgresDQLMixin", "PostgresJSONMixin", "PostgresGeneratedColumnMixin", + "PostgresIdentityColumnMixin", + "PostgresAutoIncrementMixin", "PostgresExpressionMixin", "PostgresFunctionMixin", # DDL mixins diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/auto_increment.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/auto_increment.py new file mode 100644 index 00000000..ff55830c --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/auto_increment.py @@ -0,0 +1,28 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/auto_increment.py +"""PostgreSQL auto-increment marker support implementation. + +``AUTO_INCREMENT`` is MySQL / MariaDB spelling. PostgreSQL has no such column +marker, so this backend answers the mechanism probe ``False`` and the core +formatter refuses the clause instead of rendering a token the server rejects. + +The probe is declared explicitly, the way ``supports_sequence_order`` is: +a ``False`` answer is a decision about the backend's grammar, and it should be +readable here rather than inferred from an inherited default. +""" + +from rhosocial.activerecord.backend.dialect.mixins import AutoIncrementMixin + + +class PostgresAutoIncrementMixin(AutoIncrementMixin): + """PostgreSQL auto-increment marker override implementation.""" + + def supports_auto_increment_column(self) -> bool: + """Whether a bare ``AUTO_INCREMENT`` column marker can be used. + + Always ``False``: ``AUTO_INCREMENT`` is MySQL / MariaDB syntax and is + absent from the PostgreSQL grammar. The server-generated-value + mechanisms PostgreSQL does have are the SQL-standard identity clause + (:class:`~...mixins.identity_column.PostgresIdentityColumnMixin`) and + the ``SERIAL`` type family. + """ + return False diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py index c92260b6..9c806485 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py @@ -3,6 +3,7 @@ from typing import Any, List, Tuple from rhosocial.activerecord.backend.expression.objects import Table +from rhosocial.activerecord.backend.expression.statements.ddl_table import IdentityClause class PostgresTableMixin: @@ -207,7 +208,17 @@ def format_column_definition(self, col_def) -> Tuple[str, tuple]: 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" + # An auto-increment constraint on PostgreSQL is the standard + # identity clause in its default generation mode. Routing it + # through the gated formatter keeps the version boundary in + # one place: PostgreSQL 9 refuses here instead of rendering + # a GENERATED clause its server rejects, and no silent + # fallback to SERIAL is attempted. + identity_sql, identity_params = self.format_identity_clause( + IdentityClause(self, "BY DEFAULT") + ) + col_sql += identity_sql + all_params.extend(identity_params) auto_identity_added = True suffix, params = self.format_column_constraint(constraint) col_sql += suffix diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py index cd32328b..93fecc38 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/generated_column.py @@ -5,13 +5,6 @@ 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. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py new file mode 100644 index 00000000..c6c73820 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py @@ -0,0 +1,67 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py +"""PostgreSQL identity column feature support implementation. + +PostgreSQL has the SQL-standard ``GENERATED {ALWAYS|BY DEFAULT} AS IDENTITY`` +column clause since version 10. The boundary is measured, not assumed: on +PostgreSQL 9 the live server rejects the clause with ``syntax error at or near +"GENERATED"``, while 10 through 19 all accept it -- bare, ``ALWAYS``, and with +sequence options. The generation mode and every sequence option belong to the +same synopsis, so the six parameter probes are version-independent and answer +``True``; only the mechanism probe carries the version boundary. + +PostgreSQL has no fallback to ``SERIAL``: ``SERIAL`` is a *type*, carried by +:class:`~...expression.types.PostgresSerialType` and its siblings, not an +identity clause. A user who wants SERIAL asks for that type explicitly; a +PostgreSQL 9 server that is asked for an identity column is refused rather +than handed a type it did not ask for. + +These probes mirror +:class:`~...protocols.identity_column.PostgresIdentitySupport`. The protocol +and this mixin are held in step by ``TestProtocolMixinForwardCoverage`` and +``TestProtocolMixinReverseCoverage`` in ``test_postgres_protocol_conformance``, +which fail if a switch is declared in one and not the other. +""" + +from rhosocial.activerecord.backend.dialect.mixins import IdentityColumnMixin + + +class PostgresIdentityColumnMixin(IdentityColumnMixin): + """PostgreSQL identity column override implementation. + + The formatter itself stays core's: PostgreSQL uses the SQL-standard + spelling, so ``format_identity_clause`` is inherited rather than + overridden. Only the capability questions are answered here. + """ + + def supports_identity_column(self) -> bool: + """Whether ``GENERATED ... AS IDENTITY`` can be used. + + ``True`` from PostgreSQL 10 on. PostgreSQL 9 has no identity columns: + the live server rejects the clause with a syntax error, so the + formatter must refuse rather than render it. + """ + return self.version >= (10, 0, 0) + + def supports_identity_generation_always(self) -> bool: + """``GENERATED ALWAYS AS IDENTITY`` is in the PostgreSQL synopsis.""" + return True + + def supports_identity_start(self) -> bool: + """``START WITH`` is a sequence option of the identity clause.""" + return True + + def supports_identity_increment(self) -> bool: + """``INCREMENT BY`` is a sequence option of the identity clause.""" + return True + + def supports_identity_minvalue(self) -> bool: + """``MINVALUE`` is a sequence option of the identity clause.""" + return True + + def supports_identity_maxvalue(self) -> bool: + """``MAXVALUE`` is a sequence option of the identity clause.""" + return True + + def supports_identity_cycle(self) -> bool: + """``CYCLE`` / ``NO CYCLE`` is a sequence option of the identity clause.""" + return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py index 79419491..72a26309 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/__init__.py @@ -85,6 +85,10 @@ from .logical_replication import PostgresLogicalReplicationSupport from .parallel_query import PostgresParallelQuerySupport +# Identity / auto-increment mechanism protocols +from .identity_column import PostgresIdentitySupport +from .auto_increment import PostgresAutoIncrementSupport + # Per-feature protocol imports from .cte import PostgresCTESupport from .window import PostgresWindowSupport @@ -182,6 +186,9 @@ "PostgresQueryOptimizationSupport", "PostgresLogicalReplicationSupport", "PostgresParallelQuerySupport", + # Identity / auto-increment mechanism protocols + "PostgresIdentitySupport", + "PostgresAutoIncrementSupport", # Per-feature protocols "PostgresCTESupport", "PostgresWindowSupport", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/auto_increment.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/auto_increment.py new file mode 100644 index 00000000..bc83523d --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/auto_increment.py @@ -0,0 +1,30 @@ +# src/rhosocial/activerecord/backend/impl/postgres/protocols/auto_increment.py +"""PostgreSQL auto-increment marker support protocol. + +Declared here, implemented by +:class:`~...mixins.auto_increment.PostgresAutoIncrementMixin`. PostgreSQL +answers the mechanism probe ``False`` -- ``AUTO_INCREMENT`` is MySQL / MariaDB +spelling -- so this protocol restates that one switch. The one-for-one mirror +is enforced by the forward/reverse coverage tests in +``test_postgres_protocol_conformance``. +""" + +from typing import Protocol, runtime_checkable + +from rhosocial.activerecord.backend.dialect.protocols import AutoIncrementColumnSupport + + +@runtime_checkable +class PostgresAutoIncrementSupport(AutoIncrementColumnSupport, Protocol): + """PostgreSQL auto-increment marker support protocol. + + Derives from core's :class:`AutoIncrementColumnSupport` so the interface it + restates is inherited rather than merely duplicated. The non-overlap guard + in ``test_postgres_protocol_conformance`` reads that inheritance as the + deliberate overlap it is. + + The body below restates the capability question so this protocol mirrors + :class:`PostgresAutoIncrementMixin` method for method. + """ + + def supports_auto_increment_column(self) -> bool: ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py new file mode 100644 index 00000000..754f0616 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py @@ -0,0 +1,43 @@ +# src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py +"""PostgreSQL identity column feature support protocol. + +Declared here, implemented by +:class:`~...mixins.identity_column.PostgresIdentityColumnMixin`. PostgreSQL +uses the SQL-standard ``GENERATED {ALWAYS|BY DEFAULT} AS IDENTITY`` spelling, +so the formatter is core's; this protocol restates the mechanism probe and the +six option probes the mixin answers. The one-for-one mirror is enforced by the +forward/reverse coverage tests in ``test_postgres_protocol_conformance``. +""" + +from typing import Protocol, runtime_checkable + +from rhosocial.activerecord.backend.dialect.protocols import IdentityColumnSupport + + +@runtime_checkable +class PostgresIdentitySupport(IdentityColumnSupport, Protocol): + """PostgreSQL identity column feature support protocol. + + Derives from core's :class:`IdentityColumnSupport` so the interface it + restates is inherited rather than merely duplicated. The non-overlap guard + in ``test_postgres_protocol_conformance`` reads that inheritance as the + deliberate overlap it is; a standalone protocol declaring the same switches + would look like an accidental collision instead. + + The bodies below restate each capability question so this protocol mirrors + :class:`PostgresIdentityColumnMixin` method for method. + """ + + def supports_identity_column(self) -> bool: ... + + def supports_identity_generation_always(self) -> bool: ... + + def supports_identity_start(self) -> bool: ... + + def supports_identity_increment(self) -> bool: ... + + def supports_identity_minvalue(self) -> bool: ... + + def supports_identity_maxvalue(self) -> bool: ... + + def supports_identity_cycle(self) -> bool: ... 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 c0676280..c0db7376 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 @@ -8,10 +8,17 @@ - format_create_table_statement must return a string for all types. - BooleanType with DEFAULT True must be accepted. - TimestampType must map to TIMESTAMP. + +The auto-increment constraint compiles to the SQL-standard identity clause, so +it is version-gated: PostgreSQL 9 must refuse it rather than render a +``GENERATED`` clause its server rejects, and the refusal must name the feature +instead of falling back to ``SERIAL`` (the SERIAL type family stays available +for users who ask for it explicitly). """ import pytest +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.expression import ( CreateTableExpression, ColumnDefinition, ColumnConstraint, ColumnConstraintType, ) @@ -24,7 +31,9 @@ @pytest.fixture def dialect(): - return PostgresDialect() + # An adapted dialect: the identity probe carries the measured version + # boundary (PostgreSQL 10), so an un-adapted dialect cannot answer it. + return PostgresDialect(version=(16, 0, 0)) def _pk(dialect): @@ -82,3 +91,36 @@ def test_timestamp_type(self, dialect): ) sql, _ = expr.to_sql() assert "TIMESTAMP" in sql.upper() + + +class TestAutoIncrementVersionGate: + """The constraint compiles to an identity clause, so it carries the gate.""" + + @pytest.mark.parametrize( + "version", [(10, 0, 0), (14, 0, 0), (18, 0, 0)], ids=lambda v: str(v) + ) + def test_identity_clause_rendered_from_pg10(self, version): + sql, _ = _build_table(PostgresDialect(version=version)).to_sql() + assert "GENERATED BY DEFAULT AS IDENTITY" in sql.upper() + + def test_refused_on_pg9(self): + """PostgreSQL 9 has no identity columns: refuse, do not fall back.""" + with pytest.raises(UnsupportedFeatureError, match="IDENTITY column"): + _build_table(PostgresDialect(version=(9, 6, 0))).to_sql() + + def test_refusal_is_not_a_serial_rewrite(self): + """The refused request must not be silently rewritten to a SERIAL type.""" + dialect = PostgresDialect(version=(9, 6, 0)) + with pytest.raises(UnsupportedFeatureError): + _build_table(dialect).to_sql() + # A user who wants SERIAL asks for the type explicitly, and that path + # is version-independent. + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + ) + serial = CreateTableExpression( + dialect=dialect, table=Table(dialect, 'serial_tbl'), + columns=[ColumnDefinition(dialect, "id", PostgresSerialType(dialect=dialect))], + ) + sql, _ = serial.to_sql() + assert "SERIAL" in sql.upper() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py new file mode 100644 index 00000000..6063a021 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py @@ -0,0 +1,255 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py +"""Execution confirmation for PostgreSQL identity / auto-increment / SERIAL DDL. + +Rendering is not execution. PostgreSQL 9 renders a well-formed +``GENERATED ... AS IDENTITY`` clause and its server rejects it with +``syntax error at or near "GENERATED"``, which is exactly the failure this +file exists to catch. Every clause is built with the expression layer, rendered +by the dialect the live backend carries, and executed against that server. + +The three outcomes stay distinct, as core's helper requires: + +* the server accepts -> ``ACCEPTED``; +* the server rejects SQL the expression rendered -> the helper raises, and + that is a defect; +* the expression refuses to render (``UnsupportedFeatureError``) -> + ``NOT_RENDERED``, which on PostgreSQL 9 is the *correct* answer for an + identity column, not a skip. + +Each verdict is preceded by a sentinel: a deliberately invalid statement must +be classified ``REJECTED``, or a null result could be read as acceptance. +""" + +import pytest + +from rhosocial.activerecord.base import IdentityAttribute +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.execution_testing import ( + ExecutionOutcome, + classify_execution, + confirm_expression_execution, +) +from rhosocial.activerecord.backend.expression.objects import Table +from rhosocial.activerecord.backend.expression.statements import ( + AutoIncrementClause, + ColumnConstraint, + ColumnConstraintType, + ColumnDefinition, + CreateTableExpression, + DropTableExpression, + IdentityClause, +) +from rhosocial.activerecord.backend.expression.types import IntegerType +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresBigSerialType, + PostgresSerialType, + PostgresSmallSerialType, +) + + +# --------------------------------------------------------------------------- +# Offline: the declaration itself +# --------------------------------------------------------------------------- + + +class TestIdentityProbeDeclaration: + """The probes answer the measured boundary and the renderer agrees.""" + + @pytest.mark.parametrize( + "version,expected", + [((9, 6, 0), False), ((10, 0, 0), True), ((14, 0, 0), True), ((18, 0, 0), True)], + ids=["pg9", "pg10", "pg14", "pg18"], + ) + def test_mechanism_probe_carries_the_version_boundary(self, version, expected): + dialect = PostgresDialect(version=version) + assert dialect.supports_identity_column() is expected + + @pytest.mark.parametrize( + "version", [(10, 0, 0), (14, 0, 0), (18, 0, 0)], ids=["pg10", "pg14", "pg18"] + ) + def test_every_option_probe_is_declared(self, version): + dialect = PostgresDialect(version=version) + for probe in ( + "supports_identity_generation_always", + "supports_identity_start", + "supports_identity_increment", + "supports_identity_minvalue", + "supports_identity_maxvalue", + "supports_identity_cycle", + ): + assert getattr(dialect, probe)() is True, probe + + def test_pg9_refuses_the_clause(self): + """PostgreSQL 9 has no identity columns, so the formatter refuses. + + The refusal is the point: there is no fallback to SERIAL (the explicit + SERIAL type path is covered separately and executes on every version). + """ + dialect = PostgresDialect(version=(9, 6, 0)) + with pytest.raises(UnsupportedFeatureError, match="IDENTITY column"): + dialect.format_identity_clause(IdentityClause(dialect)) + + def test_auto_increment_marker_is_declared_false_and_refused(self): + dialect = PostgresDialect(version=(18, 0, 0)) + assert dialect.supports_auto_increment_column() is False + with pytest.raises(UnsupportedFeatureError, match="AUTO_INCREMENT column"): + AutoIncrementClause(dialect).to_sql() + + +# --------------------------------------------------------------------------- +# Live: the rendered clause is accepted (or correctly refused) by the server +# --------------------------------------------------------------------------- + + +def _identity_table(dialect, table_name, **identity_kwargs): + return CreateTableExpression( + dialect=dialect, + table=Table(dialect, table_name), + columns=[ + ColumnDefinition( + dialect, + "id", + IntegerType(dialect=dialect), + attributes=[IdentityAttribute(**identity_kwargs)], + ) + ], + ) + + +def _auto_increment_table(dialect, table_name): + return CreateTableExpression( + dialect=dialect, + table=Table(dialect, table_name), + columns=[ + ColumnDefinition( + dialect, + "id", + IntegerType(dialect=dialect), + constraints=[ + ColumnConstraint( + dialect, + ColumnConstraintType.PRIMARY_KEY, + is_auto_increment=True, + ) + ], + ) + ], + ) + + +def _serial_table(dialect, table_name, data_type): + return CreateTableExpression( + dialect=dialect, + table=Table(dialect, table_name), + columns=[ColumnDefinition(dialect, "id", data_type(dialect=dialect))], + ) + + +def _confirm(backend, expression, table_name): + dialect = backend.dialect + return confirm_expression_execution( + backend, + expression, + teardown=[ + DropTableExpression(dialect, Table(dialect, table_name), if_exists=True) + ], + ) + + +class TestIdentityExecutionConfirmation: + """Acceptance is confirmed against the live server, per version.""" + + def test_sentinel_rejected_and_control_accepted(self, postgres_backend): + """The classifier must see both answers before any verdict is trusted.""" + assert ( + classify_execution(postgres_backend, "THIS IS NOT SQL") + is ExecutionOutcome.REJECTED + ) + assert ( + classify_execution(postgres_backend, "SELECT 1") + is ExecutionOutcome.ACCEPTED + ) + + def test_bare_identity_matches_the_declaration(self, postgres_backend): + dialect = postgres_backend.dialect + outcome = _confirm( + postgres_backend, + _identity_table(dialect, "test_identity_exec_bare"), + "test_identity_exec_bare", + ) + if dialect.supports_identity_column(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + def test_always_with_options_matches_the_declaration(self, postgres_backend): + dialect = postgres_backend.dialect + outcome = _confirm( + postgres_backend, + _identity_table( + dialect, + "test_identity_exec_options", + generation="ALWAYS", + start=100, + increment=5, + minvalue=1, + maxvalue=1000000, + cycle=True, + ), + "test_identity_exec_options", + ) + if dialect.supports_identity_column(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + def test_no_cycle_is_executable(self, postgres_backend): + dialect = postgres_backend.dialect + outcome = _confirm( + postgres_backend, + _identity_table( + dialect, "test_identity_exec_nocycle", cycle=False + ), + "test_identity_exec_nocycle", + ) + if dialect.supports_identity_column(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + def test_auto_increment_constraint_matches_the_declaration(self, postgres_backend): + """The legacy is_auto_increment flag routes through the identity gate.""" + dialect = postgres_backend.dialect + outcome = _confirm( + postgres_backend, + _auto_increment_table(dialect, "test_identity_exec_legacy"), + "test_identity_exec_legacy", + ) + if dialect.supports_identity_column(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + +class TestSerialTypeExecution: + """SERIAL is a type, not an identity clause: it executes on every version.""" + + @pytest.mark.parametrize( + "data_type,table_suffix", + [ + (PostgresSmallSerialType, "smallserial"), + (PostgresSerialType, "serial"), + (PostgresBigSerialType, "bigserial"), + ], + ids=["smallserial", "serial", "bigserial"], + ) + def test_serial_type_executes(self, postgres_backend, data_type, table_suffix): + dialect = postgres_backend.dialect + table_name = f"test_identity_exec_{table_suffix}" + outcome = _confirm( + postgres_backend, + _serial_table(dialect, table_name, data_type), + table_name, + ) + assert outcome is ExecutionOutcome.ACCEPTED 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 b38f1b62..488e86d3 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 @@ -156,7 +156,8 @@ def get_postgres_protocols(): dialect_protocols.DDLTypeSupport, dialect_protocols.GraphTableSupport, dialect_protocols.ILIKESupport, - dialect_protocols.AutoIncrementSupport, + dialect_protocols.AutoIncrementColumnSupport, + dialect_protocols.IdentityColumnSupport, dialect_protocols.GeneratedColumnSupport, # Statement forms PostgreSQL implements through its mixins, so the # partition test below can classify them rather than leaving them @@ -486,6 +487,8 @@ class TestPostgresProtocolDerivation: ("PostgresDomainSupport", "CreateDomainSupport"), ("PostgresDomainSupport", "AlterDomainSupport"), ("PostgresDomainSupport", "DropDomainSupport"), + ("PostgresIdentitySupport", "IdentityColumnSupport"), + ("PostgresAutoIncrementSupport", "AutoIncrementColumnSupport"), ] @pytest.mark.parametrize("pg_name,generic_name", PROTOCOL_DERIVATIONS) @@ -637,6 +640,8 @@ def test_expression_delegates_to_dialect(self, expr_name, format_method): # render time. (postgres_protocols.PostgresNamespaceSupport, postgres_mixins.PostgresNamespaceMixin), (postgres_protocols.PostgresSequenceSupport, postgres_mixins.PostgresSequenceMixin), + (postgres_protocols.PostgresIdentitySupport, postgres_mixins.PostgresIdentityColumnMixin), + (postgres_protocols.PostgresAutoIncrementSupport, postgres_mixins.PostgresAutoIncrementMixin), (postgres_protocols.PostgresTransactionSupport, postgres_mixins.PostgresTransactionMixin), (postgres_protocols.PostgresViewSupport, postgres_mixins.PostgresViewMixin), (postgres_protocols.PostgresXMLSupport, postgres_mixins.PostgresXMLMixin), From b575a820d947435009bfabc09b6d7d7d2ada67a0 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 7 Oct 2026 19:10:57 +0800 Subject: [PATCH 5/8] feat(postgres): consume the clause-parameter pairs and close the dialect gaps Core split every two-spelling clause into one parameter per spelling (438b7aa): unspecified means neither parameter, both means ValueError. The PostgreSQL formatters still read the pre-split fields, so this follows. Formatters consuming a pair: - format_truncate_statement: restart_identity|continue_identity (CONTINUE IDENTITY was silently dropped) and cascade|restrict (RESTRICT was silently dropped). - format_create_materialized_view_statement and format_refresh_materialized_view_pg_statement: with_data|no_data; unspecified renders no tail (the old else-branch spelled WITH NO DATA). - format_drop_materialized_view_statement: cascade|restrict (RESTRICT was silently dropped). - format_begin_transaction / format_set_transaction: deferrable|not_deferrable; [NOT] DEFERRABLE renders whenever requested, not only alongside SERIALIZABLE, and unspecified no longer spells NOT DEFERRABLE. - _format_exclude_constraint: deferrable|not_deferrable and initially_deferred|initially_immediate; INITIALLY ... renders as the independent constraint attribute it is. - format_copy_to_statement: force_array|force_array_false. - format_table_constraint: the enforced gate no longer reads a tri-state, so table constraints render again instead of refusing every state. Dialect-specific nodes split to the pair shape: - PostgresRefreshMaterializedViewExpression: with_data|no_data. - PostgresCopyToExpression: force_array|force_array_false. - PostgresCreateMaterializedViewExpression: with_data|no_data (its constructor dropped no_data and defaulted to WITH DATA). - PostgresDropMaterializedViewExpression: restrict pass-through. - PostgresExcludeConstraint: deferrable|not_deferrable and initially_deferred|initially_immediate. The audit registry did not carry this node; it was found by the sweep and measured before/after. New probes declared True (measured against live PostgreSQL 16 and 19): supports_schema_restrict, supports_restrict_view, supports_drop_function_restrict, supports_materialized_view_restrict, supports_truncate_restrict, and supports_identity_cache. The CACHE option's negative spelling, NO CACHE, has no PostgreSQL form (syntax error, measured), so no_cache is refused by name for sequences and identity while cache=n renders; ORDER/NO ORDER stay refused. The transaction manager now drives the split setters, so set_deferrable(False) renders NOT DEFERRABLE instead of raising. Behaviour changes (intended, per the round): - CREATE MATERIALIZED VIEW default no longer emits WITH NO DATA. - SET TRANSACTION with nothing requested no longer emits NOT DEFERRABLE. - BEGIN ... DEFERRABLE renders for any isolation level. - RESTRICT and CONTINUE IDENTITY become reachable where they were dropped. Guard: test_clause_pair_guard.py asserts the four states of every pair the dialect consumes are pairwise distinguishable; run red against the unmodified tree first (24 failing test ids), green after. Two mutations (one removed exclusion check, one reverted restrict branch) were each caught by the expected guard test. --- .../backend/impl/postgres/expression/copy.py | 8 +- .../expression/ddl/exclude_constraint.py | 17 +- .../impl/postgres/expression/ddl/mv.py | 13 +- .../impl/postgres/mixins/ddl/constraint.py | 21 +- .../impl/postgres/mixins/ddl/routine.py | 9 + .../backend/impl/postgres/mixins/ddl_view.py | 9 + .../backend/impl/postgres/mixins/dml/copy.py | 8 +- .../impl/postgres/mixins/identity_column.py | 45 +- .../impl/postgres/mixins/materialized_view.py | 34 +- .../backend/impl/postgres/mixins/schema.py | 9 + .../backend/impl/postgres/mixins/sequence.py | 54 ++ .../impl/postgres/mixins/transaction.py | 21 +- .../backend/impl/postgres/mixins/truncate.py | 39 +- .../postgres/protocols/identity_column.py | 10 +- .../postgres/protocols/materialized_view.py | 8 + .../backend/impl/postgres/protocols/schema.py | 2 + .../impl/postgres/protocols/sequence.py | 16 +- .../impl/postgres/protocols/truncate.py | 2 + .../backend/impl/postgres/protocols/view.py | 1 + .../backend/impl/postgres/transaction.py | 15 +- .../dialect/test_drop_table_cascade.py | 14 +- .../backend/dialect/test_materialized_view.py | 57 +- .../test_expressions_transaction.py | 23 +- .../backend/postgres/test_ddl_alter_config.py | 12 +- .../backend/postgres/test_ddl_expressions.py | 14 +- .../test_materialized_view_integration.py | 6 +- .../backend/postgres/test_view_execution.py | 4 +- .../feature/backend/test_clause_pair_guard.py | 670 ++++++++++++++++++ .../backend/test_expression_roundtrip_all.py | 11 +- .../backend/test_identity_execution.py | 16 + .../test_transaction_advanced_features.py | 9 +- 31 files changed, 1086 insertions(+), 91 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/copy.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/copy.py index 3b610964..28a462c9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/copy.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/copy.py @@ -92,7 +92,8 @@ def __init__( quote: Optional[str] = None, escape: Optional[str] = None, force_quote: Optional[CopyColumnSelection] = None, - force_array: Optional[bool] = None, + force_array: bool = False, + force_array_false: bool = False, partitioned: bool = False, is_partitioned: Optional[bool] = None, ): @@ -101,6 +102,10 @@ def __init__( raise ValueError("COPY TO requires exactly one table_name or query source") if table_name is not None and query is not None: raise ValueError("COPY TO accepts either table_name or query, not both") + if force_array and force_array_false: + raise ValueError( + "force_array and force_array_false are mutually exclusive options" + ) if query is not None and not isinstance(query, BaseExpression): raise TypeError("COPY TO query must be a BaseExpression") if is_partitioned is not None: @@ -121,6 +126,7 @@ def __init__( self.escape = escape self.force_quote = _copy_force_columns(force_quote) self.force_array = force_array + self.force_array_false = force_array_false self.partitioned = partitioned self.is_partitioned = partitioned diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/exclude_constraint.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/exclude_constraint.py index b5ef079a..1e273c1e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/exclude_constraint.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/exclude_constraint.py @@ -27,6 +27,11 @@ class PostgresExcludeConstraint(TableConstraint): expression is either a column name (``str``) or a ``BaseExpression``; ``using`` selects the index access method (default ``gist``) and ``where`` is an optional partial-exclusion predicate. + + The deferrability and enforcement options follow the round's two-parameter + shape (``deferrable`` / ``not_deferrable``, ``initially_deferred`` / + ``initially_immediate``, ``enforced`` / ``not_enforced``); setting both of a + pair raises ``ValueError`` at construction through the generic base. """ def __init__( @@ -37,19 +42,25 @@ def __init__( *, using: str = "gist", where: Optional[SQLPredicate] = None, - deferrable: Optional[bool] = None, - initially_deferred: Optional[bool] = None, + deferrable: bool = False, + not_deferrable: bool = False, + initially_deferred: bool = False, + initially_immediate: bool = False, validation: Any = None, - enforced: Optional[bool] = None, + enforced: bool = False, + not_enforced: bool = False, ): super().__init__( dialect, TableConstraintType.EXCLUDE, name=name, deferrable=deferrable, + not_deferrable=not_deferrable, initially_deferred=initially_deferred, + initially_immediate=initially_immediate, validation=validation, enforced=enforced, + not_enforced=not_enforced, ) self.elements = elements or [] self.using = using 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 8e8b32e9..0ed960b5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py @@ -78,6 +78,7 @@ class PostgresCreateMaterializedViewExpression(CreateMaterializedViewExpression) ... view=MaterializedView(dialect, "monthly_sales_summary", schema_name="reporting"), ... query=sales_query, ... if_not_exists=True, + ... with_data=True, ... storage_options={PostgresStorageParameter.FILLFACTOR: 70}, ... ) >>> sql, params = create.to_sql() @@ -93,7 +94,8 @@ def __init__( query: Any, column_aliases: Optional[List[str]] = None, tablespace: Optional[str] = None, - with_data: bool = True, + with_data: bool = False, + no_data: bool = False, storage_options: Optional[Dict[Any, Any]] = None, if_not_exists: bool = False, allow_unlisted_storage_parameters: bool = False, @@ -105,6 +107,7 @@ def __init__( column_aliases=column_aliases, tablespace=tablespace, with_data=with_data, + no_data=no_data, storage_options=storage_options, ) if storage_options: @@ -136,12 +139,14 @@ def __init__( view: MaterializedView, if_exists: bool = False, cascade: bool = False, + restrict: bool = False, ): super().__init__( dialect, view=view, if_exists=if_exists, cascade=cascade, + restrict=restrict, ) @@ -186,7 +191,7 @@ class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpressio >>> refresh = PostgresRefreshMaterializedViewExpression( ... dialect=dialect, ... view=MaterializedView(dialect, "monthly_sales_summary"), - ... with_data=False, + ... no_data=True, ... ) """ @@ -196,13 +201,15 @@ def __init__( dialect: "SQLDialectBase", view: MaterializedView, concurrently: bool = False, - with_data: Optional[bool] = None, + with_data: bool = False, + no_data: bool = False, ): super().__init__( dialect, view=view, concurrent=concurrently, with_data=with_data, + no_data=no_data, ) @property 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 eb026be5..061c3fef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py @@ -121,7 +121,7 @@ def format_table_constraint(self, expr: "TableConstraint") -> Tuple[str, tuple]: from rhosocial.activerecord.backend.expression.statements import TableConstraintType constraint_type = normalize_table_constraint_type(expr.constraint_type) - if getattr(expr, "enforced", None) is not None: + if getattr(expr, "enforced", False) or getattr(expr, "not_enforced", False): self._require_constraint_enforced(constraint_type) if constraint_type == TableConstraintType.EXCLUDE: return self._format_exclude_constraint(expr) @@ -134,7 +134,9 @@ def format_add_table_constraint_action( constraint_type = normalize_table_constraint_type(action.constraint.constraint_type) if constraint_type == TableConstraintType.EXCLUDE: - if getattr(action.constraint, "enforced", None) is not None: + if getattr(action.constraint, "enforced", False) or getattr( + action.constraint, "not_enforced", False + ): self._require_constraint_enforced(constraint_type) validation_sql = self._format_constraint_validation(action.constraint) exclude_sql, params = self._format_exclude_constraint(action.constraint) @@ -258,14 +260,13 @@ def _format_exclude_constraint( params.extend(where_params) parts.append(f"WHERE ({where_sql})") - if getattr(constraint, "deferrable", None) is True: - if getattr(constraint, "initially_deferred", None) is True: - parts.append("DEFERRABLE INITIALLY DEFERRED") - elif getattr(constraint, "initially_deferred", None) is False: - parts.append("DEFERRABLE INITIALLY IMMEDIATE") - else: - parts.append("DEFERRABLE") - elif getattr(constraint, "deferrable", None) is False: + if getattr(constraint, "deferrable", False): + parts.append("DEFERRABLE") + elif getattr(constraint, "not_deferrable", False): parts.append("NOT DEFERRABLE") + if getattr(constraint, "initially_deferred", False): + parts.append("INITIALLY DEFERRED") + elif getattr(constraint, "initially_immediate", False): + parts.append("INITIALLY IMMEDIATE") return " ".join(parts), tuple(params) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py index e42a5b69..11f03bc5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/routine.py @@ -99,6 +99,15 @@ def supports_drop_function_cascade(self) -> bool: """Whether DROP FUNCTION CASCADE is supported.""" return self.supports_function_ddl() + def supports_drop_function_restrict(self) -> bool: + """Whether DROP FUNCTION RESTRICT is supported. + + ``RESTRICT`` is the default behavior and is accepted by every version + this backend claims; the statement itself is gated on + :meth:`supports_function_ddl`, so the answer follows that gate. + """ + return self.supports_function_ddl() + # ------------------------------------------------------------------ # # Core's portable CREATE/DROP FUNCTION # ------------------------------------------------------------------ # diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py index a47c3407..c4364360 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py @@ -34,6 +34,15 @@ def supports_view_check_option(self) -> bool: def supports_cascade_view(self) -> bool: return True + def supports_restrict_view(self) -> bool: + """``DROP VIEW ... RESTRICT`` is in the synopsis. + + PostgreSQL accepts ``DROP VIEW name [ CASCADE | RESTRICT ]`` (measured + on PostgreSQL 16); RESTRICT is the default behavior but the token + itself is legal. + """ + return True + def format_create_view_statement(self, expr: "CreateViewExpression") -> Tuple[str, tuple]: """Format CREATE VIEW statement for PostgreSQL. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/copy.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/copy.py index d07f4aea..93e1412a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/copy.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/copy.py @@ -421,9 +421,11 @@ def format_copy_to_statement( ) ) ) - if expr.force_array is not None: - if not isinstance(expr.force_array, bool): - raise TypeError("force_array must be a bool") + if expr.force_array or expr.force_array_false: + if not isinstance(expr.force_array, bool) or not isinstance( + expr.force_array_false, bool + ): + raise TypeError("force_array and force_array_false must be bools") if copy_format != "json": raise ValueError("FORCE_ARRAY requires COPY TO JSON format") self._require_copy_version( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py index c6c73820..0a5d3176 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py @@ -22,15 +22,27 @@ which fail if a switch is declared in one and not the other. """ +from typing import Tuple, TYPE_CHECKING + from rhosocial.activerecord.backend.dialect.mixins import IdentityColumnMixin +if TYPE_CHECKING: + from rhosocial.activerecord.backend.expression.statements.ddl_table import ( + IdentityClause, + ) + class PostgresIdentityColumnMixin(IdentityColumnMixin): """PostgreSQL identity column override implementation. The formatter itself stays core's: PostgreSQL uses the SQL-standard - spelling, so ``format_identity_clause`` is inherited rather than - overridden. Only the capability questions are answered here. + spelling, so ``format_identity_clause`` delegates rather than restating the + gating. It is overridden only for the one spelling the identity synopsis + lacks: ``NO CACHE``. The option's positive form is supported -- + ``GENERATED BY DEFAULT AS IDENTITY (CACHE 1)`` is accepted (measured on + PostgreSQL 16) -- while ``NO CACHE`` is a syntax error there + (``syntax error at or near "CACHE"``), so ``supports_identity_cache()`` + answers True and ``no_cache=True`` is refused by name instead of rendered. """ def supports_identity_column(self) -> bool: @@ -65,3 +77,32 @@ def supports_identity_maxvalue(self) -> bool: def supports_identity_cycle(self) -> bool: """``CYCLE`` / ``NO CYCLE`` is a sequence option of the identity clause.""" return True + + def supports_identity_cache(self) -> bool: + """``CACHE n`` is a sequence option of the identity clause. + + ``GENERATED ... AS IDENTITY (CACHE 1)`` is accepted (measured on + PostgreSQL 16). The option's negative spelling, ``NO CACHE``, has no + identity form; :meth:`format_identity_clause` refuses it by name. + """ + return True + + def format_identity_clause(self, expr: "IdentityClause") -> Tuple[str, tuple]: + """Render the identity clause, refusing the unspellable negative. + + Delegates to the core formatter so the option gating and spelling + hooks stay in one place. ``no_cache`` is the one spelling PostgreSQL's + synopsis lacks; it is refused by name rather than rendered. + """ + if expr.no_cache: + from rhosocial.activerecord.backend.dialect.exceptions import ( + UnsupportedFeatureError, + ) + + raise UnsupportedFeatureError( + self.name, + "IDENTITY NO CACHE", + f"{self.name} has no NO CACHE identity spelling; cache=1 is " + f"the uncached form. The CACHE option itself is supported.", + ) + return super().format_identity_clause(expr) 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 4100c4cb..f9198043 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py @@ -83,6 +83,15 @@ def supports_alter_materialized_view(self) -> bool: """ALTER MATERIALIZED VIEW is supported since PostgreSQL 9.3.""" return self.version >= (9, 3, 0) + def supports_materialized_view_restrict(self) -> bool: + """``DROP MATERIALIZED VIEW ... RESTRICT`` is in the synopsis. + + PostgreSQL accepts ``DROP MATERIALIZED VIEW name [ CASCADE | RESTRICT ]`` + (measured on PostgreSQL 16); RESTRICT is the default behavior but the + token itself is legal. + """ + return True + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ @@ -142,7 +151,9 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie - ``expr.storage_options`` — optional dict of storage parameters (``WITH (… )``). - ``expr.tablespace`` — optional tablespace. - ``expr.query`` — source SELECT expression. - - ``expr.with_data`` — ``WITH DATA`` / ``WITH NO DATA``. + - ``expr.with_data`` / ``expr.no_data`` — ``WITH DATA`` / ``WITH NO + DATA``; neither set renders no tail (both set is refused at + construction). Args: expr: CreateMaterializedViewExpression instance @@ -193,7 +204,7 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie if expr.with_data: parts.append("WITH DATA") - else: + elif expr.no_data: parts.append("WITH NO DATA") return " ".join(parts), query_params @@ -201,13 +212,16 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie def format_drop_materialized_view_statement(self, expr: Any) -> Tuple[str, tuple]: """Format DROP MATERIALIZED VIEW statement for PostgreSQL. - Extends the core formatter with schema qualification. + Extends the core formatter with schema qualification and consumes the + ``cascade`` / ``restrict`` pair. An explicit RESTRICT whose probe is + False raises instead of being dropped. Raises: TypeError: ``expr.view`` is not a :class:`MaterializedView`. A plain :class:`View` is the wrong kind here: the two share a name shape but not a statement, and PostgreSQL refuses ``DROP MATERIALIZED VIEW `` at execution time with a far worse message. + UnsupportedFeatureError: If RESTRICT is requested but not supported. """ if not isinstance(expr.view, MaterializedView): raise TypeError( @@ -223,6 +237,14 @@ def format_drop_materialized_view_statement(self, expr: Any) -> Tuple[str, tuple parts.append(self._format_materialized_view_name(expr)) if expr.cascade: parts.append("CASCADE") + elif expr.restrict: + if not self.supports_materialized_view_restrict(): + raise UnsupportedFeatureError( + self.name, + "DROP MATERIALIZED VIEW RESTRICT", + f"{self.name} does not support DROP MATERIALIZED VIEW RESTRICT.", + ) + parts.append("RESTRICT") return " ".join(parts), () def format_refresh_materialized_view_statement(self, expr: Any) -> Tuple[str, tuple]: @@ -261,8 +283,10 @@ def format_refresh_materialized_view_pg_statement( if expr.concurrent: parts.append("CONCURRENTLY") parts.append(self._format_materialized_view_name(expr)) - if expr.with_data is not None: - parts.append("WITH DATA" if expr.with_data else "WITH NO DATA") + if expr.with_data: + parts.append("WITH DATA") + elif expr.no_data: + parts.append("WITH NO DATA") return " ".join(parts), () def format_alter_materialized_view_statement( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py index 3923b646..1aba0ec0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/schema.py @@ -43,5 +43,14 @@ def supports_schema_if_exists(self) -> bool: def supports_schema_cascade(self) -> bool: return True + def supports_schema_restrict(self) -> bool: + """``DROP SCHEMA ... RESTRICT`` is in the synopsis. + + PostgreSQL accepts ``DROP SCHEMA name [ CASCADE | RESTRICT ]`` + (measured on PostgreSQL 16); RESTRICT is the default behavior but the + token itself is legal. + """ + return True + def supports_schema_authorization(self) -> bool: return True diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py index 60bd9a84..b54fe91f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py @@ -17,6 +17,14 @@ which fail if a switch is declared in one and not the other. """ +from typing import Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.expression.statements.ddl_sequence import ( + AlterSequenceExpression, + CreateSequenceExpression, + ) + class PostgresSequenceMixin: """PostgreSQL sequence override implementation. @@ -24,6 +32,15 @@ class PostgresSequenceMixin: Every form below has been accepted since PostgreSQL 9.6, this backend's baseline, so the answers are version-independent and the ``version`` the dialect carries does not enter into them. + + One spelling the CACHE option pair carries has no PostgreSQL form: + ``NO CACHE``. The ``CREATE SEQUENCE`` / ``ALTER SEQUENCE`` synopsis reads + ``[ CACHE cache ]`` with a positive minimum, so the uncached form is + ``CACHE 1``; ``NO CACHE`` is a syntax error (measured on PostgreSQL 16, + ``syntax error at or near "CACHE"``). The formatters below therefore refuse + ``no_cache=True`` by name rather than render a token the server rejects -- + the same fail-closed rule the core formatters apply to options whose probe + is False, applied here to the one spelling of an option whose probe is True. """ def supports_sequence(self) -> bool: @@ -88,3 +105,40 @@ def supports_sequence_order(self) -> bool: def supports_sequence_owned_by(self) -> bool: return True + + # ------------------------------------------------------------------ + # Formatter overrides: the one spelling PostgreSQL cannot express + # ------------------------------------------------------------------ + + def _refuse_no_cache(self, feature: str) -> None: + """Refuse ``NO CACHE`` by name; PostgreSQL spells it ``CACHE 1``.""" + from rhosocial.activerecord.backend.dialect.exceptions import ( + UnsupportedFeatureError, + ) + + raise UnsupportedFeatureError( + self.name, + feature, + f"{self.name} has no NO CACHE spelling; cache=1 is the uncached " + f"form. The CACHE option itself is supported.", + ) + + def format_create_sequence_statement(self, expr: "CreateSequenceExpression") -> Tuple[str, tuple]: + """Render ``CREATE SEQUENCE``, refusing the unspellable negative. + + Delegates to the core formatter for everything else so the option + gating and ordering stay in one place. ``no_cache`` is the one spelling + PostgreSQL's synopsis lacks; it is refused by name rather than rendered. + """ + if expr.no_cache: + self._refuse_no_cache("SEQUENCE NO CACHE") + return super().format_create_sequence_statement(expr) + + def format_alter_sequence_statement(self, expr: "AlterSequenceExpression") -> Tuple[str, tuple]: + """Render ``ALTER SEQUENCE``, refusing the unspellable negative. + + See :meth:`format_create_sequence_statement`. + """ + if expr.no_cache: + self._refuse_no_cache("ALTER SEQUENCE NO CACHE") + return super().format_alter_sequence_statement(expr) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py index 4b24681e..667bc444 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py @@ -41,7 +41,10 @@ def format_begin_transaction( [ { READ WRITE | READ ONLY } ] [ { NOT DEFERRABLE | DEFERRABLE } ] - DEFERRABLE is only meaningful for SERIALIZABLE isolation level. + ``[NOT] DEFERRABLE`` is an independent transaction mode in the + grammar; it is rendered whenever requested, not only alongside + SERIALIZABLE (``BEGIN DEFERRABLE`` without an isolation level is + accepted by the server -- measured on PostgreSQL 16). """ params = expr.get_params() parts = ["BEGIN"] @@ -59,11 +62,10 @@ def format_begin_transaction( 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") + if params.get("deferrable"): + parts.append("DEFERRABLE") + elif params.get("not_deferrable"): + parts.append("NOT DEFERRABLE") return " ".join(parts), () @@ -103,8 +105,11 @@ def format_set_transaction( options.append("READ WRITE") deferrable = params.get("deferrable") - if deferrable is not None: - options.append("DEFERRABLE" if deferrable else "NOT DEFERRABLE") + not_deferrable = params.get("not_deferrable") + if deferrable: + options.append("DEFERRABLE") + elif not_deferrable: + options.append("NOT DEFERRABLE") if options: parts.append(" ".join(options)) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py index 15808d30..1405b170 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py @@ -22,17 +22,31 @@ def supports_truncate_restart_identity(self) -> bool: def supports_truncate_cascade(self) -> bool: return True + def supports_truncate_restrict(self) -> bool: + """``TRUNCATE ... RESTRICT`` is in the synopsis. + + PostgreSQL accepts ``TRUNCATE [ TABLE ] name [ RESTART IDENTITY | + CONTINUE IDENTITY ] [ CASCADE | RESTRICT ]`` (measured on PostgreSQL + 16); RESTRICT is the default behavior but the token itself is legal. + """ + return True + def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tuple]: """Format TRUNCATE statement for PostgreSQL. - ``expr.table`` — the table being truncated, which renders its own name and namespace. - - ``expr.restart_identity`` — add ``RESTART IDENTITY`` (PG 8.4+). - - ``expr.cascade`` — add ``CASCADE``. + - ``expr.restart_identity`` / ``expr.continue_identity`` — add + ``RESTART IDENTITY`` / ``CONTINUE IDENTITY`` (PG 8.4+); neither set + renders neither token. + - ``expr.cascade`` / ``expr.restrict`` — add ``CASCADE`` / + ``RESTRICT``; neither set renders neither token. Raises: TypeError: ``expr.table`` is not a :class:`Table`. Another object kind would have had its own name rendered as the table's. + UnsupportedFeatureError: If the requested identity continuation or + dependent-object behavior is not supported by this version. """ if not isinstance(expr.table, Table): raise TypeError( @@ -41,17 +55,30 @@ def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tu ) parts = ["TRUNCATE TABLE", expr.table.to_sql()[0]] - if expr.restart_identity: + if expr.restart_identity or expr.continue_identity: if not self.supports_truncate_restart_identity(): + feature = ( + "RESTART IDENTITY" + if expr.restart_identity + else "CONTINUE IDENTITY" + ) raise UnsupportedFeatureError( self.name, - "RESTART IDENTITY", - f"{self.name} does not support TRUNCATE ... RESTART IDENTITY " + feature, + f"{self.name} does not support TRUNCATE ... {feature} " f"for versions < 8.4." ) - parts.append("RESTART IDENTITY") + parts.append("RESTART IDENTITY" if expr.restart_identity else "CONTINUE IDENTITY") if expr.cascade: parts.append("CASCADE") + elif expr.restrict: + if not self.supports_truncate_restrict(): + raise UnsupportedFeatureError( + self.name, + "TRUNCATE RESTRICT", + f"{self.name} does not support TRUNCATE ... RESTRICT.", + ) + parts.append("RESTRICT") return " ".join(parts), () diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py index 754f0616..301a1a39 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py @@ -4,9 +4,11 @@ Declared here, implemented by :class:`~...mixins.identity_column.PostgresIdentityColumnMixin`. PostgreSQL uses the SQL-standard ``GENERATED {ALWAYS|BY DEFAULT} AS IDENTITY`` spelling, -so the formatter is core's; this protocol restates the mechanism probe and the -six option probes the mixin answers. The one-for-one mirror is enforced by the -forward/reverse coverage tests in ``test_postgres_protocol_conformance``. +so the formatter stays core's except for the one spelling the identity synopsis +lacks (``NO CACHE``, refused by name); this protocol restates the mechanism +probe and the seven option probes the mixin answers. The one-for-one mirror is +enforced by the forward/reverse coverage tests in +``test_postgres_protocol_conformance``. """ from typing import Protocol, runtime_checkable @@ -41,3 +43,5 @@ def supports_identity_minvalue(self) -> bool: ... def supports_identity_maxvalue(self) -> bool: ... def supports_identity_cycle(self) -> bool: ... + + def supports_identity_cache(self) -> bool: ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py index 2a23b390..2e1e55f7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py @@ -68,6 +68,14 @@ def supports_alter_materialized_view(self) -> bool: """Whether ALTER MATERIALIZED VIEW is supported (PG 9.3+).""" ... + def supports_materialized_view_restrict(self) -> bool: + """Whether ``DROP MATERIALIZED VIEW ... RESTRICT`` can be expressed. + + PostgreSQL accepts the token in the drop synopsis; it is the default + behavior but is legal to spell. + """ + ... + def format_create_materialized_view_statement(self, expr: Any) -> Tuple[str, tuple]: """Format CREATE MATERIALIZED VIEW statement for PostgreSQL. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py index 7f31ed66..6aeb44e9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/schema.py @@ -30,4 +30,6 @@ def supports_schema_if_exists(self)-> bool: ... def supports_schema_cascade(self)-> bool: ... + def supports_schema_restrict(self)-> bool: ... + def supports_schema_authorization(self)-> bool: ... \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py index 9506200a..8e3df803 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py @@ -10,7 +10,7 @@ forward/reverse coverage tests in ``test_postgres_protocol_conformance``. """ -from typing import Protocol, runtime_checkable +from typing import Protocol, runtime_checkable, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.protocols import ( AlterSequenceSupport, @@ -18,6 +18,12 @@ DropSequenceSupport, ) +if TYPE_CHECKING: + from ...expression.statements.ddl_sequence import ( + AlterSequenceExpression, + CreateSequenceExpression, + ) + @runtime_checkable class PostgresSequenceSupport( @@ -65,3 +71,11 @@ def supports_sequence_cache(self) -> bool: ... def supports_sequence_order(self) -> bool: ... def supports_sequence_owned_by(self) -> bool: ... + + def format_create_sequence_statement( + self, expr: "CreateSequenceExpression" + ) -> Tuple[str, tuple]: ... + + def format_alter_sequence_statement( + self, expr: "AlterSequenceExpression" + ) -> Tuple[str, tuple]: ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py index 889511ec..df576fd1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py @@ -12,4 +12,6 @@ def supports_truncate_restart_identity(self)-> bool: ... def supports_truncate_cascade(self)-> bool: ... + def supports_truncate_restrict(self)-> bool: ... + def format_truncate_statement(self, expr: Any) -> Tuple[str, tuple]: ... diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/view.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/view.py index f1c4a7ca..ca70d6c0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/view.py @@ -15,6 +15,7 @@ def supports_temporary_view(self) -> bool: ... def supports_if_exists_view(self) -> bool: ... def supports_view_check_option(self) -> bool: ... def supports_cascade_view(self) -> bool: ... + def supports_restrict_view(self) -> bool: ... def format_create_view_statement(self, expr: Any) -> Tuple[str, tuple]: """Format CREATE VIEW statement for PostgreSQL. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/transaction.py b/src/rhosocial/activerecord/backend/impl/postgres/transaction.py index 23a5b41b..a881c920 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/transaction.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/transaction.py @@ -64,16 +64,23 @@ def _build_begin_sql(self) -> Tuple[str, tuple]: if self._transaction_mode == TransactionMode.READ_ONLY: expr.read_only() - # DEFERRABLE is handled by the Expression/Dialect system - if self._isolation_level == IsolationLevel.SERIALIZABLE and self._is_deferrable is not None: - expr.deferrable(self._is_deferrable) + # [NOT] DEFERRABLE is an independent transaction mode in the grammar; + # it is rendered whenever the caller requested it, not only alongside + # SERIALIZABLE (``BEGIN DEFERRABLE`` alone is accepted by PostgreSQL). + if self._is_deferrable is not None: + if self._is_deferrable: + expr.deferrable() + else: + expr.not_deferrable() return expr.to_sql() def set_deferrable(self, deferrable: bool = True) -> None: """Set transaction deferrable mode. - In PostgreSQL, DEFERRABLE only affects SERIALIZABLE transactions. + ``[NOT] DEFERRABLE`` is an independent transaction mode: ``True`` + requests ``DEFERRABLE``, ``False`` requests ``NOT DEFERRABLE``, and + leaving it unset renders neither. Args: deferrable: Whether constraints should be deferrable diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py index cc0db174..239926c4 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_drop_table_cascade.py @@ -32,18 +32,26 @@ def test_cascade_renders_standard_token(self, dialect): assert params == () def test_restrict_renders_standard_token(self, dialect): - expr = DropTableExpression(dialect, table=Table(dialect, 'users'), cascade=False) + expr = DropTableExpression(dialect, table=Table(dialect, 'users'), restrict=True) sql, params = expr.to_sql() assert sql.endswith(" RESTRICT") assert params == () - def test_cascade_none_omits_token(self, dialect): - expr = DropTableExpression(dialect, table=Table(dialect, 'users'), cascade=None) + def test_neither_cascade_nor_restrict_omits_token(self, dialect): + expr = DropTableExpression(dialect, table=Table(dialect, 'users')) sql, params = expr.to_sql() assert "CASCADE" not in sql assert "RESTRICT" not in sql assert params == () + def test_both_cascade_and_restrict_refused(self, dialect): + with pytest.raises( + ValueError, match="cascade and restrict are mutually exclusive options" + ): + DropTableExpression( + dialect, table=Table(dialect, 'users'), cascade=True, restrict=True + ) + def test_if_exists_combined_with_cascade(self, dialect): expr = DropTableExpression( dialect, table=Table(dialect, 'users'), if_exists=True, cascade=True diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py index 2de9ffb7..cbfc7179 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py @@ -132,7 +132,7 @@ def test_storage_options_rendered(self): sql, _ = _create(dialect, storage_options={"fillfactor": 70}).to_sql() assert sql == ( 'CREATE MATERIALIZED VIEW "sales_summary" WITH (fillfactor = 70) ' - 'AS SELECT "product_id" FROM "sales" WITH DATA' + 'AS SELECT "product_id" FROM "sales"' ) def test_multiple_storage_options_rendered(self): @@ -145,7 +145,7 @@ def test_multiple_storage_options_rendered(self): assert sql == ( 'CREATE MATERIALIZED VIEW "sales_summary" ' 'WITH (fillfactor = 70, autovacuum_enabled = true) ' - 'AS SELECT "product_id" FROM "sales" WITH DATA' + 'AS SELECT "product_id" FROM "sales"' ) def test_tablespace_column_aliases_with_data(self): @@ -156,6 +156,7 @@ def test_tablespace_column_aliases_with_data(self): column_aliases=["product_id", "total_sales"], storage_options={"fillfactor": 90}, tablespace="fast_ssd", + with_data=True, ).to_sql() assert sql == ( 'CREATE MATERIALIZED VIEW "sales_summary" ("product_id", "total_sales") ' @@ -163,12 +164,30 @@ def test_tablespace_column_aliases_with_data(self): 'AS SELECT "product_id" FROM "sales" WITH DATA' ) + def test_with_data(self): + """T-08a: WITH DATA tail is reachable through its own parameter.""" + dialect = _dialect() + sql, _ = _create(dialect, with_data=True).to_sql() + assert sql.endswith("WITH DATA") + def test_with_no_data(self): - """T-08: WITH NO DATA tail.""" + """T-08b: WITH NO DATA tail is reachable through its own parameter.""" dialect = _dialect() - sql, _ = _create(dialect, with_data=False).to_sql() + sql, _ = _create(dialect, no_data=True).to_sql() assert sql.endswith("WITH NO DATA") + def test_neither_with_data_nor_no_data_omits_the_tail(self): + """T-08c: unspecified renders no population tail (the PG default).""" + dialect = _dialect() + sql, _ = _create(dialect).to_sql() + assert "WITH DATA" not in sql + + def test_both_with_data_and_no_data_refused(self): + """T-08d: setting both of the pair is API misuse.""" + dialect = _dialect() + with pytest.raises(ValueError, match="with_data and no_data are mutually exclusive"): + _create(dialect, with_data=True, no_data=True) + def test_empty_storage_options_omitted(self): """T-09: no empty ``WITH ()`` is emitted when storage_options is empty.""" dialect = _dialect() @@ -176,7 +195,7 @@ def test_empty_storage_options_omitted(self): assert "WITH (" not in sql assert sql == ( 'CREATE MATERIALIZED VIEW "sales_summary" ' - 'AS SELECT "product_id" FROM "sales" WITH DATA' + 'AS SELECT "product_id" FROM "sales"' ) @@ -198,6 +217,22 @@ def test_drop_if_exists_cascade(self): ).to_sql() assert sql == 'DROP MATERIALIZED VIEW IF EXISTS "sales_summary" CASCADE' + def test_drop_restrict_renders_standard_token(self): + """The RESTRICT spelling of the pair reaches the PostgreSQL formatter.""" + dialect = _dialect() + sql, _ = DropMaterializedViewExpression( + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), restrict=True + ).to_sql() + assert sql == 'DROP MATERIALIZED VIEW "sales_summary" RESTRICT' + + def test_drop_neither_cascade_nor_restrict_omits_token(self): + dialect = _dialect() + sql, _ = DropMaterializedViewExpression( + dialect=dialect, view=MaterializedView(dialect, 'sales_summary') + ).to_sql() + assert "CASCADE" not in sql + assert "RESTRICT" not in sql + class TestRefreshMaterializedViewFormatting: """G2: CONCURRENTLY must be gated on PostgreSQL 9.4+ on every code path.""" @@ -210,11 +245,15 @@ def test_refresh_plain(self): assert sql == 'REFRESH MATERIALIZED VIEW "sales_summary"' assert params == () - @pytest.mark.parametrize("with_data,expected", [(True, " WITH DATA"), (False, " WITH NO DATA")]) - def test_refresh_with_data_variants(self, with_data, expected): + @pytest.mark.parametrize( + "kwargs,expected", + [({"with_data": True}, " WITH DATA"), ({"no_data": True}, " WITH NO DATA")], + ids=["with_data", "no_data"], + ) + def test_refresh_data_variants(self, kwargs, expected): dialect = _dialect() sql, _ = RefreshMaterializedViewExpression( - dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), with_data=with_data + dialect=dialect, view=MaterializedView(dialect, 'sales_summary'), **kwargs ).to_sql() assert sql == f'REFRESH MATERIALIZED VIEW "sales_summary"{expected}' @@ -378,7 +417,7 @@ def test_if_not_exists_rendered(self): ).to_sql() assert sql == ( 'CREATE MATERIALIZED VIEW IF NOT EXISTS "sales_summary" ' - 'AS SELECT "product_id" FROM "sales" WITH DATA' + 'AS SELECT "product_id" FROM "sales"' ) def test_if_not_exists_false_omitted(self): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expressions_transaction.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expressions_transaction.py index 47c45e19..761a53db 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expressions_transaction.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/expression/test_expressions_transaction.py @@ -67,18 +67,24 @@ def test_begin_deferrable(self, postgres_dialect): def test_begin_not_deferrable(self, postgres_dialect): """Test BEGIN with NOT DEFERRABLE.""" expr = BeginTransactionExpression(postgres_dialect) - expr.isolation_level(IsolationLevel.SERIALIZABLE).deferrable(False) + expr.isolation_level(IsolationLevel.SERIALIZABLE).not_deferrable() sql, params = expr.to_sql() assert "NOT DEFERRABLE" in sql assert params == () - def test_begin_deferrable_ignored_for_non_serializable(self, postgres_dialect): - """Test DEFERRABLE is ignored for non-SERIALIZABLE isolation levels.""" + def test_begin_deferrable_renders_for_non_serializable(self, postgres_dialect): + """``[NOT] DEFERRABLE`` is an independent transaction mode. + + The clause is rendered whenever requested; it is not restricted to + SERIALIZABLE isolation (``BEGIN READ COMMITTED DEFERRABLE`` is accepted + by the server). + """ expr = BeginTransactionExpression(postgres_dialect) expr.isolation_level(IsolationLevel.READ_COMMITTED).deferrable() sql, params = expr.to_sql() - assert "DEFERRABLE" not in sql + assert "DEFERRABLE" in sql assert "READ COMMITTED" in sql + assert params == () @pytest.mark.parametrize("level,expected_name", [ (IsolationLevel.READ_UNCOMMITTED, "READ UNCOMMITTED"), @@ -152,6 +158,7 @@ def test_set_transaction_isolation(self, postgres_dialect): sql, params = expr.to_sql() assert "SET TRANSACTION" in sql assert "ISOLATION LEVEL SERIALIZABLE" in sql + assert "DEFERRABLE" not in sql assert params == () def test_set_transaction_read_only(self, postgres_dialect): @@ -182,6 +189,14 @@ def test_set_transaction_deferrable(self, postgres_dialect): assert "DEFERRABLE" in sql assert params == () + def test_set_transaction_not_deferrable(self, postgres_dialect): + """Test SET TRANSACTION NOT DEFERRABLE reaches its own spelling.""" + expr = SetTransactionExpression(postgres_dialect) + expr.not_deferrable() + sql, params = expr.to_sql() + assert sql == "SET TRANSACTION NOT DEFERRABLE" + assert params == () + class TestPostgreSQLTransactionCapabilities: """Tests for PostgreSQL transaction capabilities.""" 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 540a5247..002b07a3 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 @@ -399,7 +399,7 @@ def test_create_and_add_check_fk_enforcement(self): TableConstraintType.CHECK, name="age_check", check_condition=condition, - enforced=False, + not_enforced=True, ) fk = ForeignKeyConstraint( dialect, @@ -412,7 +412,7 @@ def test_create_and_add_check_fk_enforcement(self): dialect, ColumnConstraintType.CHECK, check_condition=condition, - enforced=False, + not_enforced=True, ) assert check.to_sql()[0] == 'CONSTRAINT "age_check" CHECK ("age" > 0) NOT ENFORCED' @@ -484,7 +484,7 @@ def test_alter_and_validate_actions(self): fk_action = PostgresAlterConstraint( pg18, "parent_fk", - False, + not_enforced=True, constraint_type=ColumnConstraintType.FOREIGN_KEY, ) assert fk_action.constraint_type is TableConstraintType.FOREIGN_KEY @@ -495,7 +495,7 @@ def test_alter_and_validate_actions(self): PostgresAlterConstraint( pg18, "age_check", - False, + not_enforced=True, constraint_type=TableConstraintType.CHECK, ).to_sql() assert PostgresValidateConstraint(pg18, "age_check").to_sql()[0] == ( @@ -506,11 +506,11 @@ def test_alter_and_validate_actions(self): assert PostgresAlterConstraint( pg19, "age_check", - False, + not_enforced=True, constraint_type=TableConstraintType.CHECK, ).to_sql()[0] == 'ALTER CONSTRAINT "age_check" NOT ENFORCED' with pytest.raises(TypeError): - PostgresAlterConstraint(pg19, "age_check", False) + PostgresAlterConstraint(pg19, "age_check", not_enforced=True) def test_exclude_create_and_add(self): from rhosocial.activerecord.backend.expression import ( 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 20fcab0f..b5625cb8 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 @@ -102,36 +102,36 @@ def test_refresh_concurrently_pg94(self, dialect): assert "CONCURRENTLY" in sql assert params == () - def test_refresh_with_data_false_pg93(self): - """Test WITH NO DATA requires PG 9.4+.""" + def test_refresh_no_data_pg93(self): + """Test WITH NO DATA on the PG 9.3 baseline.""" dialect_pg93 = PostgresDialect(version=(9, 3, 0)) expr = PostgresRefreshMaterializedViewExpression( dialect=dialect_pg93, view=MaterializedView(dialect_pg93, "monthly_sales_summary"), - with_data=False, + no_data=True, ) sql, params = expr.to_sql() assert "WITH NO DATA" in sql - def test_refresh_with_data_false_pg94(self, dialect): + def test_refresh_no_data_pg94(self, dialect): """Test WITH NO DATA with PG 9.4+.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, view=MaterializedView(dialect, "monthly_sales_summary"), - with_data=False, + no_data=True, ) sql, params = expr.to_sql() assert "WITH NO DATA" in sql assert params == () - def test_refresh_concurrently_and_with_data(self, dialect): + def test_refresh_concurrently_and_no_data(self, dialect): """Test CONCURRENTLY with WITH NO DATA.""" expr = PostgresRefreshMaterializedViewExpression( dialect=dialect, view=MaterializedView(dialect, "monthly_sales_summary"), concurrently=True, - with_data=False, + no_data=True, ) sql, params = expr.to_sql() assert "CONCURRENTLY" in sql diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py index 09da602f..e036de41 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py @@ -78,7 +78,7 @@ def test_with_no_data_is_unqueryable_until_refresh(self, mv_backend, daily_query dialect=mv_backend.dialect, view=MaterializedView(mv_backend.dialect, "mv_sales_daily"), query=daily_query(), - with_data=False, + no_data=True, ) sql, params = expression.to_sql() mv_backend.execute(sql, params, options=DDL) @@ -204,7 +204,7 @@ def test_list_reports_populated_flag(self, mv_backend, create_summary_mv, daily_ dialect=mv_backend.dialect, view=MaterializedView(mv_backend.dialect, "mv_sales_daily"), query=daily_query(), - with_data=False, + no_data=True, ) sql, params = empty.to_sql() mv_backend.execute(sql, params, options=DDL) @@ -339,7 +339,7 @@ async def test_teardown_leaves_no_residue(self, async_mv_backend, daily_query): dialect=dialect, view=MaterializedView(dialect, "mv_sales_daily"), query=daily_query(), - with_data=False, + no_data=True, ) sql, params = create.to_sql() await async_mv_backend.execute(sql, params, options=DDL) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py index 6e8c218e..6b021786 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_view_execution.py @@ -382,7 +382,7 @@ def test_create_materialized_view_with_no_data(self, postgres_view_backend): dialect, view=MaterializedView(dialect, 'test_mv'), query=query, - with_data=False + no_data=True ) sql, params = create_mv.to_sql() @@ -548,7 +548,7 @@ def test_refresh_materialized_view_with_data(self, postgres_view_backend): dialect, view=MaterializedView(dialect, 'test_mv'), query=query, - with_data=False + no_data=True ) sql, params = create_mv.to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py new file mode 100644 index 00000000..d4f558df --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py @@ -0,0 +1,670 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py +"""Guard: every clause pair the PostgreSQL dialect consumes is fully spellable. + +The rule this file enforces (the round's rule 1/2/3/4): + +* each spellable alternative has its own parameter; +* "unspecified" is the state where none of the group's parameters is set; +* setting more than one of them is API misuse and raises ``ValueError`` at + construction time; +* no ``Optional[bool]`` tri-state and no sentinel value. + +For every pair the PostgreSQL dialect consumes the four states must be +pairwise distinguishable: + +==================== ============================================= +neither parameter neither spelling rendered +parameter A A's spelling rendered (and not B's) +parameter B B's spelling rendered (and not A's) +both parameters ``ValueError`` +==================== ============================================= + +Two deliberate shapes are pinned separately: + +* A spelling PostgreSQL cannot express must refuse *by name* + (``UnsupportedFeatureError``) -- never render a token the server rejects and + never drop the clause. ``TestRefusedSpellingsFailClosed`` covers these, and + names the measured server evidence for each. +* ``AlterConstraint.enforced`` is mandatory in the action's grammar: "neither" + is refused as well as "both". + +The render evidence in this file is produced offline by the dialect. The +server-side evidence for the capability answers (which tokens PostgreSQL +accepts) was measured against live PostgreSQL 16 and 19 servers; the probes +are version-independent for every clause covered here. +""" + +import re + +import pytest + +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError +from rhosocial.activerecord.backend.expression.core import Column +from rhosocial.activerecord.backend.expression.objects import ( + Function, + MaterializedView, + Schema, + Sequence, + Table, + Type, + View, +) +from rhosocial.activerecord.backend.expression.pivot import UnpivotExpression +from rhosocial.activerecord.backend.expression.query_sources import ( + CTEExpression, + SetOperationExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( + AlterConstraint, +) +from rhosocial.activerecord.backend.expression.statements.ddl_function import ( + DropFunctionExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_schema import ( + DropSchemaExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_sequence import ( + AlterSequenceExpression, + CreateSequenceExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_table import ( + ColumnConstraint, + ColumnConstraintType, + CreateTableAsExpression, + DropTableExpression, + IdentityClause, + TableConstraint, + TableConstraintType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_truncate import ( + TruncateExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_view import ( + CreateMaterializedViewExpression, + DropMaterializedViewExpression, + DropViewExpression, + RefreshMaterializedViewExpression, +) +from rhosocial.activerecord.backend.expression.statements.dql import QueryExpression +from rhosocial.activerecord.backend.expression.transaction import ( + BeginTransactionExpression, + SetTransactionExpression, +) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.expression.copy import ( + PostgresCopyToExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.exclude_constraint import ( + PostgresExcludeConstraint, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.mv import ( + PostgresRefreshMaterializedViewExpression, +) +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.type import ( + PostgresDropTypeExpression, +) + +PG16 = (16, 0, 0) +PG19 = (19, 0, 0) + + +def _dialect(version=PG16): + return PostgresDialect(version=version) + + +def _table(d, name="t"): + return Table(d, name) + + +def _query(d): + return QueryExpression(d, select=[Column(d, "id")], from_=_table(d)) + + +def _fk(d, **kw): + return TableConstraint( + d, + TableConstraintType.FOREIGN_KEY, + name="fk", + columns=["a"], + foreign_key_table=_table(d, "t2"), + foreign_key_columns=["b"], + **kw, + ) + + +def _check(d, **kw): + from rhosocial.activerecord.backend.expression.predicates import ( + ComparisonPredicate, + ) + + return TableConstraint( + d, + TableConstraintType.CHECK, + name="ck", + check_condition=ComparisonPredicate(d, "=", Column(d, "a"), Column(d, "b")), + **kw, + ) + + +def _column_fk(d, **kw): + return ColumnConstraint( + d, + ColumnConstraintType.FOREIGN_KEY, + name="fk", + foreign_key_reference=(_table(d, "t2"), ["b"]), + **kw, + ) + + +def _column_check(d, **kw): + from rhosocial.activerecord.backend.expression.predicates import ( + ComparisonPredicate, + ) + + return ColumnConstraint( + d, + ColumnConstraintType.CHECK, + name="ck", + check_condition=ComparisonPredicate(d, "=", Column(d, "a"), Column(d, "b")), + **kw, + ) + + +def _exclude(d, **kw): + return PostgresExcludeConstraint( + d, name="ex", elements=[("range", "&&")], **kw + ) + + +class PairCase: + """One two-spelling clause and the four states of its parameter pair.""" + + def __init__( + self, + case_id, + builder, + a, + b, + a_pattern, + b_pattern, + a_value=True, + b_value=True, + version=PG16, + neither_error=None, + both_match=None, + a_refusal=None, + b_refusal=None, + ): + self.case_id = case_id + self.builder = builder + self.a = a + self.b = b + self.a_pattern = re.compile(a_pattern) + self.b_pattern = re.compile(b_pattern) + self.a_value = a_value + self.b_value = b_value + self.version = version + self.neither_error = neither_error + self.both_match = both_match or ( + f"{a} and {b} are mutually exclusive options" + ) + self.a_refusal = a_refusal + self.b_refusal = b_refusal + + def render(self, **kwargs): + dialect = _dialect(self.version) + sql, _params = self.builder(dialect, **kwargs).to_sql() + return sql + + def outcome(self, side): + """The spelling state of one side: render or refusal feature name.""" + if side == "a": + return self.a_refusal, self.a_value + return self.b_refusal, self.b_value + + def matches_a(self, sql): + return bool(self.a_pattern.search(sql)) + + def matches_b(self, sql): + return bool(self.b_pattern.search(sql)) + + +PAIR_CASES = ( + PairCase( + "CreateSequenceExpression.cycle", + lambda d, **kw: CreateSequenceExpression(d, Sequence(d, "s"), **kw), + "cycle", + "no_cycle", + r"(? Date: Thu, 8 Oct 2026 01:11:32 +0800 Subject: [PATCH 6/8] feat(postgres): declare the master probes core now gates on Core's c4d6adc gave call sites to three probes that had been decorative: format_cte_expression consults supports_materialized_cte(), format_truncate_statement consults supports_truncate(), and the CTAS / CREATE MATERIALIZED VIEW / REFRESH MATERIALIZED VIEW renderers consult the new supports_with_data_clause() before spelling WITH [NO] DATA. It also added supports_transaction_wait() for the wait / no_wait pair. A dialect that leaves any of them to a core default refuses statements it used to render: this package answered the with-data probe False through core's default, and its transaction formatters dropped the wait pair entirely. Declare each from measurement or the documented grammar: - supports_materialized_cte() keeps its >= 12 gate (already declared here). Live measurement: 9.6, 10 and 11 reject AS MATERIALIZED / AS NOT MATERIALIZED with a syntax error; 12 through 19beta4 accept both. - supports_truncate() answers True explicitly. TRUNCATE is in the synopsis of every supported version and the live matrix accepts it everywhere. - supports_with_data_clause() answers True, version-independent. The clause is in all three synopses -- the CTAS form long predates materialized views, and CREATE / REFRESH MATERIALIZED VIEW ... [ WITH [ NO ] DATA ] is documented from 9.3 (the version that introduced materialized views) -- and the live matrix accepts every form. The same probe covers the REFRESH form; it needs no probe of its own. - supports_transaction_wait() answers False. BEGIN NO WAIT and SET TRANSACTION NO WAIT are syntax errors on every measured server (9.6 through 19beta4); PostgreSQL has no lock-wait clause. The PostgreSQL overrides now consume the probes the core formatters consume: format_create_materialized_view_statement and format_refresh_materialized_view_pg_statement refuse WITH [NO] DATA by name when the clause probe declines (generic REFRESH and CTAS delegate to core's gate), format_truncate_statement consults supports_truncate(), and format_begin_transaction / format_set_transaction refuse wait / no_wait by name instead of silently dropping the pair. PostgresTruncateSupport, PostgresMaterializedViewSupport and PostgresTransactionSupport restate the declarations so the protocol and the mixin stay mirrored; the conformance forward / reverse coverage tests enforce the mirror. Behaviour changes (intended, per the round): - CTAS and generic REFRESH MATERIALIZED VIEW with WITH DATA / WITH NO DATA render instead of being refused by the fail-closed default. - BEGIN / SET TRANSACTION wait=True / no_wait=True raise UnsupportedFeatureError naming the option instead of rendering a statement that dropped it. Guard: test_clause_pair_guard.py adds the wait pair and a declaration / gate class; run red against the unmodified tree first (13 failing ids of 155), green after. test_master_probe_execution.py renders each clause through the live backend and asserts acceptance against the probe, with a sentinel and a control before every verdict; green on the 11-scenario matrix (9.6 through 19beta4). --- .../backend/impl/postgres/mixins/cte.py | 12 +- .../impl/postgres/mixins/materialized_view.py | 41 ++- .../impl/postgres/mixins/transaction.py | 41 ++- .../backend/impl/postgres/mixins/truncate.py | 22 +- .../postgres/protocols/materialized_view.py | 9 + .../impl/postgres/protocols/transaction.py | 2 + .../impl/postgres/protocols/truncate.py | 2 + .../feature/backend/test_clause_pair_guard.py | 181 ++++++++++++ .../backend/test_master_probe_execution.py | 257 ++++++++++++++++++ 9 files changed, 561 insertions(+), 6 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_master_probe_execution.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/cte.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/cte.py index 347fc658..a1e496d0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/cte.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/cte.py @@ -5,7 +5,10 @@ class PostgresCTEMixin: """PostgreSQL cte override implementation. - All features are native, using version number for detection. + ``AS [NOT] MATERIALIZED`` arrived in PostgreSQL 12. The boundary is + measured, not assumed: the live servers in this repository's matrix + reject both spellings with a syntax error through 11 and accept them + from 12 on (9.6, 10, 11 reject; 12 through 19 accept, measured). """ def supports_basic_cte(self) -> bool: @@ -15,4 +18,11 @@ def supports_recursive_cte(self) -> bool: return True def supports_materialized_cte(self) -> bool: + """Whether the ``AS [NOT] MATERIALIZED`` CTE hint can be used. + + ``True`` from PostgreSQL 12 on. PostgreSQL 11 and older have no hint + and reject both spellings with ``syntax error at or near + "MATERIALIZED"`` (measured), so those versions must answer ``False`` + or core's ``format_cte_expression`` renders a hint the server refuses. + """ return self.version >= (12, 0, 0) 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 f9198043..a50df525 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py @@ -92,10 +92,41 @@ def supports_materialized_view_restrict(self) -> bool: """ return True + def supports_with_data_clause(self) -> bool: + """Whether the ``WITH [NO] DATA`` population clause can be used. + + One probe answers all three consumers -- ``CREATE TABLE ... AS``, + ``CREATE MATERIALIZED VIEW`` and ``REFRESH MATERIALIZED VIEW`` -- as + core's ``MaterializedViewSupport`` declares. The clause is part of + all three synopses: the CTAS form long predates materialized views, + and ``CREATE``/``REFRESH MATERIALIZED VIEW ... [ WITH [ NO ] DATA ]`` + is documented from 9.3, the version that introduced materialized + views. The live matrix (9.6 through 19beta4) accepts every form + (measured), so the answer is version-independent; the statement-level + probes still gate the statements themselves. + """ + return True + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ + def _check_with_data_clause_support(self, expr: Any) -> None: + """Refuse ``WITH [NO] DATA`` by name when the clause probe declines. + + The core formatter this mixin overrides applies the same gate, so the + refusal vocabulary stays identical across the three consumers. The + gate runs before any SQL is assembled: a clause the dialect declines + is refused, never dropped and never rendered. + """ + if (expr.with_data or expr.no_data) and not self.supports_with_data_clause(): + feature = "WITH DATA" if expr.with_data else "WITH NO DATA" + raise UnsupportedFeatureError( + self.name, + feature, + f"{self.name} does not support {feature} for materialized views.", + ) + def _format_materialized_view_name(self, expr: Any) -> str: """Render a materialized view name, schema-qualified when available. @@ -166,7 +197,9 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie :class:`View` is the wrong kind here and would have had its own name rendered as the materialized view's. UnsupportedFeatureError: If the dialect or the target server version - does not support the requested materialized view feature. + does not support the requested materialized view feature, or if + the ``WITH [NO] DATA`` clause is requested and the clause probe + declines it. """ if not isinstance(expr.view, MaterializedView): raise TypeError( @@ -175,6 +208,7 @@ def format_create_materialized_view_statement(self, expr: "CreateMaterializedVie ) if not self.supports_materialized_view(): raise UnsupportedFeatureError(self.name, "CREATE MATERIALIZED VIEW") + self._check_with_data_clause_support(expr) parts = ["CREATE MATERIALIZED VIEW"] @@ -270,7 +304,9 @@ def format_refresh_materialized_view_pg_statement( Raises: TypeError: ``expr.view`` is not a :class:`MaterializedView`. - UnsupportedFeatureError: If CONCURRENTLY is requested on PG < 9.4. + UnsupportedFeatureError: If CONCURRENTLY is requested on PG < 9.4, + or if the ``WITH [NO] DATA`` clause is requested and the clause + probe declines it. """ if not isinstance(expr.view, MaterializedView): raise TypeError( @@ -278,6 +314,7 @@ def format_refresh_materialized_view_pg_statement( f"got {type(expr.view).__name__}" ) self._check_concurrent_refresh_support(expr) + self._check_with_data_clause_support(expr) parts = ["REFRESH MATERIALIZED VIEW"] if expr.concurrent: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py index 667bc444..e340e7e8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py @@ -1,7 +1,9 @@ # src/rhosocial/activerecord/backend/impl/postgres/mixins/transaction.py """PostgreSQL transaction feature support implementation.""" -from typing import Tuple, TYPE_CHECKING +from typing import Any, Dict, Tuple, TYPE_CHECKING + +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError if TYPE_CHECKING: from rhosocial.activerecord.backend.expression.transaction import ( @@ -28,9 +30,38 @@ def supports_read_only_transaction(self) -> bool: def supports_deferrable_transaction(self) -> bool: return True + def supports_transaction_wait(self) -> bool: + """Whether ``WAIT`` / ``NO WAIT`` can be spelled on a transaction. + + PostgreSQL has no lock-wait clause on ``BEGIN`` or ``SET + TRANSACTION``: ``BEGIN NO WAIT`` and ``SET TRANSACTION NO WAIT`` are + both syntax errors (measured on 9.6 through 19beta4), so the pair is + refused by name rather than dropped. The clause belongs to Firebird's + grammar. + """ + return False + def supports_savepoint(self) -> bool: return True + def _refuse_transaction_wait(self, params: Dict[str, Any]) -> None: + """Refuse the WAIT / NO WAIT pair by name when the probe declines it. + + The pair has no spelling on this dialect, so a request must fail + closed -- the same shape as every other probe-gated clause -- instead + of being silently dropped from the rendered statement. + """ + wait = params.get("wait") + no_wait = params.get("no_wait") + if (wait or no_wait) and not self.supports_transaction_wait(): + feature = "WAIT" if wait else "NO WAIT" + raise UnsupportedFeatureError( + self.name, + feature, + f"{self.name} has no {feature} transaction spelling; " + f"WAIT / NO WAIT is Firebird grammar.", + ) + def format_begin_transaction( self, expr: "BeginTransactionExpression" ) -> Tuple[str, tuple]: @@ -45,8 +76,12 @@ def format_begin_transaction( grammar; it is rendered whenever requested, not only alongside SERIALIZABLE (``BEGIN DEFERRABLE`` without an isolation level is accepted by the server -- measured on PostgreSQL 16). + + ``WAIT`` / ``NO WAIT`` has no PostgreSQL spelling; the pair is refused + by name through :meth:`supports_transaction_wait`. """ params = expr.get_params() + self._refuse_transaction_wait(params) parts = ["BEGIN"] isolation = params.get("isolation_level") @@ -80,8 +115,12 @@ def format_set_transaction( Syntax: SET TRANSACTION { ISOLATION LEVEL { ... } | { READ WRITE | READ ONLY } | [ NOT ] DEFERRABLE } [, ...] SET SESSION CHARACTERISTICS AS TRANSACTION { ... } + + ``WAIT`` / ``NO WAIT`` has no PostgreSQL spelling; the pair is refused + by name through :meth:`supports_transaction_wait`. """ params = expr.get_params() + self._refuse_transaction_wait(params) parts = [] if params.get("session"): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py index 1405b170..c92c04ee 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/truncate.py @@ -13,9 +13,20 @@ class PostgresTruncateMixin: """PostgreSQL truncate override implementation. - All features are native, using version number for detection. + ``TRUNCATE`` is in the synopsis of every version this backend supports; + only its options are version-gated. The live matrix (9.6 through + 19beta4) accepts the bare statement everywhere (measured). """ + def supports_truncate(self) -> bool: + """``TRUNCATE`` itself is in the PostgreSQL synopsis. + + Version-independent: accepted by every server in this repository's + matrix (9.6 through 19beta4, measured). The options keep their own + probes below. + """ + return True + def supports_truncate_restart_identity(self) -> bool: return self.version >= (8, 4, 0) @@ -45,7 +56,8 @@ def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tu Raises: TypeError: ``expr.table`` is not a :class:`Table`. Another object kind would have had its own name rendered as the table's. - UnsupportedFeatureError: If the requested identity continuation or + UnsupportedFeatureError: If TRUNCATE itself is not supported by this + version, or if the requested identity continuation or dependent-object behavior is not supported by this version. """ if not isinstance(expr.table, Table): @@ -53,6 +65,12 @@ def format_truncate_statement(self, expr: "TruncateExpression") -> Tuple[str, tu f"TruncateExpression.table must be a Table, " f"got {type(expr.table).__name__}" ) + if not self.supports_truncate(): + raise UnsupportedFeatureError( + self.name, + "TRUNCATE", + f"{self.name} does not support TRUNCATE.", + ) parts = ["TRUNCATE TABLE", expr.table.to_sql()[0]] if expr.restart_identity or expr.continue_identity: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py index 2e1e55f7..e08ef7a9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/materialized_view.py @@ -76,6 +76,15 @@ def supports_materialized_view_restrict(self) -> bool: """ ... + def supports_with_data_clause(self) -> bool: + """Whether the ``WITH [NO] DATA`` population clause can be used. + + Shared by ``CREATE TABLE ... AS``, ``CREATE MATERIALIZED VIEW`` and + ``REFRESH MATERIALIZED VIEW``; PostgreSQL accepts every form on all + versions this backend supports (measured on 9.6 through 19beta4). + """ + ... + def format_create_materialized_view_statement(self, expr: Any) -> Tuple[str, tuple]: """Format CREATE MATERIALIZED VIEW statement for PostgreSQL. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/transaction.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/transaction.py index 72adc730..c3189e81 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/transaction.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/transaction.py @@ -22,6 +22,8 @@ def supports_read_only_transaction(self)-> bool: ... def supports_deferrable_transaction(self)-> bool: ... + def supports_transaction_wait(self)-> bool: ... + def supports_savepoint(self)-> bool: ... def format_begin_transaction( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py index df576fd1..75b709bc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py @@ -8,6 +8,8 @@ class PostgresTruncateSupport(Protocol): """PostgreSQL truncate feature support protocol.""" + def supports_truncate(self) -> bool: ... + def supports_truncate_restart_identity(self)-> bool: ... def supports_truncate_cascade(self)-> bool: ... diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py index d4f558df..a774eb13 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py @@ -94,6 +94,12 @@ from rhosocial.activerecord.backend.impl.postgres.expression.copy import ( PostgresCopyToExpression, ) +from rhosocial.activerecord.backend.impl.postgres.mixins import ( + PostgresCTEMixin, + PostgresMaterializedViewMixin, + PostgresTransactionMixin, + PostgresTruncateMixin, +) from rhosocial.activerecord.backend.impl.postgres.expression.ddl.exclude_constraint import ( PostgresExcludeConstraint, ) @@ -482,6 +488,29 @@ def matches_b(self, sql): r"(? bool: + return False + + class _NoTruncate(PostgresDialect): + def supports_truncate(self) -> bool: + return False + + def test_with_data_is_gated_on_every_consumer(self): + """CTAS, CREATE MV and both REFRESH forms consult the one probe.""" + d = self._NoWithData(version=PG16) + assert d.supports_with_data_clause() is False + cases = ( + ( + lambda dd, **kw: CreateTableAsExpression(dd, _table(dd), _query(dd), **kw), + {"with_data": True}, + "WITH DATA", + ), + ( + lambda dd, **kw: CreateTableAsExpression(dd, _table(dd), _query(dd), **kw), + {"no_data": True}, + "WITH NO DATA", + ), + ( + lambda dd, **kw: CreateMaterializedViewExpression( + dd, MaterializedView(dd, "mv"), _query(dd), **kw + ), + {"with_data": True}, + "WITH DATA", + ), + ( + lambda dd, **kw: CreateMaterializedViewExpression( + dd, MaterializedView(dd, "mv"), _query(dd), **kw + ), + {"no_data": True}, + "WITH NO DATA", + ), + ( + lambda dd, **kw: RefreshMaterializedViewExpression( + dd, MaterializedView(dd, "mv"), **kw + ), + {"with_data": True}, + "WITH DATA", + ), + ( + lambda dd, **kw: RefreshMaterializedViewExpression( + dd, MaterializedView(dd, "mv"), **kw + ), + {"no_data": True}, + "WITH NO DATA", + ), + ( + lambda dd, **kw: PostgresRefreshMaterializedViewExpression( + dd, MaterializedView(dd, "mv"), **kw + ), + {"with_data": True}, + "WITH DATA", + ), + ( + lambda dd, **kw: PostgresRefreshMaterializedViewExpression( + dd, MaterializedView(dd, "mv"), **kw + ), + {"no_data": True}, + "WITH NO DATA", + ), + ) + for builder, kwargs, feature in cases: + with pytest.raises(UnsupportedFeatureError, match=feature): + builder(d, **kwargs).to_sql() + + def test_truncate_is_gated_on_its_probe(self): + d = self._NoTruncate(version=PG16) + assert d.supports_truncate() is False + with pytest.raises(UnsupportedFeatureError, match="TRUNCATE"): + TruncateExpression(d, _table(d)).to_sql() + + def test_wait_pair_refuses_by_name_on_postgres(self): + d = _dialect(PG16) + with pytest.raises(UnsupportedFeatureError, match="WAIT"): + BeginTransactionExpression(d, wait=True).to_sql() + with pytest.raises(UnsupportedFeatureError, match="NO WAIT"): + BeginTransactionExpression(d, no_wait=True).to_sql() + with pytest.raises(UnsupportedFeatureError, match="WAIT"): + SetTransactionExpression(d, wait=True).to_sql() + with pytest.raises(UnsupportedFeatureError, match="NO WAIT"): + SetTransactionExpression(d, no_wait=True).to_sql() diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_master_probe_execution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_master_probe_execution.py new file mode 100644 index 00000000..0c69937b --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_master_probe_execution.py @@ -0,0 +1,257 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_master_probe_execution.py +"""Execution confirmation for the master probes this backend declares. + +Rendering is not execution: the probe answers below are confirmed against the +live server the scenario carries. Every verdict is preceded by a sentinel -- +a deliberately invalid statement must classify ``REJECTED`` -- and an accepted +control, so a null result can never be read as acceptance. + +The declarations were measured on this repository's matrix (PostgreSQL 9.6 +through 19beta4); this file keeps that measurement from rotting, and each test +asserts outcome against the dialect's own probe answer so a probe that drifts +from the server fails here rather than in the field. +""" + +import pytest + +from rhosocial.activerecord.backend.expression.core import Literal +from rhosocial.activerecord.backend.expression.execution_testing import ( + ExecutionOutcome, + classify_execution, + confirm_expression_execution, +) +from rhosocial.activerecord.backend.expression.objects import ( + MaterializedView, + Table, +) +from rhosocial.activerecord.backend.expression.query_sources import ( + CTEExpression, + WithQueryExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_table import ( + CreateTableAsExpression, + DropTableExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_truncate import ( + TruncateExpression, +) +from rhosocial.activerecord.backend.expression.statements.ddl_view import ( + CreateMaterializedViewExpression, + DropMaterializedViewExpression, + RefreshMaterializedViewExpression, +) +from rhosocial.activerecord.backend.expression.statements.dql import QueryExpression + +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.mv import ( + PostgresRefreshMaterializedViewExpression, +) + + +def _table(d, name): + return Table(d, name) + + +def _literal(d, value=1): + """An inlined literal: MV/CTAS bodies reject bind parameters on PostgreSQL.""" + return Literal(d, value, inline_literals=True) + + +def _query(d): + return QueryExpression(d, select=[_literal(d)]) + + +def _cte_query(d, cte_name, **kwargs): + """A WITH query whose main query selects from the single CTE it defines.""" + return WithQueryExpression( + d, + ctes=[ + CTEExpression( + d, + cte_name, + QueryExpression(d, select=[_literal(d)]), + **kwargs, + ) + ], + main_query=QueryExpression( + d, select=[_literal(d)], from_=_table(d, cte_name) + ), + ) + + +class TestMasterProbeExecutionConfirmation: + """The four master probes' outcomes are confirmed per live server.""" + + def test_sentinel_rejected_and_control_accepted(self, postgres_backend): + """The classifier must see both answers before any verdict is trusted.""" + assert ( + classify_execution(postgres_backend, "THIS IS NOT SQL") + is ExecutionOutcome.REJECTED + ) + assert ( + classify_execution(postgres_backend, "SELECT 1") + is ExecutionOutcome.ACCEPTED + ) + + @pytest.mark.parametrize( + "side,kwargs,table_name", + [ + ("with_data", {"with_data": True}, "master_probe_ctas_with"), + ("no_data", {"no_data": True}, "master_probe_ctas_no"), + ], + ids=["with_data", "no_data"], + ) + def test_ctas_with_data_matches_the_probe( + self, postgres_backend, side, kwargs, table_name + ): + dialect = postgres_backend.dialect + outcome = confirm_expression_execution( + postgres_backend, + CreateTableAsExpression( + dialect, _table(dialect, table_name), _query(dialect), **kwargs + ), + teardown=[ + DropTableExpression( + dialect, _table(dialect, table_name), if_exists=True + ) + ], + ) + if dialect.supports_with_data_clause(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + @pytest.mark.parametrize( + "side,kwargs,view_name", + [ + ("with_data", {"with_data": True}, "master_probe_mv_with"), + ("no_data", {"no_data": True}, "master_probe_mv_no"), + ], + ids=["with_data", "no_data"], + ) + def test_create_materialized_view_with_data_matches_the_probe( + self, postgres_backend, side, kwargs, view_name + ): + dialect = postgres_backend.dialect + view = MaterializedView(dialect, view_name) + outcome = confirm_expression_execution( + postgres_backend, + CreateMaterializedViewExpression(dialect, view, _query(dialect), **kwargs), + teardown=[ + DropMaterializedViewExpression(dialect, view, if_exists=True) + ], + ) + if dialect.supports_materialized_view() and dialect.supports_with_data_clause(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + @pytest.mark.parametrize( + "expression_cls,side,kwargs", + [ + (RefreshMaterializedViewExpression, "with_data", {"with_data": True}), + (RefreshMaterializedViewExpression, "no_data", {"no_data": True}), + ( + PostgresRefreshMaterializedViewExpression, + "with_data", + {"with_data": True}, + ), + ( + PostgresRefreshMaterializedViewExpression, + "no_data", + {"no_data": True}, + ), + ], + ids=[ + "generic-with_data", + "generic-no_data", + "postgres-with_data", + "postgres-no_data", + ], + ) + def test_refresh_materialized_view_with_data_matches_the_probe( + self, postgres_backend, expression_cls, side, kwargs + ): + dialect = postgres_backend.dialect + view_name = "master_probe_mv_refresh" + view = MaterializedView(dialect, view_name) + teardown = [ + DropMaterializedViewExpression(dialect, view, if_exists=True) + ] + + if dialect.supports_materialized_view(): + # Build an unpopulated MV so the refresh has something to target. + prepare = [ + CreateMaterializedViewExpression( + dialect, view, _query(dialect), no_data=True + ) + ] + else: + prepare = [] + + outcome = confirm_expression_execution( + postgres_backend, + expression_cls(dialect, view, **kwargs), + prepare=prepare, + teardown=teardown, + ) + if ( + dialect.supports_refresh_materialized_view() + and dialect.supports_with_data_clause() + ): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + @pytest.mark.parametrize( + "side,kwargs", + [ + ("materialized", {"materialized": True}), + ("not_materialized", {"not_materialized": True}), + ], + ids=["materialized", "not_materialized"], + ) + def test_materialized_cte_matches_the_probe(self, postgres_backend, side, kwargs): + dialect = postgres_backend.dialect + outcome = confirm_expression_execution( + postgres_backend, + _cte_query(dialect, f"master_probe_cte_{side}", **kwargs), + ) + if dialect.supports_materialized_cte(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + # PostgreSQL 11 and older: the hint is a syntax error, so the + # expression must refuse instead of rendering it. + assert outcome is ExecutionOutcome.NOT_RENDERED + + def test_truncate_matches_the_probe(self, postgres_backend): + dialect = postgres_backend.dialect + table_name = "master_probe_truncate" + outcome = confirm_expression_execution( + postgres_backend, + TruncateExpression(dialect, _table(dialect, table_name)), + prepare=[ + CreateTableAsExpression( + dialect, _table(dialect, table_name), _query(dialect) + ) + ], + teardown=[ + DropTableExpression( + dialect, _table(dialect, table_name), if_exists=True + ) + ], + ) + if dialect.supports_truncate(): + assert outcome is ExecutionOutcome.ACCEPTED + else: + assert outcome is ExecutionOutcome.NOT_RENDERED + + def test_server_rejects_wait_spelling(self, postgres_backend): + """WAIT / NO WAIT is not PostgreSQL grammar; the False probe holds.""" + assert ( + classify_execution(postgres_backend, "BEGIN NO WAIT") + is ExecutionOutcome.REJECTED + ) + assert ( + classify_execution(postgres_backend, "SET TRANSACTION NO WAIT") + is ExecutionOutcome.REJECTED + ) From 724621449d40b4771a508ade6ad3f9d24b8993cd Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 8 Oct 2026 03:55:42 +0800 Subject: [PATCH 7/8] ci: pin the core dependency to the merged release line The schema-object refactor these branches depend on landed on the core's release/v1.0.0.dev30 line as PR #125, so the workflow no longer needs to install the core from the feature branch. CORE_PKG now points at the release branch; the testsuite dependency stays on its own feature branch, which is not merged yet. --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8933152a..38081818 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,7 +68,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/expression-schema-object-refactor" + 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@feature/expression-schema-object-refactor" - name: Create postgres scenario config @@ -279,7 +279,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/expression-schema-object-refactor" + 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@feature/expression-schema-object-refactor" - name: Create postgres scenario config @@ -391,7 +391,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/expression-schema-object-refactor" + 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@feature/expression-schema-object-refactor" - name: Create postgres scenario config @@ -489,7 +489,7 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/expression-schema-object-refactor" + 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@feature/expression-schema-object-refactor" - name: Create postgres scenario config From aebf3d77cd83ed263bfeaf7a417f645d3d93730e Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Thu, 8 Oct 2026 05:07:36 +0800 Subject: [PATCH 8/8] chore: add changelog fragment for PR #61 --- changelog.d/61.added.md | 1 + changelog.d/61.changed.md | 1 + changelog.d/61.fixed.md | 1 + changelog.d/61.removed.md | 1 + 4 files changed, 4 insertions(+) create mode 100644 changelog.d/61.added.md create mode 100644 changelog.d/61.changed.md create mode 100644 changelog.d/61.fixed.md create mode 100644 changelog.d/61.removed.md diff --git a/changelog.d/61.added.md b/changelog.d/61.added.md new file mode 100644 index 00000000..8f1ac03f --- /dev/null +++ b/changelog.d/61.added.md @@ -0,0 +1 @@ +Add the PostgreSQL capability declarations the expression/dialect split asks the backend to state: `PostgresIdentityColumnMixin`/`PostgresIdentitySupport` declare SQL-standard identity columns from PostgreSQL 10 on with one probe per generation mode and sequence option (`ALWAYS`, `START`, `INCREMENT`, `MINVALUE`, `MAXVALUE`, `CYCLE`, `CACHE`) and route the legacy `is_auto_increment` column constraint through the gated formatter, `PostgresAutoIncrementMixin`/`PostgresAutoIncrementSupport` answer the bare `AUTO_INCREMENT` marker `False` explicitly, the sequence probes now cover every `CREATE`/`ALTER SEQUENCE` option including `ALTER SEQUENCE ... START WITH`, the newly measured restrict and cache probes (`supports_schema_restrict`, `supports_restrict_view`, `supports_materialized_view_restrict`, `supports_truncate_restrict`, `supports_drop_function_restrict`, `supports_identity_cache`) and the routine probes are declared, and the master probes (`supports_materialized_cte`, `supports_truncate`, `supports_with_data_clause`, `supports_transaction_wait`) let core's formatters render `WITH [NO] DATA` on CTAS, `CREATE MATERIALIZED VIEW` and `REFRESH MATERIALIZED VIEW`; the naming side is stated once in the new `PostgresNamespaceMixin`/`PostgresNamespaceSupport`, which spells `[database.][schema.]name` level by level. diff --git a/changelog.d/61.changed.md b/changelog.d/61.changed.md new file mode 100644 index 00000000..bbff5869 --- /dev/null +++ b/changelog.d/61.changed.md @@ -0,0 +1 @@ +Change what PostgreSQL renders when no spelling was requested and how unexpressible spellings fail: `CREATE MATERIALIZED VIEW` with neither `with_data` nor `no_data` set no longer appends `WITH NO DATA`, `SET TRANSACTION` with neither deferrability parameter set no longer appends `NOT DEFERRABLE`, `BEGIN ... DEFERRABLE` renders for any isolation level instead of only alongside `SERIALIZABLE`, `WAIT`/`NO WAIT` on `BEGIN`/`SET TRANSACTION` and `NO CACHE` on sequences and identity columns now raise `UnsupportedFeatureError` naming the option instead of being silently dropped or rendered, and a PostgreSQL 9 server asked for an identity column is refused instead of being handed `GENERATED ... AS IDENTITY` (the `SERIAL` type family remains the explicit pre-10 choice). diff --git a/changelog.d/61.fixed.md b/changelog.d/61.fixed.md new file mode 100644 index 00000000..77f64081 --- /dev/null +++ b/changelog.d/61.fixed.md @@ -0,0 +1 @@ +Fix the clauses the pre-split formatters dropped or could not reach: `TRUNCATE ... CONTINUE IDENTITY` and `TRUNCATE ... RESTRICT`, `DROP MATERIALIZED VIEW ... RESTRICT`, `COPY ... FORCE_ARRAY FALSE`, table-constraint `ENFORCED`/`NOT ENFORCED` (the tri-state gate refused every state) and the `EXCLUDE` constraint's independent `DEFERRABLE`/`INITIALLY ...` attributes now render, the transaction manager's `set_deferrable(False)` renders `NOT DEFERRABLE` instead of raising, `CreateFunctionExpression`/`DropFunctionExpression` render through the core formatters instead of resolving to a protocol stub, and the six type names that were never module attributes (`VECTOR`, `HALFVEC`, `BITVEC`, `SPARSEVEC`, `GEOMETRY`, `GEOGRAPHY`) were removed from `type_values.__all__`, where they broke star imports. diff --git a/changelog.d/61.removed.md b/changelog.d/61.removed.md new file mode 100644 index 00000000..02462166 --- /dev/null +++ b/changelog.d/61.removed.md @@ -0,0 +1 @@ +**BREAKING**: Remove `PostgresDialect.supports_auto_increment()` (ask `supports_identity_column()` or `supports_auto_increment_column()` instead), reshape the PostgreSQL DDL expressions to carry schema objects (`PostgresCreateMaterializedViewExpression`, `PostgresDropMaterializedViewExpression`, `PostgresRefreshMaterializedViewExpression` and `PostgresAlterMaterializedViewExpression` take a `MaterializedView` instead of `view_name`/`name` plus `schema`, `PostgresCreateIndexExpression` takes `index` and `table` instead of `index_name` and `table_name`, `PostgresDropTypeExpression` takes `type` instead of `type_name` plus `schema_name`, and `PostgresCommentExpression` takes `object` instead of `object_name` plus `schema`), replace their tri-state options with one parameter per spelling (`with_data`/`no_data` on `PostgresCreateMaterializedViewExpression` and `PostgresRefreshMaterializedViewExpression`, `force_array`/`force_array_false` on `PostgresCopyToExpression`, and `deferrable`/`not_deferrable`, `initially_deferred`/`initially_immediate`, `enforced`/`not_enforced` on `PostgresExcludeConstraint`), and return `(sql, params)` from `format_materialized_view_alter_action()` instead of a bare string; setting both sides of a pair raises `ValueError` at construction.