Skip to content

feat(postgres): adopt the expression/dialect split and declare the probes core now gates on - #61

Merged
vistart merged 8 commits into
release/v1.0.0.dev17from
feature/expression-schema-object-refactor
Oct 8, 2026
Merged

vistart merged 8 commits into
release/v1.0.0.dev17from
feature/expression-schema-object-refactor

Conversation

@vistart

@vistart vistart commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Description

This is the PostgreSQL backend's part of the eleven-repository coordinated change that follows
core PR rhosocial/python-activerecord#125 ("separate what an expression collects from what a
dialect decides"), which landed on the core release/v1.0.0.dev30 line. Every repository in the
series carries a branch named feature/expression-schema-object-refactor; this one is branched
from, and targets, release/v1.0.0.dev17 and is self-contained (see Dependency below).

What core changed, in outline:

  • Expressions collect parameters and the dialect decides the spelling: nodes no longer validate
    or pick spellings themselves. Every two-spelling clause got one parameter per spelling
    (cycle/no_cycle, cascade/restrict, enforced/not_enforced, with_data/no_data,
    materialized/not_materialized, restart_identity/continue_identity, include_nulls/
    exclude_nulls, all_/distinct, wait/no_wait, cache/no_cache, order/no_order,
    deferrable/not_deferrable, initially_deferred/initially_immediate). "Not specified" is
    "neither parameter set"; setting both raises ValueError.
  • dialect/protocols.py became the dialect/protocols/ package, with the umbrella protocols
    split per statement (SchemaSupport -> CreateSchemaSupport/DropSchemaSupport/
    NamespaceSupport; IndexSupport -> CreateIndexSupport/DropIndexSupport/
    FulltextIndexSupport/IndexObjectSupport).
  • Capability probes became binding: a probe answering False while SQL was still rendered is a
    defect, and probed answers that had never been measured were measured and corrected.

This branch does seven commits (oldest first):

  1. 249a5cb refactor(postgres): carry catalogue objects instead of parsed names -- a relation is
    split into the object it names and the source that reads it; qualified names are carried as
    schema objects rather than parsed strings, and COMMENT ON renders from the object it
    annotates. The naming side is stated once, in mixins/namespace.py +
    protocols/catalog.py (PostgresNamespaceSupport); PostgresCatalogMixin /
    PostgresCatalogSupport are kept as importable deprecated aliases. Six names (VECTOR,
    HALFVEC, BITVEC, SPARSEVEC, GEOMETRY, GEOGRAPHY) were removed from
    type_values.__all__; they were never module attributes, and the pgvector/PostGIS types that
    do exist are exported under their Postgres* class names. Resolving every format_* name
    through PostgresDialect.__mro__ (411 names) showed none answered by a protocol stub body.
    The expression round-trip matrix was reworked (see Testing).
  2. 877cebe fix(postgres): declare the sequence probes core now gates on -- core made
    supports_sequence() the master switch for all three sequence formatters and added four
    option probes. The mixin now answers every probe those formatters consult.
  3. 1887630 fix(postgres): declare the ALTER SEQUENCE START probe explicitly -- START WITH on
    ALTER SEQUENCE is a different clause from CREATE SEQUENCE ... START WITH, and core's
    default for the former is False.
  4. 0ea4d39 feat(postgres): declare the identity probes and gate identity DDL on them -- identity
    and auto-increment nodes became fail-closed in core; this backend declared none of the new
    probes, so core's formatters refused every identity clause. The measured boundary is declared
    and the legacy is_auto_increment constraint now routes through the gated formatter.
  5. b575a82 feat(postgres): consume the clause-parameter pairs and close the dialect gaps -- the
    formatters that still read pre-split fields now consume the pair shape; dialect-specific nodes
    were split to the same shape; new probes were declared and two unspellable options refused by
    name.
  6. 592e4f9 feat(postgres): declare the master probes core now gates on -- the CTE, TRUNCATE,
    WITH [NO] DATA and transaction wait probes are declared from measurement, and the PostgreSQL
    overrides consume the probes the core formatters consume.
  7. 7246214 ci: pin the core dependency to the merged release line -- CORE_PKG now points at
    release/v1.0.0.dev30 instead of the core feature branch.

