Skip to content

Commit b6729ca

Browse files
authored
feat(postgres)!: the catalogue as nodes, and one introspect question per family (#63)
* fix(types): refuse a non-boolean unsigned when the type is rendered The shape check now lives where the flag has to be honoured rather than where it is declared, so a value that would silently render the wrong DDL is named instead. The attribute is read directly: whether the field exists is settled by the class, so probing for it with a default would only turn a wrong class into a quiet signed column. CI installs the core library from fix/column-constraint-gates so this backend runs against the branch it depends on. * refactor(postgres): one class per storage, not two Remove PostgresByteaType, PostgresUUIDType and PostgresXMLType. Each rendered byte-identical SQL to the core class it derived from, and DataType.__eq__ is type(self) is type(other) -- so a class differing only in its name made a declaration and the catalog row it produced compare unequal, and the differ reported an unchanged column as changed. Unlike Firebird's, these were not unreachable: parse_type answered BYTEA, BLOB, UUID and XML with the Postgres class rather than the concept, so the breakage was live in both directions. It now answers with the core class, which is what makes the round trip close -- each of the three comes back to itself, where before it came back as a different class than it started as. The round-trip test's WIDENING_ANSWERS loses three entries for that reason. Those were not widenings: they recorded that parse_type disagreed with the declaration, which is the defect. * refactor(postgres): read declared fields directly instead of probing with getattr Each of these sits behind a parameter whose annotation names the class, and each field is in that class's constructor signature -- so nothing was ever in doubt, and the default was only making a missing field look like a legal answer. * refactor(postgres): follow the statement and expression renames Mechanical rename wave against the core's W1/W2/W3a/W3b/S1-c breaks. Constraint surfaces follow in the next commit -- see below. Migration surface (src + tests + docs, 215 files): W2, 57 statement-level classes -> *Statement (core 34e1fb45). The largest here: QueryExpression->QueryStatement 688, CreateTableExpression ->CreateTableStatement 415, DropTableExpression->DropTableStatement 309, InsertExpression->InsertStatement 283, CreateIndexExpression ->CreateIndexStatement 86, UpdateExpression->UpdateStatement 39, RefreshMaterializedViewExpression->RefreshMaterializedViewStatement 36, TruncateExpression->TruncateStatement 36, CreateTableLikeExpression 36, CreateMaterializedViewExpression 33, DropMaterializedViewExpression 32, and the rest. W3a, predicate/clause/match nodes (core 890aaaa3): ILIKEExpression ->ILikePredicate 29, FulltextMatchExpression->FulltextMatch 13. W3b, value/function/introspection families (core 5dc64fd5): WildcardExpression->Wildcard 30, SQLValue 18, QualifiedIdentifier 10, JSONTextValue 10, Introspection*Info 53, CastFunction 8, JSONDocumentValue 8, CaseFunction 3, CollateFunction 3, UUIDCastFunction 3. UUIDCastExpression->UUIDCastFunction is the one mapping the wave guide does not list; verified against the core commit. W1, CTE family (core 683a183e): CTEExpression->CTEClause 24, WithQueryExpression->WithStatement 20, and the call-site keyword main_query=->query= 13 (every occurrence in this repo was an argument of WithQueryExpression). S1-c, PASSING: no change needed -- this backend constructs the XML expressions through the xmlexists()/xmlquery() factories, whose passing= signature is unchanged. Override points renamed (functional; not renaming would drop the override): format_ilike_predicate 5, format_begin_transaction_statement 2, format_cte_clause 1 (mixins/cte.py comment). Behaviour: none. Every edit is a name; no SQL spelling moved. Note: the constraint surfaces still reference the enums core's S3 removed (ColumnConstraintType/TableConstraintType), so this commit is an expected red window -- the whole package fails to import until the next commit. Wave 2 migrates the construction sites onto the branch nodes. Verification: - import smoke over the postgres backend modules: 147 import clean, 13 fail and all 13 share the single deleted-symbol cause (cannot import name 'TableConstraintType') raised from expression/ddl/exclude_constraint.py, which is a wave-2 surface. - python -m compileall src tests docs: exit 0 (only the two pre-existing SyntaxWarnings in docs/examples/chapter_02_connection_pool). - ruff check on the 215 changed files: identical to the HEAD baseline for the same file set (one pre-existing E501 in test_insert_on_conflict_clauses.py resolved by the rename). No new finding outside mixins/ddl/constraint.py, whose five F821s are the known draft middle state. * refactor(postgres): migrate constraints onto the two-level branch nodes Core's S3 wave replaced the fat constraint nodes with a production plus a branch: ``ColumnConstraint(branch=...)`` / ``TableConstraint(branch=...)`` name the branch, the branch names the kind, and the ``*ConstraintType`` enums are gone. This commit migrates every construction site, every override and every assertion in the package onto that shape, and absorbs the mechanical part of the B-series draft that was sitting uncommitted in the worktree. Construction sites migrated (src + tests + docs, 96 files): ColumnPrimaryKeyConstraint 147 ColumnDefaultConstraint 39 ColumnNotNullConstraint 131 ColumnForeignKeyConstraint 8 ColumnNullableConstraint 4 ColumnUniqueConstraint 2 ColumnCheckConstraint 1 TablePrimaryKeyConstraint 7 TableForeignKeyConstraint 2 TableUniqueConstraint 1 TableCheckConstraint 1 Call shape: ``ColumnConstraint(dialect, ColumnConstraintType.NOT_NULL)`` becomes ``ColumnConstraint(dialect, branch=ColumnNotNullConstraint(dialect))``. The old keyword-per-field form is gone with it -- ``default_value=`` is now the branch's positional ``value``, ``foreign_key_reference=`` the branch's ``references`` pair, ``check_condition=`` the CHECK branch's ``condition``. GeneratedColumnExpression is deleted from core, so its one call site became ``ColumnGeneratedConstraint``. EXCLUDE. PostgreSQL's grammar is richer than core's ``TableExcludeConstraint`` (one operator per element plus a partial predicate), so the branch is ``PostgresExcludeBranch`` and ``PostgresExcludeConstraint`` becomes the production-level sugar over it -- the same shape core's own ``ForeignKeyConstraint`` has, so the 20-odd call sites keep working unchanged. ``_format_exclude_constraint`` (private, production-and-body in one) became the public ``format_table_exclude_constraint``, which now renders the body only: the ``CONSTRAINT <name>`` prefix and the traits are the production's and are rendered by ``format_table_constraint``, where the grammar puts them. Override points: - ``format_table_constraint`` -- narrows the enforcement trait to CHECK and FOREIGN KEY (measured 0A000 for PRIMARY KEY/UNIQUE) before delegating. - ``format_alter_constraint_action`` -- gates the verb on the measured per-kind boundary (FOREIGN KEY from 18, CHECK from 19). - ``format_column_constraint`` -- refuses the enforcement trait outright. - ``format_add_table_constraint_action`` -- **removed**. Core already renders the production, dispatches to the branch (so EXCLUDE gets its own formatter) and appends NOT VALID; the only PostgreSQL-specific judgement was the enforcement one and it lives in format_table_constraint. This is the narrowing the guide asks for: fewer overrides, not more. - ``_format_constraint_enforcement`` is deliberately NOT overridden. It is shared by the column-level trait renderer and the table-level one, so an override there would have to tell the two families apart by hand; the public column-level seam says which family it is. Behaviour changes (all measured, all previously silent or unimplemented): - ``[NOT] ENFORCED`` on a column constraint now refuses. Measured 42601 ``misplaced NOT ENFORCED clause`` for every kind on 18/19. - A string or enum where a branch class belongs is a TypeError at construction, not a tag that happens to be accepted. - ``AlterConstraint`` takes ``constraint_kind=`` (branch class or spelling) instead of ``constraint_type=``. - EXCLUDE refuses DEFERRABLE/INITIALLY/ENFORCED: core's branch whitelist does not list it and this backend does not widen it, so the request cannot be written. ``PostgresExcludeConstraint(..., enforced=True)`` is a TypeError. Serialization. ``PostgresAlterConstraint`` gains a ``constraint_kind`` view: core's ``AlterConstraint`` declares ``constraint_kind`` but stores it as ``constraint_branch``, and ``get_params()`` -- the single serialization path -- resolves parameters by name, so without the view the kind is dropped and the action cannot be rebuilt. The view yields the *spelling*, because a class object is not encodable by any of the three channels. Wildcard. ``functions/vector_search.py`` took ``Wildcard`` from core, which the split removed, and the package would not import without it. That one import and its one call site move to ``AllColumnsWildcard`` (a select-list position) here; the rest of the split is the next commit. Tests rewritten to the new API: the per-kind enforcement probe tests became probe-version-gate plus formatter-behaviour; the column-level "renders successfully" assertion became a refusal (the draft contradicted its own docstring evidence -- pattern 10 of the ten reconciliation rules says the assertion moves, not the measurement); the EXCLUDE deferral pair guards became construction-refusal tests with the measurement quoted; ``format_default_ constraint`` became ``format_column_default_constraint`` on the branch. Verification: - import smoke over every module in the backend package: 346 imported, 0 failures (was 147 ok / 13 failing on the deleted enums at the previous commit). - targeted constraint suites, exit 0: tests/.../postgres/test_ddl_alter_config.py tests/.../postgres/test_postgres_constraint_capabilities.py tests/.../test_postgres_dialect_security.py tests/.../test_clause_pair_guard.py tests/.../postgres/test_ddl_source_collection.py tests/.../test_expression_roundtrip_all.py (472 passed, 3 skipped) - ruff check on the 102 changed files: one finding below the HEAD baseline for the same file set and no new class of finding. The one new F821 is one more reference to the already-undefined `dialect` in tests/.../extensions/test_pgvector_integration.py, a file with six such findings at HEAD. Not green, and not this repository's to fix: the testsuite's shared feature/ddl contract cannot run, because python-activerecord-testsuite@fix/column-constraint-gates (a57f266) still imports the deleted ``ColumnConstraintType`` in feature/ddl/{fixtures/models,test_derivation,test_derivation_async, test_ddl_source_contract}.py. That is WP-T/S4 work in the testsuite repo. * fix(postgres): converge the constraint switches onto the (self) probe protocol The probes now say what they can answer and the formatters say the rest. **Probes.** ``supports_constraint_enforced()`` and ``supports_alter_constraint_enforced()`` take no argument. A probe answers one question -- "is this capability family present on this server" -- and the kind is not part of that question; a parameterised probe has two call shapes depending on its own signature, which is how the draft ended up with a switch whose kind-less answer (``False`` on 19!) contradicted its kinded one. Both now answer the version gate they were always answering: supports_constraint_enforced() version >= 18 (170011 / 180006 / 190000) supports_alter_constraint_enforced() version >= 18 The per-kind answers did not disappear; they moved to the place that can see the kind, which is the branch class: _require_constraint_enforced(branch) TableCheckConstraint / TableForeignKeyConstraint and version >= 18; anything else refuses _require_enforceable_constraint(branch) TableForeignKeyConstraint from 18, TableCheckConstraint from 19 Both raise TypeError on a non-branch argument (pattern 4: a spelling is a type question) and UnsupportedFeatureError naming the SQL spelling -- ``CHECK NOT ENFORCED``, ``ALTER CONSTRAINT CHECK NOT ENFORCED`` -- with the message the other ten backends use (pattern 7). **Narrowing seams.** No private formatter was added, and the shared private hook was left alone: - column-level refusal -> the public ``format_column_constraint``, before delegation. ``_format_constraint_enforcement`` is shared by the column-level trait renderer and the table-level one, so an override there would have to tell the two families apart by hand; overriding the column seam makes the family the override's own business. - table-level per-kind refusal -> the public ``format_table_constraint``. - ALTER per-kind version -> the public ``format_alter_constraint_action``. - EXCLUDE -> the public ``format_table_exclude_constraint``. **Local declaration discipline** (pattern 5), now enforced rather than asserted in prose. New in test_postgres_constraint_capabilities.py: - ``test_resolved_switch_is_owned_by_this_backend`` -- for every switch this backend relies on, the first class in the dialect's MRO that has the name in ``__dict__`` must be inside this package. A core default that reaches the MRO first would otherwise answer instead of the measured value, silently. - ``test_the_declaring_class_is_a_postgres_mixin`` -- the answering class is a PostgreSQL one, whatever mixin owns it. (Not every switch is on ``PostgresConstraintMixin``: ``supports_drop_constraint_if_exists`` is declared by the ALTER COLUMN modifier, which reaches the MRO first. The guard is therefore package-scoped, which is what the rule actually says.) - ``test_probes_take_no_argument`` -- ``(self)`` and nothing else, checked on the bound method so a defaulted parameter cannot slip back in. - ``test_version_gate_matches_the_measurement`` -- the 17/18/19 boundary. - ``test_docstring_quotes_the_measurement`` -- pattern 6, made mechanical: a switch docstring must contain a SQLSTATE, a server version, or the server's own "accepted" verdict. A docstring that stops quoting its measurement fails here instead of quietly becoming a guess with a name. **Docstrings.** The two "KNOWN DEFECT (core S3, deliberately not fixed here)" paragraphs are gone -- the defect they described was core's ``inspect.signature`` sniffing of a parameterised probe, which core removed. Each probe now points at where its per-kind half lives. The two stray "Core S3 may rename it" notes are reworded against what S3 actually did (moved constraint *nodes*, not the REFERENCES clause and not ``supports_rename_constraint``). **Measured facts re-asserted, not re-derived** (pattern 10). Every SQLSTATE in the new text was already in the draft's evidence and is quoted verbatim: 170011 42601 the clause is not in the grammar, any kind, any statement 180006 0A000 PRIMARY KEY / UNIQUE constraints cannot be marked NOT ENFORCED 180006 42809 cannot alter enforceability ... Only foreign key and check 190000 accepted for both statements, both kinds column level, all versions: 42601 misplaced NOT ENFORCED clause Verification: - tests/.../postgres/test_postgres_constraint_capabilities.py, exit 0. - tests/.../postgres/test_ddl_alter_config.py, exit 0. - tests/.../test_clause_pair_guard.py, exit 0. - ruff on the changed files: clean. * refactor(postgres): split the wildcard by grammar position Core split the single asterisk into two by where it sits in the grammar (spec 4.10): ``AllColumnsWildcard`` for the select list and RETURNING, ``RowCountWildcard`` for ``COUNT``'s argument. The two answer different questions -- ``COUNT(*)`` counts rows, ``t.*`` expands columns -- and only the aggregate's contract admits the second. Call sites migrated by position, not by name: select-list / RETURNING (13) Wildcard -> AllColumnsWildcard COUNT argument (3) Wildcard -> RowCountWildcard examples/ddl/materialized_view.py, examples/query/pagination.py, test_partition_operations.py One qualified form changed shape as well as name: ``Wildcard(dialect, table="users", schema_name="app")`` became ``AllColumnsWildcard(dialect, relation=NamedRelationRef(...))``. The qualification rides the relation object because the name to render is the name the query uses, and that is what the reference holds. Backend-specific: ``PostgreSQLRowCountWildcard``. ``COUNT(t.*)`` counts the non-NULL rows of one relation rather than the rows of the join -- under a LEFT JOIN the two answer differently -- so it is a separate node and not a flag. Core's ``RowCountWildcard`` accepts no relation by contract, so the derivation lives here and the override is ``format_row_count_wildcard``. The override tests the class rather than probing for a missing attribute: core's node has no ``relation`` at all, and asking for one anyway is the reflection rule 9.8 forbids. Anything that is not the qualified form renders the bare asterisk, so core's ``COUNT(*)`` path is untouched. No ``format_wildcard`` override existed here, so none needed renaming. Tests: ``test_schema_namespace.py`` gains the two wildcard contracts -- bare ``COUNT(*)`` stays bare, and ``COUNT(t.*)`` renders the alias the query uses. Verification: - import smoke over every module in the backend package: 347 imported, 0 failures. - tests/.../postgres/test_schema_namespace.py, exit 0. - tests/.../test_partition_explain.py, exit 0. - tests/.../test_expression_roundtrip_all.py, exit 0. - python -m compileall src tests docs: exit 0. - ruff on the changed files: clean. * ci: point testsuite at the matching branch Every install line in .github/workflows/test.yml (four jobs: sync, async, scenarios, lint) pinned the testsuite to its release branch while the core line already pointed at fix/column-constraint-gates. The two have to move together: the release branch's testsuite still builds constraints the way the fat nodes were built, so against this backend's two-level nodes it would fail at import. rhosocial-activerecord-testsuite @ .../python-activerecord-testsuite.git @release/v1.0.0.dev17 -> @fix/column-constraint-gates (4 places) Checked and already correct, so untouched: the core install line is git+https://github.com/rhosocial/python-activerecord.git@fix/column-constraint-gates at all four places. No other workflow installs either dependency by ref (publish.yml and changelog-check.yml have no such line). No test evidence: a workflow-file ref change has nothing to run against. The import smoke over the backend package (347 modules, 0 failures) is what stands behind the branch pairing it points at. * fix(postgres): name the protocol's column-constraint parameter as the mixin does The signature-conformance test compares the protocol's declared parameters with the mixin's, and they had to agree: the protocol said \�xpr\, the override said \constraint\ (which is also what core's own \ ormat_column_constraint\ calls it). Fixed the protocol, not the override, because the override's name matches the one it overrides. Caught by tests/.../test_postgres_protocol_conformance.py, exit 1 before, exit 0 after. * fix(postgres): read the auto-increment flag where the two-level split put it The identity injection in format_column_definition still probed the column-constraint clause for is_auto_increment with getattr -- a 9.8 reflection on a field that now lives on the PRIMARY KEY branch, so the probe always answered False and every auto-increment declaration was silently dropped from CREATE TABLE. Read constraint.branch.is_auto_increment behind an isinstance of ColumnPrimaryKeyConstraint instead. The two DDL tests had migrated the branch but left the flag on the clause (TypeError: unexpected keyword argument), which is how the CI matrix caught it: 11 failed / 9944 passed at b52c8a9, all in test_auto_increment_ddl and test_identity_execution. Targeted: both files exit 0 on the central venv. * docs(postgres): carry the renames into the README and docs * refactor(expression): follow the core renames and the word-form contracts Carry the W3c class renames, the R1 signature alignment and the R2a arithmetic/concatenation renaming through the backend sources, examples and tests. The roundtrip matrix needed the two new core classes registered (ArithmeticOperation, SetOperation) and three imports that the rename left dangling. * refactor(postgres): read declared fields instead of probing with getattr Carry the core S2 discipline (101f71a5) through the 26 remaining probe sites, following the fcc9939 precedent. Three shapes were needed: * direct read (form A) where the field is declared on the node the annotation names: ddl_table.py partition/table_options/temporary, ddl/type.py definition_kind/action_kind (abstract properties on TypeDefinition/TypeAlterAction), ddl/domain.py action_kind (DomainAlterAction), materialized_view.py concurrent (RefreshMaterializedViewStatement) and action_kind (MaterializedViewAlterAction), types/jsonb_enhanced.py mode (JSONDocumentValue declares it, JSONTextValue inherits); * isinstance dispatch where the field exists only on the PostgreSQL subclass: DROP DOMAIN options and CREATE INDEX opclasses / nulls_not_distinct / with_options, DROP TYPE cascade/restrict, CREATE MATERIALIZED VIEW if_not_exists -- the core nodes carry none of these and now say so with a False branch instead of a silent default; * enum-or-spelling (form C, core ddl_comment.py:72 pattern): comment.py object_type, copy.py on_error/log_verbosity now branch on isinstance(x, Enum). type_values/text_search.py judges the to_postgres_string protocol by isinstance(TsQueryNode) instead of hasattr. Migration face: 26 getattr/hasattr probes -> 0 (static gate 'rg (getattr|hasattr)\(\s*(expr|node|clause|item)\b src' is now empty). Behaviour tightening: an object that reaches the ALTER DOMAIN / ALTER TYPE / MATERIALIZED VIEW fall-throughs without the declared action_kind, or a JSONPath node without mode, now fails with AttributeError instead of rendering the class name into the refusal message. No in-repo caller hit the removed branches. Also dropped two dead imports in mixins/ddl/type.py (SchemaObject, shadowed typing.Type) so the touched file is ruff-clean; both were flagged on HEAD already. Tests (central venv, PYTHONPATH=src;tests, all exit 0): postgres/test_index_pg.py, dialect/test_materialized_view.py, postgres/test_json_path_modes.py, postgres/test_textsearch_types.py, postgres/test_ddl_expressions.py, postgres/test_pg19_copy_repack.py, postgres/test_ddl_objects.py, postgres/test_ddl_protocols.py, postgres/test_enum_types.py, dialect/test_materialized_view_docs.py, postgres/test_ddl_alter_config.py, test_partition_operations.py, test_postgres_protocol_conformance.py, postgres/test_ddl_source_collection.py. Pre-existing, unrelated: test_clause_pair_guard.py TestSetOperationWordsAreClosed[union_all*] fails on HEAD 8f56c2c with a substring assertion ('UNION' is contained in 'UNION ALL'); to be repaired with the set-operation follow-up. * refactor(postgres): follow the core's kind-based artifact registry Core ee5f4b55 "give an artifact a kind, not a class path" removed register_expression_class from backend.expression.serialization: an artifact is now a bare {kind, params} node, the kind is computed by BaseExpression.kind(), and deserialization resolves kinds through a registry whitelist the framework populates by walking its own packages (core, sqlite, dummy). The backend-side registration module this repository kept for the old FQN index is therefore dead code, and after that core commit it broke the whole backend import chain (dialect -> mixins -> expression/__init__ -> registry). Removed expression/registry.py and its import-time call from expression/__init__.py, following the shape mysql already converged to (no backend-side registration module; whoever reads an artifact back enters the kinds it names, which is the core commit's own answer to "who registers a backend's kinds"). Same core commit, test side: the two TYPE-definition round-trip tests in postgres/test_ddl_objects.py deserialize PostgreSQL-specific kinds, which the framework whitelist does not carry. The test module now registers exactly what it reads back -- the two definition classes, the composite attribute dataclass and the UNSET sentinel enum -- into the default registry at import time. Also dropped the pre-existing unused Literal import so the touched file is ruff-clean. Behaviour change: PostgreSQL-specific expression classes are no longer entered into the (removed) global deserialization index at import time. Serializing them was never affected - a node carries its own kind. Test evidence (central venv, PYTHONPATH=src;tests): - import smoke `from ...postgres.dialect import PostgresDialect` exit 0 (raised ImportError before the removal); - postgres/test_ddl_objects.py exit 0 (the two round-trip tests failed with "kind ... is not in the kind whitelist" before the registration); - postgres/test_typed_values.py (new, next commit) and the -k "typed or literal" selection both exit 0. * feat(postgres): adopt the typed value nodes with PostgreSQL literal spellings Wire the core TypedValueMixin (473b8e37 + 5ba491d5 + 1549380b) into the dialect: a new PostgresTypedValueMixin rides ahead of it in the base list, so the twelve typed value nodes resolve per kind while exactly the three PostgreSQL spells differently stay backend-owned (spec 4.4 tier 2). Overridden, with the inline (DDL-position) spellings: - format_binary_value: '\x0102'::bytea -- the hex bytea literal, rendered from the bytes the node holds. Hex input needs PostgreSQL 9.0+; every declared version satisfies that. - format_uuid_value: '...'::uuid -- the quoted canonical form plus the cast, the same shape the dialect's UUID_SQL table spells for the UUID constants (mixins/uuid.py). - format_interval_value: INTERVAL '...' with the span decomposed into days / hours / minutes / seconds, every emitted component carrying the sign. Per-component signs are a measured answer, not a preference: live servers parse INTERVAL '-2 days 03:04:05.000006' as minus two days PLUS 03:04:05 (the leading minus does not carry into the time part), so signing each component is the only faithful spelling. A zero span renders INTERVAL '0 seconds'; microseconds render as a six-digit fraction when present. Tier-1 audit: PostgreSQL agrees with the base defaults everywhere -- TRUE / FALSE booleans, numeric text, quoted ISO dates and times, doubled-quote strings -- so no tier-1 method is overridden and no literal logic copied. The data position binds bytes / uuid.UUID / timedelta through the core _bind_value; the psycopg placeholder is left to the dialect base. Measured live 2026-10-17/18 against servers reporting 13.23, 17.11, 18.6 and 19beta4 (Debian): all four accept the three spellings, and a CREATE TABLE whose bytea / uuid / interval columns carry them as DEFAULT values round-trips exactly through INSERT ... DEFAULT VALUES + SELECT on all four (oldest scenario configured as "postgres_9" measured 13.23). New test file postgres/test_typed_values.py: MRO pin (the three methods resolve on the postgres mixin), data-position bindings for every overridden kind plus tier-1 passthrough, exact DDL-position texts for binary (bytes and bytearray), uuid and seven interval shapes, and live sentinel + column-default read-backs on every configured scenario. Test evidence (central venv, PYTHONPATH=src;tests): - postgres/test_typed_values.py: 61 executions exit 0 (17 offline + 4 live across 11 configured scenarios); - feature/backend -k "typed or literal" (5 wave-4-scope collection breakages from core's in-flight commits ignored, see report): exit 0; - ruff clean on every touched file (also fixed the pre-existing missing PostgresObjectNameMixin __all__ entry flagged in mixins/__init__.py). Open items, not this wave: the roundtrip matrix file still imports the retired serialization API and the testsuite expression helper -- wave 4 reconciles it against the core HEAD of that day, mirroring mysql's vendored harness. * fix(postgres): follow the classed function entry points Core 9b559030 classed the aggregates and the scalar string functions, a3978e10 the math family, and c5210245 reserved the generic FunctionCall for user-defined functions. The semantic sites in this repository that the core has classed now build (or delegate to) those classed nodes; the ~295 remaining FunctionCall sites spell extension-specific functions the core has not classed (postgis, hstore, pgcrypto, ll_to_earth, ...) and stay generic on purpose -- FunctionCall is the UDF node, and migrating an unclassed concept would chase the moving target the wave guide forbids. Migrated (functions/math_enhanced.py, 9 of 11 factories): - round_ / trunc -> RoundFunction / TruncateFunction via for_operand, preserving the always-rendered precision operand (default 0 binds a literal, an expression-valued precision passes through -- the previous factory wrapped a Literal in another Literal when handed one) - pow / power -> PowerFunction. pow keeps its name but now writes the canonical POWER: both spellings accepted, measured live 2026-10-18 on 17.11 (POW(2.0,10) and POWER(2.0,10) both answer 1024) - sqrt -> SquareRootFunction, mod -> ModuloFunction, ceil -> CeilFunction, floor -> FloorFunction, avg -> AvgFunction (all render the same text as before) - max_ / min_ stay FunctionCall: GREATEST / LEAST are scalar and unclassed even in the core (its own greatest/least factories also build the generic node); the aggregate Min/MaxFunction are different concepts Spelling override added (mixins/expression.py): - format_truncate_function writes TRUNC. The core node spells the SQL-standard TRUNCATE, which in PostgreSQL is the DDL statement that empties a table; measured live 2026-10-18 on 17.11: TRUNC(3.716, 2) answers 3.71, TRUNCATE(3.716) is refused with UndefinedFunction "function truncate(numeric) does not exist" char_length audit: PostgreSQL uses the core default. The core's format_char_length_function writes LENGTH and deliberately does not read the unit field; live-measured on 17.11, length('hello') = 5 characters against octet_length('hello') = 6 bytes, so PostgreSQL's LENGTH already answers the character question the core spelling assumes -- no override, like oracle / snowflake / bigquery. Leftover: examples/conftest.py described UNION as "using SetOperationStatement"; the class is SetOperation. Also migrated the ROUND site in examples/extensions/earthdistance.py to RoundFunction, and fixed three pre-existing lint findings in the touched files (Table used before its import in the earthdistance example, two overlong lines in the math test). Kept as FunctionCall, with the reason where it is subtle: - functions/bit_string.py length / bit_length / octet_length: PostgreSQL's bit-string overloads, not the text semantics the core classes carry - functions/geometric.py length: the geometric overload - examples/query/window.py ROW_NUMBER / SUM with window_spec: the classed nodes take no window clause; the core has not classed the windowed form Test evidence (central venv, PYTHONPATH=src;tests): - postgres/test_postgres_math_enhanced_functions.py (POW assertions updated to the documented POWER spelling) + test_dialect_function_support.py + examples_compile_test.py: 61 executions exit 0 - ruff clean on every touched file * refactor(postgres): follow the literal architecture the core settled Core removed TypedValueMixin and the twelve *Value classes, leaving one Literal(dialect, value, inline) node whose inline text goes through SQLDialectBase.format_literal -- three tiers, of which the second is exactly the types a backend spells for itself. PostgreSQL has three: binary, UUID and INTERVAL. The 2026-10-17/18 live measurements behind them (13.23 / 17.11 / 18.6 / 19beta4, CREATE TABLE DEFAULT read back byte-exactly, the per-component interval sign) are carried over unchanged -- an architecture change does not invalidate a measured answer. Literal mixin - mixins/typed_value.py -> mixins/literal.py: PostgresLiteralValueMixin overrides format_literal (bytes -> '\x0102'::bytea, UUID -> '...'::uuid, timedelta -> INTERVAL '...'); everything else defers to super(). - First position in the dialect base list, ahead of SQLDialectBase: a mixin placed after the class it overrides never wins the lookup. - test_typed_values.py -> test_literal_values.py, rewritten around the inline flag. Binding and inline assertions, the tier-1 audit, the base refusal and the live read-backs are all kept. Partition bounds - _inline_partition_expression is gone. Whether a value inlines is the caller's per-node decision, so the pass that copied a bound tree and swapped every Literal for the DDL class is: a bound renders as composed, a literal left binding is refused by name. Three tests that relied on the old lift now say inline=True; one new test pins the refusal. Action renames (guide 10.3, same batch) - AddColumn/DropColumn/AddTableConstraint/DropTableConstraint/AddIndex/ AlterConstraint/ValidateConstraint/AlterColumn/RenameColumn -> *Action, in src, tests and the two schema_diff examples. - The suffix lists in the roundtrip matrix lost the entries that named the removed classes; a suffix matching nothing registers nothing. - examples/ddl/alter_table.py was missing the Table import its own code uses. Plain-JSON artifacts (guide 12.1) - register_dataclass / register_enum are gone from the core registry, and an artifact carries no class path any more, so the two PostgreSQL parameters that were host objects had to become values: * PostgresCompositeTypeAttribute gains as_params/from_params and PostgresCompositeTypeDefinition overrides get_params to emit the attribute as a mapping. The constructor takes either form and stores the dataclass (entry wide, storage normalized). * _UnsetValue is now (str, Enum) with __str__ = str.__str__, so the "clause not supplied" sentinel travels as the text "UNSET"; the constructor normalises it back, without which every deserialized base TYPE would claim a DEFAULT it never declared. Tests: 1044 passed, exit 0, over the twelve backend feature files this touches. ruff check clean on every changed file except two pre-existing E501s (mixins/ddl/column.py:126, test_ddl_expressions.py:286), both present at HEAD and untouched here. * refactor(postgres): audit the kinds this backend overrides and owns Sweep the backend's expression package against the org rule that a kind core has already issued may only be overridden in rendering, never redeclared as a node class, and that a kind core does not have is the backend's own -- named with the backend identifier in front. Found and fixed, per the lineage script (core 357 kinds; backend 167 -> 161 custom kinds, 0 overrides -- the count that changes here is the one made of duplicate nodes): * ILIKE. ``expression/ilike.py`` declared ``ILikePredicate`` under the same ``format_ilike_predicate`` entry core's node declares, so one concept had two classes and the artifact could not say which it meant. The module is gone; ``PostgresILIKEMixin.format_ilike_predicate`` is the whole PostgreSQL side, and the two test modules import core's node. * ALTER/VALIDATE CONSTRAINT. ``ddl/constraint.py`` exported one class under four names each -- zero call sites, no old name to be compatible with -- and the VALIDATE one added nothing at all. ``PostgresValidateConstraint`` is deleted (core's ``ValidateConstraintAction`` is what callers build); the ALTER one keeps its one real addition, ``constraint_kind`` as a readable property that makes ``get_params()`` resolve the parameter core stores under another name, and is renamed ``PostgresAlterConstraintAction`` so the class carries the suffix its formatter already had. * ENUM type fragments. ``CreateEnumTypeExpression`` / ``DropEnumTypeExpression`` / ``AlterEnumAddValueExpression`` were ``pass`` subclasses of the ``Postgres*`` classes -- same behaviour, a second kind, no backend prefix. Deleted; the ``Postgres*`` classes are the only spelling. ``EnumTypeNameExpression`` / ``EnumValuesExpression`` are real nodes but their kinds had no backend prefix and their formatter names did not match them; renamed ``PostgresEnumTypeNameExpression`` / ``PostgresEnumValuesExpression`` with ``format_postgres_enum_*`` entries. * PARTITION BY. ``PostgresPartitionClause`` was an empty subclass of core's ``PartitionClause``: no field, no formatter of its own, one more kind for one concept. Removed; PostgreSQL renders core's clause. * Compound-word kind. ``PostgreSQLRowCountWildcard`` derived ``postgre_sql_row_count_wildcard`` -- ``PostgreSQL`` splits into two words under the class-name rule, while every other class here says ``Postgres``. Renamed ``PostgresRowCountWildcard`` (kind ``postgres_row_count_wildcard``). Audited and left alone: every ``Postgres*`` node that subclasses the core node of its concept and adds a typed field (``PostgresAlterColumn.using``, ``PostgresExcludeBranch.elements``/``where``, the type definitions, the domain actions) -- those are derived nodes with a formatter of their own, not redeclarations. The dialect MRO was read end to end: no PostgreSQL mixin sits behind a core class that would shadow it. Test evidence: ``pytest`` on test_expression_signatures.py, test_ilike_support.py, test_enum_types.py, test_ddl_alter_config.py, test_ddl_expressions.py, test_ddl_source_collection.py, test_schema_namespace.py -> exit 0. ruff on every touched file -> clean. * test(postgres): reconcile the roundtrip matrix with the plain-JSON artifact Reconcile the matrix against core at 550ae740, where an artifact is a bare {kind, params} node: no envelope, no marker keys, values as JSON scalars, and the kind is the class name derived (``Literal`` the one exception, naming its fourteen value kinds by what they hold). The harness is now this repository's own (``expression_roundtrip_harness.py``), vendored when testsuite deleted the shared module; its comparison learns that a ``(str, Enum)`` member and the word it carries are the same recorded value, because JSON has no enum and a constructor may legitimately keep the word. Matrix changes: * ``values.*`` rows are gone -- there is no class per value kind. In their place ``test_value_kind_plane`` walks all fourteen kinds: wire name, JSON payload type (NaN/+-Inf as text, Decimal and timedelta as text, bytes as base64, list vs tuple preserved, everything else a plain scalar) and the round-trip through all three channels. The set is pinned in both directions against ``LiteralKind``, and a ``float_value`` payload that is not finite is asserted to be refused by the value layer rather than rendered. * The removed K1 classes leave no rows: ``PostgresPartitionClause``, ``PostgresValidateConstraint``, the three enum compat subclasses and the six constraint aliases. * Constructors added for classes the generic guess could not build for real reasons (a closed word set, a field name, an inline DDL condition, a value rather than text, a declared-result annotation wrapping a column), and for ``CustomType`` (its ``raw`` is keyword-only), so it leaves UNCONSTRUCTIBLE. * ``_CODEC_GAPS`` / ``_CODEC_RENDER_GAPS`` pin what core's codec cannot carry or cannot read back, per class and per channel with the exception type: the XML codec writes a ``(str, Enum)`` with ``Enum.__str__`` (the member's *name*), a dataclass parameter has no encoding at all (``XMLAttribute``, ``XMLForestItem``, ``XMLTableColumn``, ``ReferentialAction``, ``TriggerEvent``, ``ViewOptions``, ``GraphColumn``, ``GeneratedColumnType``), ``CreateTableCloneMode`` is a plain Enum, and the two XML nodes' formatters read ``.value`` off a decoded word. Nothing else may lose a parameter, and each entry is exact in both directions. Backend defects the matrix surfaced, fixed here: * ``PostgresCollationMixin`` read ``expr.collation_options``; core's node declares ``schema``, so every COLLATE raised ``AttributeError``. * ``format_alter_database_statement`` compared against ``AlterDatabaseAction.CONNECTION_LIMIT`` / ``.TABLESPACE``, which core does not declare -- every ALTER DATABASE died before rendering. It now recognises PostgreSQL's two extra actions by word, resolves a decoded word back to the core member, delegates the rest to core's formatter explicitly (``super()`` reached the protocol stub and answered ``None``), and refuses an unknown action. * ``mode is LoggingMode.LOGGED`` answered "not LOGGED" for a round-tripped word and rendered the opposite setting; it compares and spells the word now, and refuses an unknown one. * ``PostgresAlterTableRlsExpression`` read ``expr.mode.value``, which is gone after a decode. * ``hstore_literal``/``ltree_literal``/``xid8_literal`` chained ``.cast()`` onto a ``Literal``; casting is a ``SQLValue`` capability and ``Literal`` is the value node, so they build a ``CastExpression`` over it. * The four value wrappers declared ``family``, stored it as ``_family``, and no code ever read it -- removed (rules §4.4: a parameter that changes nothing). * Every PostgreSQL-owned enum is now ``(str, Enum)`` with ``__str__ = str.__str__`` (§11.3), so the word is what str() and the artifact carry. Test evidence: the full matrix plus the domain round-trip file -> exit 0 (``531 constructible, 2 pinned-unconstructible; 471 rendered, 37 unsupported, 20 non-render, 3 unmodelled``). ruff clean on every touched file. * refactor(postgres): follow the rules the org settled for kinds Kind hygiene (expression-kinds.md §三, expression-dialect §3, §4.4, §11.2): * A backend's own kind is named for the backend. Renamed ``NetworkValueExpression`` / ``RangeValueExpression`` / ``HstoreValueExpression`` / ``LtreeValueExpression`` -> ``Postgres*ValueExpression``, ``PartitionValue`` -> ``PostgresPartitionValue``, ``CreateMultirangeTypeExpression`` / ``MultirangeAggFunctionExpression`` -> ``Postgres*``, ``MaterializedViewAlterAction`` -> ``PostgresMaterializedViewAlterAction``, and the two private helpers ``_LegacySqlExpression`` / ``_UnsupportedDomainAction`` -> ``_PostgresLegacySqlExpression`` / ``_PostgresUnsupportedDomainAction``. Every affected ``format_method`` follows its kind, so class name, kind and entry are one identity. * ``PostgresAlterColumn`` -> ``PostgresAlterColumnAction``: the action carries the suffix its formatter (``format_alter_column_action``) and its base (``AlterColumnAction``) already had. * The four value wrappers declared ``family``, stored ``_family``, and nothing read it. Removed -- a parameter that changes nothing is not a parameter. Kind hygiene is now a test (``dialect/test_kind_hygiene.py``): no backend class may claim a kind core issues; a backend-owned kind must be ``postgres_*``; a declared node's formatter must be ``format_<kind>`` (with an explicit, two-way-checked table for the families that share one entry); and every enum in the package must be ``(str, Enum)`` whose ``str()`` is its value. Test discipline (expression-dialect §12, §12.1): * The live-connection half of the literal work moved out of ``test_literal_values.py`` into ``test_literal_values_integration.py``, on the scenario-parameterised ``postgres_backend`` fixture. What stays is construct-and-render only, with the measured facts (and where to re-measure them) recorded in the docstring. ``conftest.py`` in the expression directory makes ``-n auto --dist=loadgroup`` the default there. Serialization obligations (guide §13): * §13.1: ``format_binary_operator`` spells the operator through ``.symbol`` when it is a ``ComparisonOperator`` -- ``expr.op`` is now the word (``EQ``), and the character is what SQL wants. A plain string operator (pg_trgm, range) is used as it is. * §13.4: audit of every ``*_need_quote`` pass-through in this backend; all are real bools, no change needed. Test evidence: the kind-hygiene file, the literal files (expression and integration), the matrix, ``test_expression_roundtrip_all``, ``test_postgres_dialect_security``, ``test_extension_type_operations``, ``test_ddl_expressions``, ``test_ddl_alter_config``, ``test_materialized_view``, ``test_alter_table_if_exists`` -> exit 0. ruff clean. * test(postgres): move live-connection checks out of expression tests The expression directory is construct-and-render only (expression-dialect §12): no connection, no schema, no file. Two things there still opened one, and one configuration still left parallelism to whoever remembered the flag. * ``test_literal_values.py`` keeps the wiring, the data-position bindings and the inline spellings, and nothing else. Its ``TestLiveLiteralRoundTrip`` -- the sentinel, the CREATE TABLE with the rendered DEFAULTs, the read-back through ``INSERT ... DEFAULT VALUES`` -- moved to ``test_literal_values_integration.py`` beside it, where the scenario-parameterised ``postgres_backend`` fixture already lives. The measured interval facts stay in the docstring that recorded them, with a pointer to where a re-measurement goes; the numbers did not change and are not restated in two places. * ``tests/.../backend/expression/conftest.py`` makes ``-n auto`` and ``--dist=loadgroup`` this directory's default, and ``pyproject.toml`` pins ``--dist=loadgroup`` for the suite: a case runs on its own only when it carries an explicit ``xdist_group``, and the reason for that marker is written where it is used. ``-n 0`` / ``-p no:xdist`` still opt out. * ``xdist_group`` is declared as a marker rather than left to the plugin, so a typo in its name fails instead of being ignored. Also in this commit, for the same rule set: the backend's transaction mixin and tests follow core's ``with_*`` setters (``expr.isolation_level()`` -> ``expr.with_isolation_level()`` and the three siblings), and the parse-contract sweep reads ``parse_roundtrip_failures`` from this repository's vendored harness now that the shared testsuite module is gone. Test evidence: the expression directory (parallel default in effect), ``test_literal_values.py``, ``test_literal_values_integration.py`` (live; skipped where no scenario server is configured), ``test_expressions_transaction.py``, ``test_transaction_advanced_features.py``, ``test_postgres_type_protocol.py``, ``test_ddl_alter_config.py``, ``test_object_kind_guards.py`` -> exit 0. Live capture is out of this change's reach (no scenario server on this host), and that is stated rather than implied by a green run. * fix(postgres): write the locking clause from its phrase, not its word ``format_for_update_clause`` interpolated ``LockStrength.UPDATE`` straight into the SQL, and after core split a member's *word* (what the artifact carries) from the SQL it spells, that renders a bare ``update`` -- a clause PostgreSQL rejects. ``LockStrength.phrase`` is where core keeps the spelling now, the same way ``SortDirection.keyword`` holds the ordering keyword. Guide §13.2's rule, applied to the one place this backend still read a wire word as SQL. Test evidence: ``test_row_level_locks.py`` -> exit 0 (one skip, a version-gated case that predates this change). * fix(postgres): read the word an artifact carries, not the member it used to be The same defect the ALTER DATABASE and RLS formatters had, in three more places: a value that was a ``(str, Enum)`` member at construction comes back as its *word* after a round-trip, and the formatter read ``.value`` off it. * ``format_create_table_statement`` interpolated ``col_def.storage.value`` for ``STORAGE``. * ``format_create_policy_statement`` interpolated ``cmd.value`` for the ``FOR <command>`` clause. * ``format_create_domain_statement`` interpolated ``expr.nullability.value`` and passed the member to ``supports_domain_nullability``, whose set membership would refuse the decoded word. The word is resolved back to its member once, by name, so the probe keeps answering and an unknown word is refused explicitly. All three now interpolate the member (its ``__str__`` is its word, §11.3) and resolve for capability checks. Test evidence: ``test_domain_expression_roundtrip.py``, ``test_policy.py``, ``test_ddl_expressions.py``, ``test_ddl_source_collection.py``, ``test_expression_roundtrip_all.py`` -> exit 0. ruff clean. * test(postgres): record the matrix reconciliation baseline The matrix file's header now states what it is reconciled against (core ``550ae740``, the plain-``{kind, params}`` artifact), what the value-kind plane assertion exists for, and the census it currently reports. That is the one place a reader looks when the numbers move, so the numbers and the baseline belong in it rather than only in a commit message. Evidence: the matrix plus the domain round-trip file -> exit 0 (533 registered, 531 constructible, 471 rendered, 37 unsupported, 20 non-render, 3 unmodelled). ruff clean. * docs(postgres): build a cast over the literal in the extension examples The examples still chained ``Literal(...).cast(T)``. ``Literal`` is the value node -- it answers "bind or inline" and nothing else -- so the cast is its own node, built from the literal. The examples now show the shape the library actually offers, which is the one a reader copies. No behaviour is asserted by these files (they are illustrative fragments and reference names that only exist inside an ``if installed`` block); they are parsed, not executed, and that is unchanged. * test(postgres): use the edge-table definition node the class is actually called ``EdgeTableDefinition`` is the CREATE PROPERTY GRAPH definition; ``EdgeTable`` is the catalogue object it names. The test imported the catalogue object under the definition's name and constructed it with definition arguments, which only passed while both were spelled similarly. The class was renamed for exactly this reason, and the test follows. Evidence: ``test_property_graph_query_format.py`` -> exit 0. * test(postgres): walk core`s package instead of reading the shared registry The kind-hygiene check compared this backend`s kinds against ``default_registry().node_kinds()``. That registry is process-global and the matrix registers this backend`s classes into it, so the comparison answered differently depending on which module imported first -- the test failed when the matrix ran before it, and passed alone. Core`s own package is the stable source, and walking it is the same mechanism the matrix already uses to decide what counts as a kind. Evidence: the matrix + domain round-trip + kind hygiene + the expression directory in one run (matrix first) -> exit 0. * fix(postgres): declare the domain formatters this backend serves PostgresDomainMixin renders two nodes the core kinds do not carry: the legacy sql= argument and the ALTER DOMAIN action words PostgreSQL spells. Both reach the dialect through format_method, so each is a member the mixin serves -- and a protocol is the declaration of that surface (§7.1.1). Declared them on PostgresDomainSupport; the reverse-coverage conformance test is now clean. Test evidence: test_postgres_protocol_conformance.py -> 559 passed (was 558 passed, 1 failed). * fix(postgres): follow core's EdgeTable rename in the PGQ scenarios Core split one name into two contracts: the relation object is EdgeTable in objects.relation, and the DDL node that declares an edge in CREATE PROPERTY GRAPH is EdgeTableDefinition in expression.graph (kind graph.edge_table_definition). The scenarios file still imported EdgeTable from the expression package, which no longer exports that name -- so the module failed to import at all, and the four DDL sites that carry source/destination keys would have been the definition node and the eleven GraphEdge sites the relation object. Split them accordingly: EdgeTableDefinition where key lists are passed, and EdgeTableObject for GraphEdge, which is what core's format_graph_edge now type-checks (GraphEdge.table must be an EdgeTableObject). Test evidence: module imports and compiles; the scenarios are capability-gated and inert without a live SQL/PGQ server -- the live-server assertions are recorded as 'to be verified in CI'. * fix(postgres): follow core's EdgeTable rename in the harness and matrix Core split one name into two contracts: the relation object is objects.relation.EdgeTable, and the CREATE PROPERTY GRAPH node is expression.graph.EdgeTableDefinition, kind graph.edge_table_definition. The vendored harness still registered the constructor under graph.EdgeTable and built the removed class, so the property-graph row could neither be constructed nor matched against the gap table; the registration now names the kind core issues and the declared-gap row is what the matrix expects. Verified: test_expression_roundtrip_all.py -> 1072 passed, 4 skipped (the pins skip by design); ruff clean on both files. * fix(postgres): build the matview list with the with_ prefix core renamed Core turned include_system from a method on the introspection expression into a declared field with a with_include_system builder, the same way schema became a field with with_schema. The introspector still chained .include_system(...) as a call -- so every materialized-view listing that asked for system objects raised 'bool' object is not callable, silently if the caller swallowed it. Routed both chained sites through the builder. Test evidence: tests/feature/backend/dialect -> 337 passed (was 336 passed, 1 failed), including test_list_query_can_include_system_objects. Ruff on the touched files matches the HEAD baseline (4 pre-existing B009, unchanged). * fix(postgres): compose collate over a Literal with the factory, not a chain The value nodes carry no operation surface any more (rules 4.3: a literal is a value and nothing else, and a composition such as an explicit COLLATE is its own node), so Literal(...).collate(...) is an AttributeError -- the chain lives on SQLValue, which a Column is and a Literal is not. The literal test now composes the CollateFunction through the collate() factory over the literal node, which renders the same SQL and the same binding: ('POSIX', ('Alice',)). The Column tests are untouched -- a Column is an SQLValue and keeps the chain. Test evidence: the five SQL-generation bodies of TestPostgresCollationExpression run green directly against a PostgresDialect(version=(15, 0, 0)); the collate-executes leg is recorded as to be verified in CI. Ruff clean on the changed file. * fix(postgres): cast a Literal with the cast factory, not a chain A Literal is a value node and carries no operation surface of its own (rules 4.3), so Literal(...).cast(t) is an AttributeError; the cast of a literal is composed with the cast() factory over the literal node. Column keeps its chain -- a Column is an SQLValue. Same rendered SQL, same bindings. Test evidence: test_type_casting.py -> 31 passed (was 30 passed, 2 failed across the two Literal casts), ruff clean. * fix(postgres): cast a Literal with the cast factory, not a chain A Literal is a value node and carries no operation surface of its own (rules 4.3), so Literal(...).cast(t) is an AttributeError. The three sites that built a value for a VALUES cell and a chained conversion now go through the cast() factory, which renders the identical SQL and the identical bindings. The chained-cast loop starts at a Literal and lands on a CastExpression, so routing every step through the factory keeps one shape for the whole chain. Test evidence: test_cast_integration.py -> 157 passed, 8 skipped (was 85 failed across the three ConstructionError groups), ruff clean. * fix(postgres): build the extension-test values through the cast factory and the operator nodes Literal carries no operation surface (rules 4.3), so the extension tests' Literal(...).cast(t) chains go through the cast() factory -- rendering and bindings unchanged. The PostgreSQL containment/overlap operators (@>, &&, <@) are operator symbols, not one of the six comparison words the artifact may carry, so they build BinaryExpression (the shape the sibling unit tests in test_cube.py already assert); the ltree AND combination builds LogicalPredicate. Local evidence: extension area --collect-only exit 0; ruff delta zero semantic findings (the remaining two kinds shift line numbers only and exist on HEAD); the live-DB legs stay for CI. * fix(postgres): give the aliased literals a typed path and the remaining casts the factory A Literal is neither aliasable nor castable (rules 4.3): aliasing belongs to what produces a named result, which is SQLValue, so the CTE depth counters go through cast(dialect, value, IntegerType) with the alias on the cast; the remaining Literal(...).cast(t) sites (extension type execution, plus one constructed through pg.dialect) go through the same factory. Rendering is a named integer either way. Local evidence: literal-chain scan is zero across src/tests/examples; the three touched test files plus the example collect clean (exit 0); the live-DB legs stay for CI. ruff clean on the touched files. * fix(postgres): collect every expression parameter at construction, and name the operators again Four defects the second CI run exposed, all of the same family -- an expression that does not have its parameters, or has them under a name that no longer exists: - cube/intarray @> (and the remaining && / <@) build BinaryExpression: they are operator symbols, not one of the six comparison words the artifact may carry, and the unit tests beside them already asserted the binary shape. - the materialized-view introspectors called .schema() and .with_include_system() as methods on nodes whose schema/include_system are constructor fields (core's fluent setters retired with the word-form split), so every materialized-view query raised TypeError: NoneType is not callable. They now pass the fields at construction. - the BEGIN path chained with_isolation_level()/read_only()/with_deferrable() onto a fresh BeginTransactionStatement the same way. READ WRITE is PostgreSQL's default and was never rendered, so the mode field stays unset unless READ ONLY is asked for -- the rendered bytes are unchanged. - the remaining Literal(...).cast(t) sites (a multiline multirange pair) go through the cast factory, and the cosine-similarity assertion reads ArithmeticOperation, which is what a word-form arithmetic node is. - the introspection default-schema lookup read config fields through getattr with a constant name; they are read directly now. Local evidence: pgvector dialect tests exit 0; the touched files collect clean; begin/mv render SQL compared byte-for-byte against the intended shapes; ruff clean on the touched set; the live-DB legs stay for CI. * refactor(postgres): build the introspection catalog queries from their node parameters Six formatters grew their pg_catalog queries with ``sql +=``: the base query was an f-string, and every conditional branch appended another fragment to a string that was already half built, so no single place showed the whole query and the WHEN/ELSE chains for a parameter were scattered across the appends. Each formatter now collects the conditions its node's parameters ask for and joins them into the WHERE tail in one place, which is how the MySQL backend already spells the same family (its ``format_table_list_query`` and friends collect conditions and join them once). Migrated formatters, each driven only by the parameters the core introspection node already collects: - format_table_list_query: include_system and table_type collect conditions - format_column_info_query: include_hidden collects a condition - format_view_list_query: include_system collects a condition - format_materialized_view_list_query: include_system collects a condition - format_trigger_list_query / format_trigger_info_query: the optional table_name collects a condition and its bind parameter Local evidence: the rendered SQL was compared byte-for-byte over 22 shapes (every include_views / include_system / table_type / include_hidden / table_name branch of the whole introspection family, plus the materialized view queries); all 22 are identical, parameters included. The one oddity kept on purpose is format_column_info_query repeating ``NOT a.attisdropped`` when include_hidden is false: the base query already excludes dropped columns, the repeat is what this query has always rendered, and it is now a named fragment with a comment saying so rather than an anonymous append. ruff clean on the touched file; the dialect-level materialized-view introspection tests and the whole DB-free introspection suite are green; the live-DB legs stay for CI. * refactor(postgres): build the database-size query through its expression node Both storage-info paths spelled the statement at the call site -- ``sql = f"SELECT pg_database_size({self.dialect.p()}) as size_bytes"`` in get_storage_info, sync and async -- so the introspector assembled SQL text instead of asking a node for it. Core issues no node for this query, so this backend declares two: - PostgresDatabaseSizeFunction (kind postgres_database_size_function) owns the ``pg_database_size(name)`` call and carries the database name as a bind parameter. The generic FunctionCall is not used for it: core reserves that node for user-defined functions, and a standard function the server ships is a named class an artifact can resolve. - PostgresDatabaseSizeQueryExpression (kind postgres_database_size_query_expression) owns the row shape -- the SELECT and the size_bytes alias callers read -- and renders the call through the function node, so the spelling lives in one place and the row shape in another. get_storage_info now builds the node with the database name and executes what it renders; the SQL text and its parameter are exactly what the f-string produced ("SELECT pg_database_size(%s) as size_bytes", (db_name,)), compared byte-for-byte. Local evidence: the new dialect test file pins the function's call text and the query's full statement, both plain and with a quote in the name; the status introspector suite, the kind-hygiene and expression-namespace guards, and the full expression roundtrip matrix (which discovers both new classes and round-trips them through dict/JSON/XML) are green; ruff clean on the touched set. The live-DB leg of get_storage_info stays for CI. * refactor(postgres): let the status, enum and constraint text belong to expression nodes Three catalog queries the execution layer used to spell for itself -- the status introspector held the pg_settings/pg_stat_activity/pg_database/pg_namespace/pg_roles text as inline literals and f-strings at the call site, the enum manager assembled its WHERE clauses by string concatenation, and the transaction manager wrote SET CONSTRAINTS with an f-string that interpolated the constraint name raw. A SQL string is a formatting artefact: the node collects the parameters, the dialect formatter writes the text, and the executing layer only constructs and runs. New nodes (kind from the class name, formatter in PostgresIntrospectionCapabilityMixin): PostgresSettingsQuery, PostgresActivityCountQuery, PostgresShowStatement, PostgresDatabaseListQuery, PostgresSchemaStatsQuery, PostgresCurrentDatabaseQuery, PostgresRoleListQuery, plus PostgresEnumTypeExistsQuery/PostgresEnumTypeValuesQuery and PostgresSetConstraintsStatement. The settings query's name list and the enum lookups travel as bind values now; the SET CONSTRAINTS name goes through the dialect's identifier quoting, so a name carrying a quote can no longer break the statement or escape it; PostgresShowStatement validates its parameter at construction. formatter-type imports land under TYPE_CHECKING, and format_postgres_set_constraints_statement is declared in PostgresTransactionSupport so the reverse-coverage conformance test passes. Rendered SQL is otherwise unchanged; the live-DB legs are covered by CI. Local: full offline area (dialect/expression/types/transaction/conformance/matrix) exit 0; new test_set_constraints_statement.py exit 0; ruff clean on the touched set. * docs(postgres): spell the partition example through its node, and correct the node's stale docstring The partition example still assembled CREATE TABLE ... PARTITION OF with an f-string at the call site. It now builds PostgresCreatePartitionExpression, whose formatter produces the standard FOR VALUES FROM/TO spelling the example was hand-writing. The node's own docstring example had drifted from what the formatter renders (a RANGE (...) shape no server accepts) and is corrected to the real text. Local: collection exit 0; the touched files accept ruff; the example's live-DB legs are covered by CI. * fix(postgres): pass the LIKE operator as its enum, not the SQL keyword The LIKE family's operator became an …
1 parent 646a251 commit b6729ca

407 files changed

Lines changed: 18356 additions & 8100 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/test.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
- name: Install activerecord and testsuite
7070
run: |
7171
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
72-
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
72+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@fix/column-constraint-gates"
7373
7474
- name: Create postgres scenario config
7575
run: |
@@ -284,7 +284,7 @@ jobs:
284284
- name: Install activerecord and testsuite
285285
run: |
286286
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
287-
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
287+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@fix/column-constraint-gates"
288288
289289
- name: Create postgres scenario config
290290
run: |
@@ -396,7 +396,7 @@ jobs:
396396
- name: Install activerecord and testsuite
397397
run: |
398398
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
399-
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
399+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@fix/column-constraint-gates"
400400
401401
- name: Create postgres scenario config
402402
run: |
@@ -459,12 +459,12 @@ jobs:
459459
fail_ci_if_error: false
460460

461461
test-postgres-19:
462-
name: Python 3.14 / postgres 19beta4
462+
name: Python 3.14 / postgres 19rc1
463463
runs-on: ubuntu-latest
464464

465465
services:
466466
postgres:
467-
image: postgres:19beta4
467+
image: postgres:19rc1
468468
env:
469469
POSTGRES_DB: test_db
470470
POSTGRES_USER: root
@@ -494,7 +494,7 @@ jobs:
494494
- name: Install activerecord and testsuite
495495
run: |
496496
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30"
497-
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17"
497+
pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@fix/column-constraint-gates"
498498
499499
- name: Create postgres scenario config
500500
run: |

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,8 @@ htmlcov/
182182
# Claude Code temporary files (local only)
183183
.claude/tmp/
184184
.claude/plan*/
185+
# Worktrees for feature/* and fix/* branches. Never tracked: a `git add -A`
186+
# from the repo root would commit each branch's whole toolchain -- the venv, the
187+
# build directory and the test artifacts -- and the directory disappears when the
188+
# branch merges, so the history would keep a second copy of the source tree.
189+
.worktree/

‎README.md‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ _979088a_
2424
- **Python**: `>=3.8`
2525
- **Core**: `rhosocial-activerecord>=1.0.0.dev0,<2.0.0`
2626
- **Driver**: `psycopg[binary]>=3.2.13`
27-
- **CI-tested server versions**: 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19beta4
27+
- **CI-tested server versions**: 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19rc1
2828
- **Published**: PyPI `1.0.0.dev16`
2929

3030
## Installation
@@ -42,11 +42,11 @@ from uuid import UUID, uuid4
4242
from rhosocial.activerecord.base import FieldProxy
4343
from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend
4444
from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig
45-
from rhosocial.activerecord.backend.expression import CreateTableExpression
45+
from rhosocial.activerecord.backend.expression import CreateTableStatement
4646
from rhosocial.activerecord.backend.expression.statements import (
4747
ColumnConstraint,
48-
ColumnConstraintType,
4948
ColumnDefinition,
49+
ColumnPrimaryKeyConstraint,
5050
)
5151
from rhosocial.activerecord.backend.expression.types import (
5252
ArrayType,
@@ -81,13 +81,15 @@ Article.configure(
8181
)
8282

8383
dialect = Article.__backend__.dialect
84-
Article.__backend__.execute(*CreateTableExpression(
84+
Article.__backend__.execute(*CreateTableStatement(
8585
dialect,
8686
"articles",
8787
columns=[
8888
ColumnDefinition(
8989
dialect, "id", UUIDType(dialect),
90-
constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)],
90+
constraints=[ColumnConstraint(
91+
dialect, branch=ColumnPrimaryKeyConstraint(dialect),
92+
)],
9193
),
9294
ColumnDefinition(dialect, "title", VarCharType(dialect, 200)),
9395
ColumnDefinition(dialect, "tags", ArrayType(dialect, TextType(dialect))),
@@ -179,7 +181,7 @@ operations.
179181
- **`RETURNING` is version-gated.** The basic clause works broadly, but
180182
`returning_old_new` and `update_returning_old` need PostgreSQL 18.
181183
- Field annotations do not drive DDL. `list[str]`, `dict` and `UUID` columns
182-
are described explicitly in the `CreateTableExpression` above.
184+
are described explicitly in the `CreateTableStatement` above.
183185

184186
## Testing
185187

‎changelog.d/63.added.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`extensions NAME` and `views NAME` on the introspect CLI, whose type list now derives from `_TYPE_CAPABILITIES`. Typed value nodes with PostgreSQL literal spellings, and the domain formatters this backend serves are declared.

‎changelog.d/63.changed.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The catalogue and status questions are built from expression nodes, materialized views, sequences and extensions are statement factories, and the CLI adapts once at the entry rather than inside selected branches. A `NAME` a type cannot use is an error rather than an ignored argument.

‎changelog.d/63.fixed.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`table_count` comes from the same database-info row rather than a second reading, and the catalogue reports what it answered: materialized, foreign and partitioned table kinds, column dimensions, and extension facts.

‎changelog.d/63.internal.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The expression round-trip harness is vendored beside the matrix, live-connection checks moved out of the expression tests, and CI installs core from `release/v1.0.0.dev30` now that core #127 has merged.

‎docs/en_US/ddl/README.md‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ The PostgreSQL backend supports the same type-safe DDL expressions as the core l
66

77
| Operation | PostgreSQL Support | Notes |
88
|----------|-------------------|-------|
9-
| `CreateTableExpression` | ✅ Full | PRIMARY KEY, NOT NULL, UNIQUE, etc. |
10-
| `DropTableExpression` | ✅ Full | IF EXISTS, CASCADE, RESTRICT |
11-
| `AlterTableExpression` | ✅ Full | ADD/DROP COLUMN, ALTER COLUMN |
12-
| `CreateIndexExpression` | ✅ Full | Index types (BTREE, HASH, GIN, GiST, BRIN) |
13-
| `DropIndexExpression` | ✅ Full | |
14-
| `CreateViewExpression` | ✅ Full | |
15-
| `DropViewExpression` | ✅ Full | |
9+
| `CreateTableStatement` | ✅ Full | PRIMARY KEY, NOT NULL, UNIQUE, etc. |
10+
| `DropTableStatement` | ✅ Full | IF EXISTS, CASCADE, RESTRICT |
11+
| `AlterTableStatement` | ✅ Full | ADD/DROP COLUMN, ALTER COLUMN |
12+
| `CreateIndexStatement` | ✅ Full | Index types (BTREE, HASH, GIN, GiST, BRIN) |
13+
| `DropIndexStatement` | ✅ Full | |
14+
| `CreateViewStatement` | ✅ Full | |
15+
| `DropViewStatement` | ✅ Full | |
1616
| `PostgresCreateMaterializedViewExpression` | ✅ Full | IF NOT EXISTS, TABLESPACE, storage parameters (see [materialized views](./materialized_view.md)) |
17-
| `PostgresRefreshMaterializedViewExpression` | ✅ Full | CONCURRENTLY (PG 9.4+, requires a UNIQUE index) |
18-
| `PostgresAlterMaterializedViewExpression` | ✅ Full | RENAME TO, SET SCHEMA, SET/RESET storage parameters, OWNER TO |
17+
| `PostgresRefreshMaterializedViewPgStatement` | ✅ Full | CONCURRENTLY (PG 9.4+, requires a UNIQUE index) |
18+
| `PostgresAlterMaterializedViewStatement` | ✅ Full | RENAME TO, SET SCHEMA, SET/RESET storage parameters, OWNER TO |
1919
| `PostgresDropMaterializedViewExpression` | ✅ Full | IF EXISTS, CASCADE |
2020
| `CreatePartitionExpression` | ✅ Full | RANGE, LIST, HASH partitioning (PG 10+) |
2121
| `AttachPartitionExpression` | ✅ Full | Standard ATTACH PARTITION; `CONCURRENTLY` is unsupported |
@@ -30,7 +30,7 @@ The PostgreSQL backend supports the same type-safe DDL expressions as the core l
3030
PostgreSQL 10+ supports declarative partitioning. See [Partition Documentation](../postgres_specific_features/partition.md).
3131

3232
```python
33-
partition = PostgresCreatePartitionExpression(
33+
partition = PostgresCreatePartitionStatement(
3434
dialect, parent_table="orders", partition_name="orders_2024_q1",
3535
partition_type="RANGE", bounds="FROM ('2024-01-01') TO ('2024-04-01')",
3636
)
@@ -44,18 +44,18 @@ See [Partition Documentation](../postgres_specific_features/partition.md) for pg
4444

4545
```python
4646
from rhosocial.activerecord.backend.impl.postgres.expression.ddl.index import (
47-
PostgresAlterIndexExpression, PostgresAlterIndexActionType,
48-
PostgresReindexExpression,
47+
PostgresAlterIndexStatement, PostgresAlterIndexActionType,
48+
PostgresReindexStatement,
4949
)
5050

5151
# ALTER INDEX ... RENAME TO
52-
alter = PostgresAlterIndexExpression(
52+
alter = PostgresAlterIndexStatement(
5353
dialect, index_name="idx_old",
5454
action=PostgresAlterIndexActionType.RENAME_TO("idx_new"),
5555
)
5656

5757
# REINDEX CONCURRENTLY
58-
reindex = PostgresReindexExpression(
58+
reindex = PostgresReindexStatement(
5959
dialect, target_type="INDEX", target_name="idx_corrupted",
6060
concurrently=True,
6161
)
@@ -66,7 +66,7 @@ reindex = PostgresReindexExpression(
6666
PostgreSQL supports various index types:
6767

6868
```python
69-
create_idx = CreateIndexExpression(
69+
create_idx = CreateIndexStatement(
7070
dialect,
7171
index_name="idx_users_name",
7272
table_name="users",
@@ -82,7 +82,7 @@ PostgreSQL supports partial indexes with WHERE clause:
8282
```python
8383
from rhosocial.activerecord.backend.expression import Column, Literal
8484

85-
create_idx = CreateIndexExpression(
85+
create_idx = CreateIndexStatement(
8686
dialect,
8787
index_name="idx_active_users",
8888
table_name="users",
@@ -96,7 +96,7 @@ create_idx = CreateIndexExpression(
9696
PostgreSQL supports schemas:
9797

9898
```python
99-
create_table = CreateTableExpression(
99+
create_table = CreateTableStatement(
100100
dialect,
101101
table_name="schema_name.users",
102102
columns=columns

‎docs/en_US/ddl/materialized_view.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@
1313
| Expression | Purpose | PostgreSQL-specific options |
1414
|---|---|---|
1515
| `PostgresCreateMaterializedViewExpression` | `CREATE MATERIALIZED VIEW` | `schema`, `if_not_exists`, `storage_options`, `tablespace`, `column_aliases`, `with_data` |
16-
| `PostgresRefreshMaterializedViewExpression` | `REFRESH MATERIALIZED VIEW` | `schema`, `concurrently`, `with_data` |
17-
| `PostgresAlterMaterializedViewExpression` | `ALTER MATERIALIZED VIEW` | `schema`, action list |
16+
| `PostgresRefreshMaterializedViewPgStatement` | `REFRESH MATERIALIZED VIEW` | `schema`, `concurrently`, `with_data` |
17+
| `PostgresAlterMaterializedViewStatement` | `ALTER MATERIALIZED VIEW` | `schema`, action list |
1818
| `PostgresDropMaterializedViewExpression` | `DROP MATERIALIZED VIEW` | `schema`, `if_exists`, `cascade` |
1919

20-
The generic core expressions (`CreateMaterializedViewExpression`,
21-
`RefreshMaterializedViewExpression`, `DropMaterializedViewExpression`) remain
20+
The generic core expressions (`CreateMaterializedViewStatement`,
21+
`RefreshMaterializedViewStatement`, `DropMaterializedViewStatement`) remain
2222
usable; they simply do not carry `schema` / `if_not_exists`.
2323

2424
## Creating a materialized view
2525

2626
```python
27-
from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression, TableExpression
27+
from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryStatement, TableExpression
2828
from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter
2929
from rhosocial.activerecord.backend.impl.postgres.expression.ddl import (
3030
PostgresCreateMaterializedViewExpression,
3131
)
3232

33-
summary = QueryExpression(
33+
summary = QueryStatement(
3434
dialect=dialect,
3535
select=[
3636
Column(dialect, "product_id"),
@@ -117,19 +117,19 @@ create = PostgresCreateMaterializedViewExpression(
117117

118118
```python
119119
from rhosocial.activerecord.backend.impl.postgres.expression.ddl import (
120-
PostgresRefreshMaterializedViewExpression,
120+
PostgresRefreshMaterializedViewPgStatement,
121121
)
122122

123123
# standard refresh
124-
PostgresRefreshMaterializedViewExpression(dialect=dialect, name="sales_summary")
124+
PostgresRefreshMaterializedViewPgStatement(dialect=dialect, name="sales_summary")
125125

126126
# concurrent refresh — requires a UNIQUE index, rejected below PG 9.4
127-
PostgresRefreshMaterializedViewExpression(
127+
PostgresRefreshMaterializedViewPgStatement(
128128
dialect=dialect, name="sales_summary", schema="reporting", concurrently=True
129129
)
130130

131131
# populate a WITH NO DATA view
132-
PostgresRefreshMaterializedViewExpression(
132+
PostgresRefreshMaterializedViewPgStatement(
133133
dialect=dialect, name="sales_daily", with_data=True
134134
)
135135
```
@@ -145,15 +145,15 @@ creation).
145145

146146
```python
147147
from rhosocial.activerecord.backend.impl.postgres.expression.ddl import (
148-
PostgresAlterMaterializedViewExpression,
148+
PostgresAlterMaterializedViewStatement,
149149
PostgresChangeMaterializedViewOwnerAction,
150150
PostgresRenameMaterializedViewAction,
151151
PostgresResetMaterializedViewPropertiesAction,
152152
PostgresSetMaterializedViewPropertiesAction,
153153
PostgresSetMaterializedViewSchemaAction,
154154
)
155155

156-
alter = PostgresAlterMaterializedViewExpression(
156+
alter = PostgresAlterMaterializedViewStatement(
157157
dialect=dialect,
158158
view_name="sales_summary",
159159
schema="reporting",
@@ -209,7 +209,7 @@ info = backend.introspector.get_materialized_view_info("sales_summary")
209209
exists = backend.introspector.materialized_view_exists("sales_summary")
210210
```
211211

212-
Each `ViewInfo` carries:
212+
Each `IntrospectionViewInfo` carries:
213213

214214
| Field | Meaning |
215215
|---|---|

‎docs/en_US/postgres_specific_features/dialect.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,22 +140,22 @@ sql, params = expr.to_sql()
140140
PostgreSQL supports copying table structure using the `LIKE` clause with fine-grained control over what gets copied via INCLUDING/EXCLUDING options.
141141

142142
PostgreSQL renders the clause form `CREATE TABLE t (LIKE src [INCLUDING ...])`,
143-
modelled by `CreateTableLikeExpression`.
143+
modelled by `CreateTableLikeStatement`.
144144

145145
```python
146-
from rhosocial.activerecord.backend.expression import CreateTableLikeExpression
146+
from rhosocial.activerecord.backend.expression import CreateTableLikeStatement
147147
from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect
148148

149149
# Basic usage - copy table structure
150-
expr = CreateTableLikeExpression(
150+
expr = CreateTableLikeStatement(
151151
PostgresDialect(),
152152
table="users_copy",
153153
like_table="users",
154154
)
155155
# Generates: CREATE TABLE "users_copy" (LIKE "users")
156156

157157
# With INCLUDING options (dictionary format - recommended)
158-
expr = CreateTableLikeExpression(
158+
expr = CreateTableLikeStatement(
159159
PostgresDialect(),
160160
table="users_copy",
161161
like_table="users",
@@ -168,15 +168,15 @@ expr = CreateTableLikeExpression(
168168
# INCLUDING CONSTRAINTS INCLUDING INDEXES EXCLUDING COMMENTS)
169169

170170
# With schema-qualified source table
171-
expr = CreateTableLikeExpression(
171+
expr = CreateTableLikeStatement(
172172
PostgresDialect(),
173173
table="users_copy",
174174
like_table=('public', 'users'),
175175
)
176176
# Generates: CREATE TABLE "users_copy" (LIKE "public"."users")
177177

178178
# With TEMPORARY and IF NOT EXISTS
179-
expr = CreateTableLikeExpression(
179+
expr = CreateTableLikeStatement(
180180
PostgresDialect(),
181181
table="temp_users",
182182
like_table="users",
@@ -186,7 +186,7 @@ expr = CreateTableLikeExpression(
186186
# Generates: CREATE TEMPORARY TABLE IF NOT EXISTS "temp_users" (LIKE "users")
187187

188188
# Including everything
189-
expr = CreateTableLikeExpression(
189+
expr = CreateTableLikeStatement(
190190
PostgresDialect(),
191191
table="users_copy",
192192
like_table="users",
@@ -207,7 +207,7 @@ expr = CreateTableLikeExpression(
207207
- `ALL` - Copy everything
208208

209209
**Important Notes:**
210-
- `CreateTableLikeExpression` carries only the target table and source `like_table`
210+
- `CreateTableLikeStatement` carries only the target table and source `like_table`
211211
- `like_options` accepts a dict (`including`/`excluding`), a list of feature
212212
strings (treated as `INCLUDING`), or a list of `(action, feature)` tuples
213213
- Only `temporary` and `if_not_exists` flags are considered

0 commit comments

Comments
 (0)