Skip to content

test(ddl): add cross-backend DDL contracts and narrow the materialized-view contract - #21

Merged
vistart merged 15 commits into
release/v1.0.0.dev17from
feature/activerecord-derived-ddl
Sep 27, 2026
Merged

vistart merged 15 commits into
release/v1.0.0.dev17from
feature/activerecord-derived-ddl

Conversation

@vistart

@vistart vistart commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Brings the shared test suite in line with the derived-DDL work in
python-activerecord, and tightens the materialized-view contract so it only asserts
behaviour every MV backend can actually provide.

The MV contract previously assumed more than SQL guarantees. Three commits narrow it:

  • MV refresh is no longer treated as a SQL-standard statement; dialects that reach it
    through a vendor-specific route are handled as such.
  • Contract skips are now graded by whether the dialect can resolve a version, instead of
    skipping or failing wholesale.
  • The contract is reduced to semantics every MV backend shares.

Alongside that, two new contract suites cover areas that had none:

  • a cross-backend materialized-view contract
  • a DDLSource collection contract, with the retained-source cases pinned by tests

The expression tests are adapted to node-based casts, and timezone-aware datetimes are
normalised so round-trips compare equal. BMP-only Unicode round-trip tests are added, and
a requires_cte_order_by decorator is introduced with the matching protocol rename.
Deprecated features.py and the unused sqlite_features.py are removed.

Type of change

  • feat: A new feature
  • docs: Documentation only changes (if related to the new feature)
  • test: Adding missing tests or correcting existing tests (for the new feature)
  • chore: Changes to the build process or auxiliary tools and libraries (if related to the new feature)

Breaking Change

  • Yes, for end-users (backward-incompatible API changes)
  • Yes, for backend developers (internal architectural changes that may affect custom implementations)
  • No breaking changes

No commit carries a ! marker, and nothing in the public helper surface used by backend
repositories changes shape. The impact is on what the contracts now require of a backend.

Impact on End-Users:

None. This package is a shared test suite, not a runtime dependency.

Impact on Backend Developers (Custom Implementations):

The materialized-view contract is stricter in one direction and looser in another. A backend
that advertised supports_materialized_view() on the strength of the old, broader contract
may now fail cases it previously skipped. The grading change also alters when a case is
skipped rather than failed, so a backend may see a different pass/skip distribution without
any code change of its own.

Backend repositories should re-run their suites against this testsuite before their own
PRs are merged, since several are adapting to the same core change.

Related Repositories/Packages

This is the test-suite half of the derived-DDL work whose core half is
rhosocial/python-activerecord#121, already merged into release/v1.0.0.dev30.

The nine backend repositories consume this package and must be merged after it, each
repointing its workflow's install source away from the now-merged
feature/activerecord-derived-ddl branch:

Repository Feature tip
python-activerecord-mysql bde82d7
python-activerecord-postgres cc825f6
python-activerecord-mariadb 54ffce0
python-activerecord-sqlserver 28351e7
python-activerecord-oracle 6e6b15f
python-activerecord-clickhouse 7e23db0
python-activerecord-firebird d1dd46f
python-activerecord-snowflake 9307aef
python-activerecord-bigquery 41bb30b

Testing

  • I have added new tests to cover my changes.
  • I have updated existing tests to reflect my changes.
  • I have run the full test suite locally and confirmed all tests pass.

Not applicable, and deliberately not claimed. This package has no test suite of its
own
— it is the shared suite that other projects install into their own environments —
and it has no CI pipeline. Its .github/workflows/ contains only changelog-check.yml
and publish.yml, so there is no test job to run or to wait on.

The change is exercised indirectly: the nine backend repositories run it as part of their
own suites, and all nine were green against the matching core branch at the tips listed
above.

Checklist

  • My code follows the project's coding conventions
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly
  • I have added a changelog fragment to changelog.d/ (to be added after this PR is
    opened, named by the actual PR number)

Additional Notes

chore: remove dead code - deprecated features.py and unused sqlite_features.py is the only
commit here that is a pure deletion with no replacement. It is included because it is the
same dead-code sweep that core PR #121 carried, and splitting it out would have left the two
repositories inconsistent.

…ize tz-aware datetimes

- CastExpression is now an AST node: tests build it explicitly and assert
  the node wrapping (placeholder for prior _cast_types state).
- ColumnDefinition/ColumnConstraint/TableConstraint/ForeignKeyConstraint
  now require the dialect as the first constructor argument.
- Mock async/sync backends supply a SQLDialectBase-specced dialect so the
  strict dialect binding accepts them.
- _normalize_for_compare converts tz-aware datetimes to UTC before
  stripping tzinfo, so the same instant read back in a different session
  time zone compares equal.
- _placeholder_for/_column_definition support the DataType-family types.

(cherry picked from commit 93d6e37)
Add test_bmp_multilingual_content_round_trip and
test_bmp_json_field_round_trip (sync + async) to the logging
test suite. These exercise the entire Basic Multilingual Plane
(U+0000..U+FFFF) with no supplementary-plane characters.

Every backend can run these tests. Backends whose charset cannot
store supplementary characters (e.g. Oracle 18c AL32UTF8) should
skip the full-Unicode tests in their own conftest.py and keep
these BMP-only tests as their Unicode coverage.