Clause pairs this dialect now consumes

Formatter / override Pair(s) What was wrong before
format_truncate_statement restart_identity/continue_identity, cascade/restrict CONTINUE IDENTITY and RESTRICT were silently dropped
format_create_materialized_view_statement, format_refresh_materialized_view_pg_statement with_data/no_data unspecified rendered WITH NO DATA; the negative was unreachable in PostgresCreateMaterializedViewExpression
format_drop_materialized_view_statement cascade/restrict RESTRICT was silently dropped
format_begin_transaction, format_set_transaction deferrable/not_deferrable DEFERRABLE rendered only alongside SERIALIZABLE; unspecified rendered NOT DEFERRABLE
_format_exclude_constraint deferrable/not_deferrable, initially_deferred/initially_immediate tri-state Optional[bool] read
format_copy_to_statement force_array/force_array_false tri-state Optional[bool] read
format_table_constraint enforced/not_enforced tri-state gate; table constraints refused every state
format_create_sequence_statement, format_alter_sequence_statement, format_identity_clause cache/no_cache no override existed; the branch adds one so the unspellable negative is refused by name instead of rendered

Dialect-specific nodes reshaped to the same pair form: PostgresRefreshMaterializedViewExpression
(with_data/no_data), PostgresCopyToExpression (force_array/force_array_false),
PostgresCreateMaterializedViewExpression (with_data/no_data),
PostgresDropMaterializedViewExpression (restrict pass-through), and
PostgresExcludeConstraint (deferrable/not_deferrable, initially_deferred/
initially_immediate). The exclude constraint was found by the sweep rather than the audit
registry and was measured before and after.

Probes declared or corrected

Sequence (commit 877cebe, 1887630):

  • supports_sequence(), supports_create_sequence(), supports_drop_sequence(),
    supports_alter_sequence(), supports_sequence_if_not_exists(),
    supports_sequence_if_exists(), supports_sequence_start(), _increment(), _minvalue(),
    _maxvalue(), _cycle(), _cache(), _owned_by() answer True.
  • supports_sequence_order() answers False: ORDER / NO ORDER is Oracle / SQL Server syntax,
    absent from the PostgreSQL CREATE SEQUENCE synopsis and rejected by the live server.
  • NO CACHE has no PostgreSQL form (syntax error at or near "CACHE", measured on PostgreSQL
    16); both sequence formatters refuse no_cache=True by name rather than render a token the
    server rejects.
  • supports_alter_sequence_start() answers True explicitly. The core default is False
    because Oracle, SQL Server, Firebird and Snowflake reject START on ALTER SEQUENCE;
    PostgreSQL accepts it (records the start value future RESTART calls use).

Identity / auto-increment (commit 0ea4d39):

  • supports_identity_column() answers True from PostgreSQL 10 on (version >= (10, 0, 0)):
    PostgreSQL 9 rejects the clause with syntax error at or near "GENERATED" (measured), while 10
    through 19 accept it. The six option probes (generation ALWAYS, start, increment,
    minvalue, maxvalue, cycle) answer True -- same synopsis.
  • supports_auto_increment_column() answers False: PostgreSQL has no AUTO_INCREMENT spelling.
    It is declared explicitly rather than inherited from the fail-closed default.

Restrict / cache option probes (commit b575a82, measured against live PostgreSQL 16 and 19):

  • supports_schema_restrict(), supports_restrict_view(),
    supports_materialized_view_restrict(), supports_truncate_restrict() answer True
    (RESTRICT is the default behavior but the token itself is legal).
  • supports_drop_function_restrict() answers True and follows the routine DDL gate
    (>= 9.6).
  • supports_identity_cache() answers True (CACHE 1 is the uncached form); NO CACHE is
    refused by name for identity too.

