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..38081818 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ 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-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - name: Create postgres scenario config run: | @@ -280,7 +280,7 @@ 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-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - name: Create postgres scenario config run: | @@ -392,7 +392,7 @@ 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-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - name: Create postgres scenario config run: | @@ -490,7 +490,7 @@ 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-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/expression-schema-object-refactor" - name: Create postgres scenario config run: | 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. 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..d98c04dc 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,40 @@ DomainMixin, ) from rhosocial.activerecord.backend.dialect.protocols import ( + AlterDatabaseSupport, + AlterDomainSupport, + AlterTableSupport, + AlterTypeSupport, + CreateDatabaseSupport, + CreateDomainSupport, + CreateIndexSupport, + CreateRoutineSupport, + CreateSchemaSupport, + CreateTableSupport, + CreateTriggerSupport, + CreateTypeSupport, + CreateViewSupport, + DropDatabaseSupport, + DropDomainSupport, + DropIndexSupport, + DropRoutineSupport, + DropSchemaSupport, + DropTableSupport, + DropTriggerSupport, + DropTypeSupport, + DropViewSupport, + FulltextIndexSupport, + MaterializedViewObjectSupport, + MaterializedViewSupport, + NamespaceSupport, + RoutineObjectSupport, + SequenceObjectSupport, + TableObjectSupport, + TriggerObjectSupport, + TypeObjectSupport, + IndexObjectSupport, + ViewObjectSupport, + ForeignTableObjectSupport, SQLXMLSupport, SQLXMLParsingSupport, SQLXMLSerializationSupport, @@ -78,9 +131,6 @@ LateralJoinSupport, WildcardSupport, JoinSupport, - ViewSupport, - SchemaSupport, - SequenceSupport, SetOperationSupport, TruncateSupport, ILIKESupport, @@ -88,10 +138,9 @@ TransactionControlSupport, SQLFunctionSupport, DDLTypeSupport, - UserDefinedTypeSupport, - DomainSupport, ) from .mixins import ( + PostgresObjectNameMixin, PostgresExtensionMixin, PostgresMaterializedViewMixin, PostgresTableMixin, @@ -126,6 +175,7 @@ PostgresJoinMixin, PostgresTruncateMixin, PostgresSchemaMixin, + PostgresNamespaceMixin, PostgresDatabaseMixin, PostgresSequenceMixin, PostgresTransactionMixin, @@ -195,6 +245,8 @@ PostgresDQLMixin, PostgresJSONMixin, PostgresGeneratedColumnMixin, + PostgresIdentityColumnMixin, + PostgresAutoIncrementMixin, PostgresExpressionMixin, PostgresFunctionMixin, ) @@ -237,7 +289,10 @@ PostgresJoinSupport, PostgresTruncateSupport, PostgresSchemaSupport, + PostgresNamespaceSupport, PostgresSequenceSupport, + PostgresIdentitySupport, + PostgresAutoIncrementSupport, PostgresTransactionSupport, PostgresViewSupport, PostgresXMLSupport, @@ -301,17 +356,35 @@ 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, PostgresJSONMixin, PostgresGeneratedColumnMixin, + PostgresIdentityColumnMixin, + PostgresAutoIncrementMixin, PostgresExpressionMixin, PostgresFunctionMixin, # Per-feature mixins @@ -329,6 +402,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 +559,49 @@ 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, + # 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, + AlterTypeSupport, + DropTypeSupport, + CreateDomainSupport, + AlterDomainSupport, + DropDomainSupport, + CreateSchemaSupport, + DropSchemaSupport, + CreateDatabaseSupport, + DropDatabaseSupport, + AlterDatabaseSupport, + CreateRoutineSupport, + DropRoutineSupport, # Introspection protocol IntrospectionSupport, # Transaction control protocol @@ -488,7 +609,6 @@ class PostgresDialect( # PostgreSQL-specific protocols PostgresExtensionSupport, PostgresMaterializedViewSupport, - PostgresTableSupport, PostgresPgvectorSupport, PostgresPostGISSupport, PostgresPostgisRasterSupport, @@ -497,7 +617,6 @@ class PostgresDialect( PostgresHstoreSupport, # Native feature protocols PostgresPartitionSupport, - PostgresIndexSupport, PostgresVacuumSupport, PostgresCopySupport, PostgresQueryOptimizationSupport, @@ -518,7 +637,10 @@ class PostgresDialect( PostgresJoinSupport, PostgresTruncateSupport, PostgresSchemaSupport, + PostgresNamespaceSupport, PostgresSequenceSupport, + PostgresIdentitySupport, + PostgresAutoIncrementSupport, PostgresTransactionSupport, PostgresViewSupport, PostgresXMLSupport, @@ -550,7 +672,6 @@ class PostgresDialect( PostgresOrafceSupport, PostgresAddressStandardizerSupport, # DDL feature protocols - PostgresTriggerSupport, PostgresCommentSupport, PostgresConstraintSupport, PostgresPolicySupport, @@ -569,6 +690,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..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,10 @@ # 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, 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)), @@ -62,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="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")), + attributes=[IdentityAttribute(generation="ALWAYS")]), ColumnDefinition(dialect, "name", VarCharType(dialect, length=100)), ] ) 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/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/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/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/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..0ed960b5 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,10 +75,10 @@ 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, + ... with_data=True, ... storage_options={PostgresStorageParameter.FILLFACTOR: 70}, ... ) >>> sql, params = create.to_sql() @@ -87,45 +90,44 @@ 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, + with_data: bool = False, + no_data: bool = False, 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, + no_data=no_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 +136,18 @@ class PostgresDropMaterializedViewExpression(DropMaterializedViewExpression): def __init__( self, dialect: "SQLDialectBase", - view_name: str, + view: MaterializedView, if_exists: bool = False, cascade: bool = False, - schema: Optional[str] = None, + restrict: bool = False, ): super().__init__( dialect, - view_name=view_name, + view=view, if_exists=if_exists, cascade=cascade, + restrict=restrict, ) - _validate_name(view_name, "view_name") - if schema is not None: - _validate_name(schema, "schema") - self.schema = schema class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpression): @@ -157,41 +156,42 @@ 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", - ... with_data=False, + ... view=MaterializedView(dialect, "monthly_sales_summary"), + ... no_data=True, ... ) """ @@ -199,26 +199,28 @@ class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpressio def __init__( self, dialect: "SQLDialectBase", - name: str, - schema: Optional[str] = None, + view: MaterializedView, concurrently: bool = False, - with_data: Optional[bool] = None, + with_data: bool = False, + no_data: bool = False, ): super().__init__( dialect, - view_name=name, + view=view, concurrent=concurrently, with_data=with_data, + no_data=no_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 +357,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 +371,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 +384,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..c9dff656 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 @@ -18,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 @@ -112,6 +115,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 @@ -132,6 +140,8 @@ "PostgresDQLMixin", "PostgresJSONMixin", "PostgresGeneratedColumnMixin", + "PostgresIdentityColumnMixin", + "PostgresAutoIncrementMixin", "PostgresExpressionMixin", "PostgresFunctionMixin", # DDL mixins @@ -219,6 +229,8 @@ "PostgresJoinMixin", "PostgresTruncateMixin", "PostgresSchemaMixin", + "PostgresNamespaceMixin", + "PostgresCatalogMixin", # deprecated alias of PostgresNamespaceMixin "PostgresDatabaseMixin", "PostgresSequenceMixin", "PostgresTransactionMixin", 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/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/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/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/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/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..11f03bc5 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,127 @@ 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() + + 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() + # ------------------------------------------------------------------ # - # 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..9c806485 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,10 @@ import re 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: """PostgreSQL table extended features implementation.""" @@ -119,11 +123,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 ...)") @@ -187,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/ddl_view.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_view.py index 3428f2bd..c4364360 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 @@ -32,12 +34,21 @@ 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. - ``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 +59,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 +79,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/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/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/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..0a5d3176 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/identity_column.py @@ -0,0 +1,108 @@ +# 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 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`` 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: + """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 + + 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 8a5e026c..a50df525 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, @@ -83,27 +83,62 @@ 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 + + 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. 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,14 +175,16 @@ 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 (… )``). - ``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 @@ -156,11 +193,22 @@ 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. + 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( + 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") + self._check_with_data_clause_support(expr) parts = ["CREATE MATERIALIZED VIEW"] @@ -190,7 +238,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 @@ -198,8 +246,22 @@ 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( + 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") @@ -209,6 +271,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]: @@ -233,16 +303,27 @@ def format_refresh_materialized_view_pg_statement( Tuple of (SQL statement, parameters tuple) Raises: - UnsupportedFeatureError: If CONCURRENTLY is requested on PG < 9.4. + TypeError: ``expr.view`` is not a :class:`MaterializedView`. + 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( + f"{type(expr).__name__}.view must be a MaterializedView, " + 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: 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( @@ -267,37 +348,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..1aba0ec0 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: @@ -27,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 a71928ef..b54fe91f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/sequence.py @@ -1,15 +1,144 @@ # 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. +""" + +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. - 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. + + 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: + 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_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 + + 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 + + # ------------------------------------------------------------------ + # 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..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]: @@ -41,9 +72,16 @@ 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). + + ``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") @@ -59,11 +97,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), () @@ -78,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"): @@ -103,8 +144,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 99603159..c92c04ee 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 @@ -12,42 +13,90 @@ 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) 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_name`` — target table. - - ``expr.restart_identity`` — add ``RESTART IDENTITY`` (PG 8.4+). - - ``expr.cascade`` — add ``CASCADE``. + - ``expr.table`` — the table being truncated, which renders its own + name and namespace. + - ``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 TRUNCATE itself is not supported by this + version, or if the requested identity continuation or + dependent-object behavior is not supported by this version. """ - 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) + 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: + 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/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..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 @@ -100,6 +104,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 @@ -181,6 +186,9 @@ "PostgresQueryOptimizationSupport", "PostgresLogicalReplicationSupport", "PostgresParallelQuerySupport", + # Identity / auto-increment mechanism protocols + "PostgresIdentitySupport", + "PostgresAutoIncrementSupport", # Per-feature protocols "PostgresCTESupport", "PostgresWindowSupport", @@ -196,6 +204,8 @@ "PostgresJoinSupport", "PostgresTruncateSupport", "PostgresSchemaSupport", + "PostgresCatalogSupport", # deprecated alias of PostgresNamespaceSupport + "PostgresNamespaceSupport", "PostgresSequenceSupport", "PostgresTransactionSupport", "PostgresViewSupport", 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/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/identity_column.py b/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py new file mode 100644 index 00000000..301a1a39 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/identity_column.py @@ -0,0 +1,47 @@ +# 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 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 + +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: ... + + 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 ed82eae3..e08ef7a9 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,23 @@ 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 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. @@ -135,7 +152,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 +162,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..6aeb44e9 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,6 @@ def supports_schema_if_exists(self)-> bool: ... def supports_schema_cascade(self)-> bool: ... - def supports_schema_authorization(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 ce507088..8e3df803 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py @@ -1,13 +1,81 @@ # src/rhosocial/activerecord/backend/impl/postgres/protocols/sequence.py -"""PostgreSQL sequence feature support protocol.""" +"""PostgreSQL sequence feature support protocol. -from typing import Protocol, runtime_checkable +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, Tuple, TYPE_CHECKING + +from rhosocial.activerecord.backend.dialect.protocols import ( + AlterSequenceSupport, + CreateSequenceSupport, + DropSequenceSupport, +) + +if TYPE_CHECKING: + from ...expression.statements.ddl_sequence import ( + AlterSequenceExpression, + CreateSequenceExpression, + ) @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_alter_sequence_start(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_sequence_order(self) -> bool: ... + + def supports_sequence_owned_by(self) -> bool: ... - def supports_create_sequence(self)-> bool: ... + def format_create_sequence_statement( + self, expr: "CreateSequenceExpression" + ) -> Tuple[str, tuple]: ... - def supports_drop_sequence(self)-> bool: ... + def format_alter_sequence_statement( + self, expr: "AlterSequenceExpression" + ) -> Tuple[str, tuple]: ... 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/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 889511ec..75b709bc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/protocols/truncate.py @@ -8,8 +8,12 @@ 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: ... + 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/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..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 @@ -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,28 +25,36 @@ 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'), 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="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="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..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 @@ -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' @@ -126,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): @@ -139,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): @@ -150,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") ' @@ -157,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() @@ -170,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"' ) @@ -180,7 +205,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,10 +213,26 @@ 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' + 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.""" @@ -199,23 +240,27 @@ 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 == () - @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_name="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}' 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 +269,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 +278,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 +287,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 +317,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 +355,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 +370,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 +379,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 +387,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 +396,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,20 +411,20 @@ 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() 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): """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 +434,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 +447,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 +470,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 +485,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 +598,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 +620,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 +630,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 +749,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 +810,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 +829,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/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/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..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 @@ -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 ) @@ -398,12 +399,12 @@ 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, columns=["parent_id"], - foreign_key_table="people", + foreign_key_table=Table(dialect, 'people'), foreign_key_columns=["id"], enforced=True, ) @@ -411,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' @@ -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, ) @@ -483,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 @@ -494,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] == ( @@ -505,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 ( @@ -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..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 @@ -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,43 +95,43 @@ 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() 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, - name="monthly_sales_summary", - with_data=False, + view=MaterializedView(dialect_pg93, "monthly_sales_summary"), + 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, - name="monthly_sales_summary", - with_data=False, + view=MaterializedView(dialect, "monthly_sales_summary"), + 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, - name="monthly_sales_summary", + view=MaterializedView(dialect, "monthly_sales_summary"), concurrently=True, - with_data=False, + no_data=True, ) sql, params = expr.to_sql() assert "CONCURRENTLY" in sql @@ -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..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 @@ -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,9 +76,9 @@ 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, + no_data=True, ) sql, params = expression.to_sql() mv_backend.execute(sql, params, options=DDL) @@ -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,9 +202,9 @@ 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, + no_data=True, ) sql, params = empty.to_sql() mv_backend.execute(sql, params, options=DDL) @@ -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(), + 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_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..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 @@ -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,14 +375,14 @@ 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 + no_data=True ) sql, params = create_mv.to_sql() @@ -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,14 +541,14 @@ 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 + no_data=True ) sql, params = create_mv.to_sql() @@ -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..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, ) @@ -19,11 +26,14 @@ BooleanType, IntegerType, TextType, TimestampType, VarCharType, ) from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.expression.objects import Table @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): @@ -32,7 +42,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 +70,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 +83,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)), @@ -81,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_clause_pair_guard.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py new file mode 100644 index 00000000..a774eb13 --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py @@ -0,0 +1,851 @@ +# 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.mixins import ( + PostgresCTEMixin, + PostgresMaterializedViewMixin, + PostgresTransactionMixin, + PostgresTruncateMixin, +) +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"(? 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_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..d9eeaef4 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,1343 @@ # 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. The enforcement keyword is mandatory in the + action's grammar, so the pair is set explicitly (``enforced=True``) as well. + 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, + enforced=True, + ) + + +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_identity_execution.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py new file mode 100644 index 00000000..2ba4398f --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py @@ -0,0 +1,271 @@ +# 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", + "supports_identity_cache", + ): + assert getattr(dialect, probe)() is True, probe + + def test_cache_is_declared_and_no_cache_is_refused(self): + """CACHE n is expressible; NO CACHE has no identity spelling. + + The positive spelling is accepted by the server (measured: + ``GENERATED BY DEFAULT AS IDENTITY (CACHE 1)``), while ``NO CACHE`` is + a syntax error there, so the negative is refused by name instead of + being rendered or dropped. + """ + dialect = PostgresDialect(version=(18, 0, 0)) + assert dialect.supports_identity_cache() is True + sql, _ = dialect.format_identity_clause(IdentityClause(dialect, cache=10)) + assert "CACHE 10" in sql + with pytest.raises(UnsupportedFeatureError, match="IDENTITY NO CACHE"): + dialect.format_identity_clause(IdentityClause(dialect, no_cache=True)) + + 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_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 + ) 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..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 @@ -105,27 +105,71 @@ 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.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 + # 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 +211,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 +249,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 +276,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 +364,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 +426,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 +473,22 @@ 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"), + ("PostgresIdentitySupport", "IdentityColumnSupport"), + ("PostgresAutoIncrementSupport", "AutoIncrementColumnSupport"), ] @pytest.mark.parametrize("pg_name,generic_name", PROTOCOL_DERIVATIONS) @@ -520,7 +634,14 @@ 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.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), @@ -585,10 +706,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 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/transaction/test_transaction_advanced_features.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/transaction/test_transaction_advanced_features.py index 1cf34cab..3e218af9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/transaction/test_transaction_advanced_features.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/transaction/test_transaction_advanced_features.py @@ -93,14 +93,15 @@ def test_not_deferrable_serializable(self, postgres_backend, test_tables): rows = postgres_backend.fetch_all("SELECT * FROM test_deferred_parent") assert len(rows) == 1 - def test_deferrable_requires_serializable(self, postgres_backend, test_tables): - """Test that DEFERRABLE setting is only meaningful for SERIALIZABLE isolation. + def test_deferrable_renders_for_non_serializable_too(self, postgres_backend, test_tables): + """``[NOT] DEFERRABLE`` is an independent transaction mode. - The dialect should ignore DEFERRABLE for non-SERIALIZABLE isolation levels. + The clause is rendered whenever requested, not only for SERIALIZABLE + isolation; PostgreSQL accepts ``BEGIN READ COMMITTED DEFERRABLE``. """ tx_manager = postgres_backend.transaction_manager - # Set READ COMMITTED with DEFERRABLE (should be ignored) + # Set READ COMMITTED with DEFERRABLE (a legal transaction mode pair) tx_manager.isolation_level = IsolationLevel.READ_COMMITTED tx_manager.set_deferrable(True)