(cherry picked from commit ff625be)
New decorator for tests requiring ORDER BY inside CTE definitions.
Used by test_cte_with_range_conditions (sync + async) which generate
CTE SQL with ORDER BY — unsupported on SQL Server.

(cherry picked from commit cf33153)
Model base already defaults to "id"; fixtures should exercise the unspecified-PK default path.

(cherry picked from commit 7f180c1)
Cover declaration ordering, field metadata, table defaults, and sync/async parity for the shared DDLSource contract.

(cherry picked from commit f2227e5)
The postgres materialized view defect (MRO order left the PG mixin dead) and
ClickHouse's equivalent (probe returned True while inheriting the core
formatter, producing SQL ClickHouse cannot execute) share one root cause:
nothing constrained a capability probe to the formatter behind it. The core
layer had no cross-backend MV contract at all, so each backend was on its own
and a defect could only ever be found inside that one backend.

Adds feature/basic/ddl/test_materialized_view_contract.py (expression/dialect
layer, no live connection) in two layers:

- Invariants (never skipped): declaring supports_materialized_view() implies
  owning the CREATE formatter; declaring supports_refresh_materialized_view()
  implies owning the refresh formatter; the generic expressions must render on
  any backend that claims support (catches formatters written against a private
  expression surface)
- Capability-gated rendering: statement shape for CREATE/DROP/REFRESH, with
  unsupported backends skipped declaratively

CREATE is constrained on its own because its syntax diverges on every backend
(PG WITH (...)/TABLESPACE/WITH [NO] DATA, Oracle BUILD IMMEDIATE/REFRESH,
Snowflake CLUSTER BY/COMMENT, ClickHouse ENGINE/TO/POPULATE/REFRESH). A backend
that genuinely wants the generic form satisfies the rule by overriding the
formatter and delegating to super(), turning a silent accident into an explicit
statement. DROP is left unconstrained: DROP MATERIALIZED VIEW [IF EXISTS]
[CASCADE] is broadly valid, so inheriting it is legitimate.

Measured: postgres 120 passed; ClickHouse 3 failed before its fix (naming the
missing override), Snowflake 3 failed before its fix (AttributeError on the
generic expression).

(cherry picked from commit 951172c)
…a version

Produced 12 failures on Oracle: OracleMaterializedViewMixin gates its formatter
on self.version (MVs need 9i, IF NOT EXISTS needs 23ai), while the ddl_dialect
fixture hands over an unadapted dialect, so every rendering assertion hit
DialectNotAdaptedException.

That is correct Oracle behaviour, not a defect — version-gated DDL genuinely
needs a resolved server version. The contract exists to catch lying probes, not
to require every backend to render SQL without a connection. Grading:

- MRO ownership invariants (need no environment, always run): probe True implies
  owning the CREATE / refresh formatter. This is the valuable one; it is what
  caught ClickHouse.
- Rendering assertions: skip when the probe or formatter raises
  DialectNotAdaptedException (handled uniformly by the _probe / _render helpers)

Verified on nine backends: postgres 96 passed, oracle 6 passed / 18 skipped
(ownership invariants still run), snowflake 6 passed / 2 skipped;
sqlite/mysql/mariadb/sqlserver/clickhouse/bigquery/firebird all skip on
capability.

(cherry picked from commit 971225c)
Landing Oracle's DBMS_MVIEW.REFRESH exposed the contract's second over-specification:
it asserted the rendered REFRESH starts with "REFRESH MATERIALIZED VIEW", but
Oracle renders BEGIN DBMS_MVIEW.REFRESH(...) END;.

Same root cause as the earlier DROP case: asserting a concrete statement shape
assumes every backend shares one syntax. What the contract must guarantee is
that the request renders and refers to the target view; the statement shape is
asserted per backend. Now: non-empty output plus a case-insensitive match on the
view name (Oracle passes a string name to DBMS_MVIEW and it folds to upper case).

Also fixes a self-contradictory assertion (upper-casing the SQL but comparing a
lower-case needle), which the previous assertion had short-circuited before it
could surface.

Verified on ten backends: oracle 12 passed (the new refresh assertion actually
executes), postgres 96 passed, the rest skip on capability.

(cherry picked from commit 3e7d997)
Landing BigQuery exposed two more over-specifications: column aliases and
DROP ... CASCADE. BigQuery's CREATE MATERIALIZED VIEW takes its column names
from the query (no alias clause) and its DROP has no CASCADE. Asserting those
while also asserting "it renders" again assumes one shared syntax.

This is the third relaxation for the same reason (DROP ownership, REFRESH
statement shape, aliases/CASCADE). The lesson is now written into the contract
docstring as an explicit scope rule: the contract asserts only what every
MV-capable database has; everything else (aliases, CASCADE, TABLESPACE, storage
parameters, refresh scheduling) is a per-backend feature asserted in that
backend's own suite.

Verified on ten backends: bigquery 5 passed, postgres 84 passed, oracle 12 passed,
snowflake 5 passed, the rest skip on capability.

(cherry picked from commit 1de74c4)
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
13.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@vistart vistart self-assigned this Sep 27, 2026
@vistart vistart added the enhancement New feature or request label Sep 27, 2026
@vistart
vistart merged commit ad914ef into release/v1.0.0.dev17 Sep 27, 2026
1 of 2 checks passed
@vistart
vistart deleted the feature/activerecord-derived-ddl branch October 1, 2026 03:53
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