Master probes (commit 592e4f9; measurements on the 9.6 - 19beta4 matrix):

  • supports_materialized_cte() keeps its >= 12 gate: 9.6, 10 and 11 reject AS MATERIALIZED
    and AS NOT MATERIALIZED; 12 through 19beta4 accept both.
  • supports_truncate() answers True -- accepted by every server in the matrix.
  • supports_with_data_clause() answers True, version-independent; the one probe answers all
    three consumers (CTAS, CREATE MATERIALIZED VIEW, REFRESH MATERIALIZED VIEW).
  • supports_transaction_wait() answers False: BEGIN NO WAIT and SET TRANSACTION NO WAIT are
    syntax errors on every measured server (9.6 through 19beta4); the pair is refused by name
    instead of being dropped from the rendered statement.

Naming side (commit 249a5cb): supports_catalog(), supports_catalog_qualification() and
supports_schema_qualification() answer True; validate_catalog_name() accepts any non-empty
database name; format_qualified_name() spells [database.][schema.]name level by level.
supports_schema() remains the DDL question ("can this engine have schemas") and stays in
mixins/schema.py.

Routine (commit 249a5cb): supports_function(), supports_create_function(),
supports_drop_function(), supports_function_or_replace(), supports_function_parameters(),
supports_drop_function_if_exists(), supports_drop_function_cascade() and
supports_drop_function_restrict() are declared; the CREATE/DROP forms are tied to
supports_function_ddl() (>= 9.6). CreateFunctionExpression / DropFunctionExpression now
render through the core formatters.

Intended behaviour changes

  • CREATE MATERIALIZED VIEW with nothing requested no longer appends WITH NO DATA.
  • SET TRANSACTION with nothing requested no longer appends NOT DEFERRABLE.
  • BEGIN ... DEFERRABLE renders for any isolation level.
  • RESTRICT and CONTINUE IDENTITY become reachable where they were previously dropped.
  • CTAS and generic REFRESH MATERIALIZED VIEW with WITH [NO] DATA render instead of being
    refused by the fail-closed default; wait / no_wait now raise UnsupportedFeatureError
    naming the option instead of being silently dropped.
  • A PostgreSQL 9 server asked for an identity column is refused
    (UnsupportedFeatureError) instead of being handed GENERATED ... AS IDENTITY; the SERIAL
    type family keeps working and stays the explicit choice.

Dependency

The workflow installs core from release/v1.0.0.dev30 (the line PR #125 landed on) and the
testsuite from its own feature/expression-schema-object-refactor branch, which is not merged
yet. No unmerged core branch is needed, so this PR can be merged on its own.

Adds # (issue): n/a -- no tracking issue in this repository; core counterpart is
rhosocial/python-activerecord#125.

Type of change

  • feat: A new feature
  • test: Adding missing tests or correcting existing tests (for the new feature)
  • ci: Changes to our CI configuration files and scripts (if related to the new feature)

Breaking Change

  • Yes
  • No
    (If Yes, please describe the impact and migration path below):

Yes, but only on the dialect/expression layer. No model- or query-level application API is
touched: every changed file under src/ is in rhosocial/activerecord/backend/impl/postgres/,
and no model or query-builder signature changed. The breaks are for code that uses this
package's dialect/expression layer directly:

  • PostgresDialect.supports_auto_increment() is removed. It answered True for "AUTO_INCREMENT/
    IDENTITY supported" and was defined on PostgresGeneratedColumnMixin. It is replaced by two
    mechanism probes: supports_identity_column() (true from PostgreSQL 10) and
    supports_auto_increment_column() (False). Migration: ask the specific probe.
  • The PostgreSQL-specific DDL expression constructors now take schema objects and one parameter
    per spelling, for example PostgresCreateMaterializedViewExpression(dialect, view=MaterializedView(...), with_data=False, no_data=True) instead of view_name="..." plus a tri-state; the same applies
    to PostgresDropMaterializedViewExpression (restrict), PostgresRefreshMaterializedViewExpression
    (with_data/no_data), PostgresCopyToExpression (force_array/force_array_false) and
    PostgresExcludeConstraint (deferrable/not_deferrable, initially_deferred/
    initially_immediate). Setting both of a pair raises ValueError at construction. Migration:
    pass the object class instead of a name string and exactly one side of each pair.
  • format_materialized_view_alter_action() now returns (sql, params) instead of a bare string
    (protocols/materialized_view.py).
  • Some existing calls now render different SQL (listed under Intended behaviour changes);
    transaction wait/no_wait is now refused by name instead of being silently dropped, and
    requests for spellings PostgreSQL lacks (no_cache on sequences and identity, a bare
    AUTO_INCREMENT, identity on PostgreSQL 9 via the legacy is_auto_increment route) raise
    UnsupportedFeatureError instead of rendering SQL the server rejects.
  • Renamed naming-side classes keep deprecated aliases: PostgresCatalogMixin ->
    PostgresNamespaceMixin, PostgresCatalogSupport -> PostgresNamespaceSupport; both old names
    stay importable.

Applicable rhosocial-activerecord Version Range:
pyproject.toml declares rhosocial-activerecord>=1.0.0.dev0,<2.0.0; this branch requires the
expression/dialect split merged as core #125 on the release/v1.0.0.dev30 line, which is what
the workflow pins.

Related Repositories/Packages

  • rhosocial/python-activerecord (core): PR #125, merged on release/v1.0.0.dev30. That is the
    entire cross-repository dependency of this branch; test.yml installs core from that line.
  • rhosocial/python-activerecord-testsuite: feature/expression-schema-object-refactor, not
    merged yet. CI installs the testsuite from that branch; it does not block merging this PR, but
    the testsuite branch must land for these tests to run against a released testsuite.
  • The other backend repositories in the series each carry a branch named
    feature/expression-schema-object-refactor; they are independent of this PR (eleven
    repositories total, core included).

Testing

  • I have added new tests to cover my changes.
  • I have updated existing tests to reflect my changes.
  • All existing tests pass.
  • This change has been tested on [PostgreSQL 9.6, 10, 11, 12, 13, 14, 15, 16, 17, 18 and 19beta4; Python 3.8 through 3.14].

Test Plan:

New guard / confirmation files added by this branch:

  • tests/rhosocial/activerecord_postgres_test/feature/backend/test_clause_pair_guard.py -- asserts
    that the four states of every pair this dialect consumes are pairwise distinguishable (neither
    set renders neither spelling; A renders A only; B renders B only; both raises ValueError),
    plus a non-vacuousness class (every case has two distinct parameters and a spelling or a named
    refusal). Run red against the unmodified tree first: 24 failing test ids; green after commit
    b575a82. Extended in 592e4f9 with the master probes and the wait pair: red first with 13
    failing ids of 155; green after. Two mutations (removed exclusion check, reverted restrict
    branch) were each caught by the expected guard test.
  • tests/rhosocial/activerecord_postgres_test/feature/backend/test_identity_execution.py -- renders
    identity / auto-increment / SERIAL DDL and executes it against a live server; each verdict is
    preceded by a rejected sentinel and accepted control, identity is NOT_RENDERED on PostgreSQL 9
    where the probe says False.
  • tests/rhosocial/activerecord_postgres_test/feature/backend/test_master_probe_execution.py --
    execution confirmation for the master probes (materialized CTE, truncate, WITH [NO] DATA,
    wait), green on the 11-scenario matrix (9.6 through 19beta4).
  • 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; the
    (statement class, slot, required kind, impostor kind) table is derived from the constructor
    annotations rather than hand-listed.

Reworked:

  • tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_roundtrip_all.py --
    the matrix used to call the testsuite's sql_consistent, which swallowed every render failure
    and reported green; the module docstring records the before-measurement (168 classes discovered,
    125 reached a SQL comparison, 20 raised and were swallowed, 23 skipped as unconstructible -- 43
    of 168 never asserted). Outcomes are now classified: renders (byte-identical SQL and bind
    parameters across dict/JSON/XML), UnsupportedFeatureError, entries in
    LEGITIMATE_NON_RENDERS pinned by type and message fragment, and anything else fails naming
    the class.

Updated: test_postgres_protocol_conformance.py (protocol/mixin forward and reverse coverage plus
the non-overlap guard for the new protocols), test_ddl_expressions.py, test_ddl_objects.py,
test_ddl_alter_config.py, test_materialized_view.py, test_materialized_view_integration.py,
test_drop_table_cascade.py, test_schema_support.py, test_schema_namespace.py,
test_view_execution.py, test_index_pg.py, test_auto_increment_ddl.py,
test_transaction_advanced_features.py, test_model_truncate.py, and the extension integration
suites.

Commands:

export PYTHONPATH=src:tests
python -m pytest tests/ --tb=short -n 8 --dist=loadgroup

The workflow matrix covers: Python 3.8 / PostgreSQL 9, 3.8 / 10, 3.9 / 11, 3.10 / 12, 3.11 / 13,
3.12 / 14, 3.13 / 15, 3.13 / 16, 3.14 / 17 and 3.14 / 18 (coverage), plus a Python 3.14 /
PostgreSQL 19beta4 job and a Python 3.14 / PostgreSQL 18 benchmark job. The recorded green run for
the last code commit (592e4f9, workflow run 37657306058,
https://github.com/rhosocial/python-activerecord-postgres/actions/runs/37657306058) shows all
twelve matrix jobs and the benchmark job passing; 7246214 only changes the workflow's core
dependency pin.

Checklist

  • My code follows the project's code style guidelines.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation (Docstrings, README.md, etc.).
  • My changes generate no new warnings.
  • I have added a Changelog fragment (changelog.d/<issue_number>.<type>.md).
  • I have verified that my changes do not introduce SQL injection vulnerabilities.
  • I have checked for potential performance regressions.

Additional Notes

  • Documentation updated by the branch: docs/en_US/postgres_specific_features/protocol_support.md,
    docs/zh_CN/postgres_specific_features/protocol_support.md,
    docs/zh_CN/ddl/materialized_view.md, .claude/backend_development.md, and the docstrings
    across the changed mixins/protocols. The examples under src/.../impl/postgres/examples/
    were updated to the object- and pair-based constructor shapes.
  • No changelog fragment is present on this branch (git diff origin/release/v1.0.0.dev17..HEAD -- changelog.d/ is empty); add one when opening the PR if
    the repository requires it (changelog.d/<issue>.added.md / .changed.md).
  • The warning checklist item is left unchecked deliberately: the test logs still carry
    pre-existing DeprecationWarnings from core (e.g. raw subtype strings), and no
    warnings-as-errors run was performed on this branch.
  • No performance regression check was recorded; the CI benchmark job ran green, but it does not
    gate on regression.

Core's expression and dialect layers were reworked: a relation is split into the
object it names and the source that reads it, the namespace shape became a
protocol the dialect states once, and to_sql() now inspects what answers a format
lookup instead of calling whatever is callable. Qualified names are carried here as
schema objects rather than parsed strings, which is what made the object and the
statement that acts on it able to disagree about what "the same table" meant.

Six types this backend does not have were being exported. COMMENT ON now renders
from the object it annotates.

The round-trip matrix this branch added was reporting nothing, which the refactor
made visible rather than causing. Three classes it pinned as raising AttributeError
name a format_method whose formatter PostgreSQL does not have, and core now reports
a missing formatter as UnsupportedFeatureError -- the spelling every other capability
probe in the tree already used. The pins move with it, and each now names the
formatter it is missing so the entry says which gap it is.

Walking PostgresDialect.__mro__ to resolve every format_* name to the class that
defines it shows none of the 411 resolving to a Protocol: the stub format_* a
protocol declares in the MRO are each overridden by a real mixin ahead of it. That
includes CreateRoutineSupport and DropRoutineSupport, whose presence in the base
list used to answer a dispatch with a protocol body returning None; the core
CreateFunctionExpression and DropFunctionExpression now render.
Core commit 9dc18ab made all three sequence DDL formatters consult
supports_sequence() first and unconditionally, and added four option probes
(supports_sequence_start, _increment, _minvalue, _maxvalue). PostgreSQL's
sequence mixin never defined supports_sequence -- core had never asked for it --
so the master switch fell back to core's default False and CREATE SEQUENCE,
DROP SEQUENCE and ALTER SEQUENCE all raised UnsupportedFeatureError. A
full-support dialect had stopped rendering the statement it supports.

The mixin now answers every probe those formatters consult. supports_sequence,
supports_create_sequence, supports_drop_sequence, supports_alter_sequence,
supports_sequence_if_not_exists, supports_sequence_if_exists, and the option
probes start, increment, minvalue, maxvalue, cycle, cache and owned_by all
return True. supports_sequence_order returns False: the PostgreSQL
CREATE SEQUENCE synopsis has no ORDER / NO ORDER clause -- that spelling is
Oracle and SQL Server -- and the live server rejects it with a syntax error,
so the formatter must refuse it rather than emit a clause the engine cannot
parse.

PostgresSequenceSupport restates the same switches, so the protocol and the
mixin agree method for method. It derives from core's CreateSequenceSupport,
DropSequenceSupport and AlterSequenceSupport, which is how the non-overlap
guard in the protocol conformance test recognises the restatement as the
deliberate overlap it is. The dialect's three redundant direct protocol bases
are dropped for the same reason: a base may not precede the subclass that
restates it. The three DDL formatters remain core's; this backend only answers
the capability questions they ask.
Core's format_alter_sequence_statement gated AlterSequenceExpression.start on
supports_sequence_start(), the probe for the CREATE SEQUENCE ... START WITH
clause. Those are two different clauses, and True for one does not imply True
for the other. PostgreSQL accepts START on ALTER -- it records the start value
that future RESTART calls use, while RESTART WITH sets the current value -- but
Oracle, SQL Server, Firebird and Snowflake all reject it. Reusing the CREATE
probe for the ALTER formatter would therefore let those four dialects emit SQL
their server rejects.

Add supports_alter_sequence_start() to PostgresSequenceMixin, answering True
because the live server accepts the clause, and declare it on
PostgresSequenceSupport so the protocol keeps mirroring the mixin. The default
for the new probe is False, which is the safe side: a dialect that accepts the
clause must say so explicitly, as this one does.
…them

Core made the identity and auto-increment nodes fail-closed: IdentityClause
consults supports_identity_column() plus one probe per option, and
AutoIncrementClause consults supports_auto_increment_column(). PostgreSQL
declared none of the new probes -- the dialect did not mix in the identity
mixin at all -- so core's formatters refused every identity clause.

Declare the measured boundary: supports_identity_column() is True from
PostgreSQL 10 on, because PostgreSQL 9 rejects the clause with `syntax error
at or near "GENERATED"` (measured) while 10 through 19 all accept it. The six
option probes -- generation ALWAYS, start, increment, minvalue, maxvalue,
cycle -- answer True: they are part of the same synopsis.
supports_auto_increment_column() answers False: PostgreSQL has no
AUTO_INCREMENT spelling, and the probe is declared explicitly rather than
inherited from the fail-closed default.

The legacy is_auto_increment constraint now routes through the gated identity
formatter, so PostgreSQL 9 refuses an identity request instead of rendering a
GENERATED clause its server rejects. There is no fallback to SERIAL: the SERIAL
type family stays available for users who ask for it explicitly, and it keeps
executing on every version.

PostgresIdentitySupport and PostgresAutoIncrementSupport mirror the two new
mixins; the protocol conformance test pairs them with the existing
forward/reverse coverage. The new execution-confirmation suite renders and
executes each clause against a live server: accepted where the probe says True,
NOT_RENDERED on PostgreSQL 9 where it says False, with a rejected sentinel and
an accepted control before each verdict. The PG9 identity example now uses the
supported IdentityAttribute path instead of a keyword ColumnDefinition never
accepted.
…ect gaps

Core split every two-spelling clause into one parameter per spelling
(438b7aa): unspecified means neither parameter, both means ValueError. The
PostgreSQL formatters still read the pre-split fields, so this follows.

Formatters consuming a pair:
- format_truncate_statement: restart_identity|continue_identity (CONTINUE
  IDENTITY was silently dropped) and cascade|restrict (RESTRICT was silently
  dropped).
- format_create_materialized_view_statement and
  format_refresh_materialized_view_pg_statement: with_data|no_data;
  unspecified renders no tail (the old else-branch spelled WITH NO DATA).
- format_drop_materialized_view_statement: cascade|restrict (RESTRICT was
  silently dropped).
- format_begin_transaction / format_set_transaction: deferrable|not_deferrable;
  [NOT] DEFERRABLE renders whenever requested, not only alongside SERIALIZABLE,
  and unspecified no longer spells NOT DEFERRABLE.
- _format_exclude_constraint: deferrable|not_deferrable and
  initially_deferred|initially_immediate; INITIALLY ... renders as the
  independent constraint attribute it is.
- format_copy_to_statement: force_array|force_array_false.
- format_table_constraint: the enforced gate no longer reads a tri-state, so
  table constraints render again instead of refusing every state.

Dialect-specific nodes split to the pair shape:
- PostgresRefreshMaterializedViewExpression: with_data|no_data.
- PostgresCopyToExpression: force_array|force_array_false.
- PostgresCreateMaterializedViewExpression: with_data|no_data (its constructor
  dropped no_data and defaulted to WITH DATA).
- PostgresDropMaterializedViewExpression: restrict pass-through.
- PostgresExcludeConstraint: deferrable|not_deferrable and
  initially_deferred|initially_immediate. The audit registry did not carry this
  node; it was found by the sweep and measured before/after.

New probes declared True (measured against live PostgreSQL 16 and 19):
supports_schema_restrict, supports_restrict_view,
supports_drop_function_restrict, supports_materialized_view_restrict,
supports_truncate_restrict, and supports_identity_cache. The CACHE option's
negative spelling, NO CACHE, has no PostgreSQL form (syntax error, measured),
so no_cache is refused by name for sequences and identity while cache=n
renders; ORDER/NO ORDER stay refused.

The transaction manager now drives the split setters, so set_deferrable(False)
renders NOT DEFERRABLE instead of raising.

Behaviour changes (intended, per the round):
- CREATE MATERIALIZED VIEW default no longer emits WITH NO DATA.
- SET TRANSACTION with nothing requested no longer emits NOT DEFERRABLE.
- BEGIN ... DEFERRABLE renders for any isolation level.
- RESTRICT and CONTINUE IDENTITY become reachable where they were dropped.

Guard: test_clause_pair_guard.py asserts the four states of every pair the
dialect consumes are pairwise distinguishable; run red against the unmodified
tree first (24 failing test ids), green after. Two mutations (one removed
exclusion check, one reverted restrict branch) were each caught by the
expected guard test.
Core's c4d6adc gave call sites to three probes that had been decorative:
format_cte_expression consults supports_materialized_cte(),
format_truncate_statement consults supports_truncate(), and the CTAS /
CREATE MATERIALIZED VIEW / REFRESH MATERIALIZED VIEW renderers consult the
new supports_with_data_clause() before spelling WITH [NO] DATA. It also
added supports_transaction_wait() for the wait / no_wait pair. A dialect
that leaves any of them to a core default refuses statements it used to
render: this package answered the with-data probe False through core's
default, and its transaction formatters dropped the wait pair entirely.

Declare each from measurement or the documented grammar:

- supports_materialized_cte() keeps its >= 12 gate (already declared here).
  Live measurement: 9.6, 10 and 11 reject AS MATERIALIZED / AS NOT
  MATERIALIZED with a syntax error; 12 through 19beta4 accept both.
- supports_truncate() answers True explicitly. TRUNCATE is in the synopsis
  of every supported version and the live matrix accepts it everywhere.
- supports_with_data_clause() answers True, version-independent. The clause
  is in all three synopses -- the CTAS form long predates materialized
  views, and CREATE / REFRESH MATERIALIZED VIEW ... [ WITH [ NO ] DATA ]
  is documented from 9.3 (the version that introduced materialized views)
  -- and the live matrix accepts every form. The same probe covers the
  REFRESH form; it needs no probe of its own.
- supports_transaction_wait() answers False. BEGIN NO WAIT and SET
  TRANSACTION NO WAIT are syntax errors on every measured server (9.6
  through 19beta4); PostgreSQL has no lock-wait clause.

The PostgreSQL overrides now consume the probes the core formatters
consume: format_create_materialized_view_statement and
format_refresh_materialized_view_pg_statement refuse WITH [NO] DATA by
name when the clause probe declines (generic REFRESH and CTAS delegate to
core's gate), format_truncate_statement consults supports_truncate(), and
format_begin_transaction / format_set_transaction refuse wait / no_wait by
name instead of silently dropping the pair.

PostgresTruncateSupport, PostgresMaterializedViewSupport and
PostgresTransactionSupport restate the declarations so the protocol and
the mixin stay mirrored; the conformance forward / reverse coverage tests
enforce the mirror.

Behaviour changes (intended, per the round):
- CTAS and generic REFRESH MATERIALIZED VIEW with WITH DATA / WITH NO DATA
  render instead of being refused by the fail-closed default.
- BEGIN / SET TRANSACTION wait=True / no_wait=True raise
  UnsupportedFeatureError naming the option instead of rendering a
  statement that dropped it.

Guard: test_clause_pair_guard.py adds the wait pair and a declaration /
gate class; run red against the unmodified tree first (13 failing ids of
155), green after. test_master_probe_execution.py renders each clause
through the live backend and asserts acceptance against the probe, with a
sentinel and a control before every verdict; green on the 11-scenario
matrix (9.6 through 19beta4).
The schema-object refactor these branches depend on landed on the core's
release/v1.0.0.dev30 line as PR #125, so the workflow no longer needs to install
the core from the feature branch. CORE_PKG now points at the release branch; the
testsuite dependency stays on its own feature branch, which is not merged yet.
@vistart vistart added the feature label Oct 7, 2026
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.45817% with 73 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.42%. Comparing base (9a74cca) to head (aebf3d7).

Files with missing lines Patch % Lines
...record/backend/impl/postgres/protocols/sequence.py 18.18% 1 Missing and 17 partials ⚠️
...backend/impl/postgres/protocols/identity_column.py 41.66% 0 Missing and 7 partials ⚠️
...record/backend/impl/postgres/mixins/ddl/routine.py 80.00% 6 Missing ⚠️
...rd/backend/impl/postgres/expression/ddl/comment.py 55.55% 2 Missing and 2 partials ⚠️
...verecord/backend/impl/postgres/functions/orafce.py 20.00% 4 Missing ⚠️
...iverecord/backend/impl/postgres/mixins/ddl/type.py 60.00% 2 Missing and 2 partials ⚠️
...erecord/backend/impl/postgres/protocols/catalog.py 70.00% 0 Missing and 3 partials ⚠️
...verecord/backend/impl/postgres/mixins/ddl/index.py 93.33% 1 Missing and 1 partial ⚠️
...record/backend/impl/postgres/mixins/ddl/trigger.py 81.81% 1 Missing and 1 partial ⚠️
...ecord/backend/impl/postgres/mixins/ddl_database.py 80.00% 1 Missing and 1 partial ⚠️
... and 14 more
Additional details and impacted files
@@                   Coverage Diff                    @@
##           release/v1.0.0.dev17      #61      +/-   ##
========================================================
+ Coverage                 75.06%   75.42%   +0.35%     
========================================================
  Files                       338      346       +8     
  Lines                     15807    16111     +304     
  Branches                   2680     2741      +61     
========================================================
+ Hits                      11866    12151     +285     
+ Misses                     2986     2969      -17     
- Partials                    955      991      +36     
Flag Coverage Δ
postgres-18 75.42% <85.45%> (+0.35%) ⬆️
python-3.14 75.42% <85.45%> (+0.35%) ⬆️
unittests 75.42% <85.45%> (+0.35%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vistart vistart self-assigned this Oct 7, 2026
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

@vistart
vistart merged commit 6733846 into release/v1.0.0.dev17 Oct 8, 2026
15 of 17 checks passed
@vistart vistart added enhancement New feature or request and removed feature labels Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant