Skip to content

feat(mysql): adapt to derived DDL with typed table and column options - #56

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

vistart merged 43 commits into
release/v1.0.0.dev20from
feature/activerecord-derived-ddl

Conversation

@vistart

@vistart vistart commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adapts the MySQL backend to the derived-DDL model in rhosocial/python-activerecord#121,
which derives DDL from model declarations instead of carrying a dialect-specific option
bag on every expression.

The central change is replacing dialect_options with typed, declared options. A dialect now
declares what it supports and the generic formatters consume that declaration:

  • MySQLColumnDefinition and typed column options, including backend-specific identity
    syntax.
  • MySQLCreateTableOptions with typed AUTO_INCREMENT and ROW_FORMAT, read from the
    generic declaration path.
  • A version-aware charset/collation/storage-engine protocol with validation, with the
    collation whitelist moved into the dialect mixin.
  • A partition lifecycle provider, with partition definitions derived from the generic base
    declarations and subpartition names validated at construction.
  • MySQLInsertExpression carrying typed IGNORE/REPLACE, and MySQLAddColumn with a
    typed after field.

Capability bits are wired through so unsupported forms fail loudly rather than emitting SQL
the server rejects: supports_view_check_option gates CREATE VIEW ... WITH CHECK OPTION,
supports_column_comment and the typed comment clauses replace the legacy identity fields,
and TABLESPACE / INHERITS are no longer silently dropped.

Two contracts are added on top: a type-domain negative contract, and coverage for
DDLSource collection, table inheritance, tablespace and inline-index rendering from derived
DDL.

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. The public model and query APIs are unchanged.

Impact on End-Users:

No backward-incompatible changes are expected for typical end-user applications. Table
options are now declared through typed options rather than a free-form bag, so DDL is
generated from the same declarations the ORM exposes.

Impact on Backend Developers (Custom Implementations):

Relevant to anyone extending or forking this backend rather than consuming it:

  • The reserved dialect_options bag is removed from data-type value objects, DDL/admin
    expressions, and partition definitions. Anything reading or writing it must move to a
    declared option or a capability bit.
  • MySQLDatabaseMixin is wired into the dialect, and MySQLTableSupport declares
    format_index_definition while inline-index capability is inherited from core, so the
    formatting entry point is unified rather than per-backend.
  • Column classes drop the legacy identity fields in favour of the typed comment clause
    nodes.
  • TYPE_CHECKING-only imports that pointed at modules moved by the namespace split were
    repaired so type checkers resolve them.

Related Repositories/Packages

This is the MySQL half of the derived-DDL work. The core half is
rhosocial/python-activerecord#121 (merged into release/v1.0.0.dev30) and the shared
contract half is rhosocial/python-activerecord-testsuite#21 (merged into
release/v1.0.0.dev17).

This PR also repoints this repository's workflow at those release branches. Both
dependencies were previously pinned to feature/activerecord-derived-ddl, a branch that
only exists to carry the derived-DDL work; leaving the old ref in place would have made the
first run after that branch is deleted fail to resolve.

The remaining eight backends carry the same adaptation and the same workflow repoint, and
should be merged in the same sequence before the feature branches are deleted:

Repository Feature tip
python-activerecord-postgres 4e9d1f4
python-activerecord-mariadb a5f9a9f
python-activerecord-sqlserver ff98cf0
python-activerecord-oracle 0870faf
python-activerecord-clickhouse 03c6d8c
python-activerecord-firebird 0c57328
python-activerecord-snowflake 9e7e282
python-activerecord-bigquery 4d2e9f0

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.

Local full-suite execution was skipped at the user's instruction. Verification relied on
remote CI, which ran on this exact tip cbd1470 and covers the version matrix against
MySQL 5.6, 5.7, 8.0, 8.4, 9.0, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6 and 9.7, on Python 3.8
through 3.15 including free-threaded builds, plus a benchmarks job. All sixteen jobs pass.

That run also covers the workflow change in this PR: it is the first execution against
release/v1.0.0.dev30 and release/v1.0.0.dev17 rather than the feature branches, so the
new dependency refs are verified to resolve.

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

Three commits in this branch are not part of the DDL adaptation and are called out so a
reviewer is not surprised:

  • ci: install core and testsuite from release branches — the workflow repoint described
    above, a prerequisite for merging.
  • fix(scripts): stop the pre-commit hook from blocking commits on older revisions — a
    repository-hygiene fix, kept identical across all ten repositories so they do not drift.
  • fix(backend): repair unresolvable TYPE_CHECKING imports and chore(test): register the pytest markers the suite already uses — mechanical repairs required for type checking
    and lint to run at all after the namespace split.

…formatting

- Drop the private supports_inline_index/format_inline_index declarations
  from MySQLTableSupport; the capability now comes from the core TableSupport
  protocol (default False, MySQL overrides True)
- Converge inline index rendering onto the core format_index_definition
  override (returns (sql, params)); remove the parallel method
- Point CI at the core feature branch that carries the protocol
…rations

MySQLPartitionDefinition / MySQLSubpartitionDefinition now derive from the
generic PartitionDefinition / SubpartitionDefinition base declarations,
keeping MySQL-specific boundary validation (requires exactly one boundary).
The generic SubpartitionDefinition base now validates the name in
__post_init__, so MySQLSubpartitionDefinition rejects an empty name at
construction time rather than in the formatter.
Introduce the MySQL-specific column definition derived from the generic
ColumnDefinition, carrying typed CHARACTER SET / COLUMN_FORMAT / STORAGE /
INVISIBLE attributes (no dialect_options bag). MySQLColumnOptions declares
them and directs the deriver to build a MySQLColumnDefinition.

MySQL format_column_definition now accepts both the generic and MySQL column
definition and renders the MySQL-only attributes. Export the new classes and
cover them with tests.
Introduce MySQLCreateTableOptions (deriving the generic CreateTableOptions)
carrying the MySQL-only ENGINE / CHARSET / table COLLATE options with no
dialect_options bag. The MySQL CREATE TABLE formatter reads these typed
fields and the generic comment field, dropping the bag fallbacks.
The MySQL formatter now reads typed table_options.comment instead of the
dialect_options bag; update the comment and engine/charset/collate tests
accordingly.
Move the ADD COLUMN ... AFTER option off the dialect_options bag onto a typed
MySQLAddColumn subclass read by the MysqlColumnMixin formatter.
The MySQL INSERT IGNORE / REPLACE variants move off the dialect_options bag
onto a typed MySQLInsertExpression read by the dialect.
…ressions

Remove the unused/reserved dialect_options parameter, attribute, pass-through
arguments and docs from MySQL statement expressions (admin/utility, table
maintenance, routine, rename table, table value statements, load XML).
The data-type family (expression/types.py) keeps its own dialect_options.
Core DataType no longer carries a dialect_options bag; backend type
subclasses stop forwarding it. Update the type-protocol tests.
…_option

The view formatter rendered WITH CHECK OPTION unconditionally, leaving the
capability bit inert.
…+ validation

Add MySQLCharsetCollationSupport protocol and MySQLCharsetCollationMixin
implementing a version-filtered whitelist (MySQLCharset, MySQLStorageEngine,
MySQLCollation) plus validate_* helpers. MySQLCreateTableOptions now
validates engine/charset/collate at construction instead of storing arbitrary
strings. Fold the expression-level COLLATE handling into the mixin and drop
the overlapping mixins/collation.py.
Fold the MySQLCollation enum and version gates into
MySQLCharsetCollationMixin; remove the standalone collation.py module and
the free validator functions. Collation/charset/engine validation is now
exclusively a dialect protocol-implementation concern.
Add auto_increment (positive int) and row_format (MySQLRowFormat) typed
fields to MySQLCreateTableOptions with validation, rendered by the MySQL
create-table formatter.
The database DDL mixin existed but was not included in MySQLDialect, so
CreateDatabaseExpression failed with a missing-formatting-method error.
Remove identity/identity_start/identity_increment/identity_clause from
XxxColumnDefinition and XxxColumnOptions to match core's column-attribute
migration; XxxColumnDefinition now forwards attributes.
Render the dialect-free column attributes channel in each backend's
format_column_definition (it was silently dropped), and give backends whose
identity syntax differs from the SQL-standard form a format_identity_clause
override (MySQL/MariaDB AUTO_INCREMENT, SQL Server IDENTITY(seed, inc)).
Oracle gates column COLLATE to 12.2+; MariaDB drops the duplicate dialect
formatter so the attribute-aware mixin is authoritative.
Core added the ColumnAttributeSupport protocol and renamed the deriver plan
entry to creation_plan(); update protocol conformance and derived-DDL tests.
Inline table/column comments are now TableCommentClause / ColumnCommentClause
nodes; render them through format_table_comment_clause /
format_column_comment_clause. Tests updated to build the clause nodes.
Model base already defaults to "id"; fixtures should exercise the unspecified-PK default path.
Verify MySQL DDLSource declarations, backend types, column options, and dialect rendering.
Expression classes moved under backend.expression/ and several were renamed
during the derived-DDL refactor, leaving relative-import depths and forward
references in TYPE_CHECKING blocks pointing at modules and classes that no
longer exist. Type checkers and any runtime evaluation of those blocks fail
to resolve them.

Verified statically: every TYPE_CHECKING import in the touched files now
resolves to an existing module and an existing top-level symbol, and no
string annotation references a name absent from the workspace.
The suite applies markers that were never declared in
[tool.pytest.ini_options] markers. pytest only warns for an unknown marker, so
the warning scrolls past unnoticed and a dropped or misspelt registration is
silent.

Registers the ones in use, with descriptions that say what they do: collection
grouping, not ordering. `feature` labels feature-area tests and
`benchmark_relation` marks relation benchmarks; neither affects scheduling, so
neither substitutes for the xdist_group marker that actually serialises a suite.

usefixtures needs no entry and is not added: pytest provides it, so it was a
false positive in the isolation audit rather than a missing registration.
… revisions

The installed hook exec'd scripts/preflight.sh unconditionally. That hook is a
local, untracked artifact, so it survives a checkout of any commit that predates
the script, and on such a commit exec failed with a bare "No such file or
directory" — blocking the commit with nothing to act on.

In this repository scripts/preflight.sh first appears at 4980455, so every earlier
commit is affected. The failure is also silent in a dangerous way: it swallowed
a `git commit --amend`, so a commit-message rewrite reported as applied when it
had not been, and had to be redone.

The hook now checks the script is executable and exits 0 otherwise, so a revision
without the pre-flight simply skips it. Normal operation is unchanged.

Also honours core.hooksPath. git ignores .git/hooks entirely once that is set, so
the hook was being written to a location that would never run while this script
still reported success. No repository sets it today, so that part is latent.
Both dependencies were pinned to feature/activerecord-derived-ddl, a branch
that exists only to carry the derived-DDL work. Now that the core change has
landed in release/v1.0.0.dev30 and the matching testsuite change in
release/v1.0.0.dev17, pinning to the feature branch means CI keeps resolving a
branch that will be deleted once the backend PRs merge.

This is a prerequisite for merging: leaving the old ref in place means the
first run after that branch disappears fails to resolve, not the code.
@codecov

codecov Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.93285% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.41%. Comparing base (4980455) to head (91b89e0).

Files with missing lines Patch % Lines
...ord/backend/impl/mysql/mixins/charset_collation.py 79.31% 26 Missing and 4 partials ⚠️
...ocial/activerecord/backend/impl/mysql/protocols.py 56.75% 16 Missing ⚠️
...ctiverecord/backend/impl/mysql/mixins/partition.py 79.16% 3 Missing and 2 partials ⚠️
...tiverecord/backend/impl/mysql/expression/column.py 90.90% 2 Missing and 2 partials ⚠️
...ord/backend/impl/mysql/expression/table_options.py 87.87% 2 Missing and 2 partials ⚠️
...erecord/backend/impl/mysql/expression/partition.py 91.66% 1 Missing and 1 partial ⚠️
...ctiverecord/backend/impl/mysql/mixins/ddl_table.py 95.65% 1 Missing and 1 partial ⚠️
...tiverecord/backend/impl/mysql/async_transaction.py 0.00% 1 Missing ⚠️
...tiverecord/backend/impl/mysql/mixins/ddl_column.py 75.00% 1 Missing ⚠️
...activerecord/backend/impl/mysql/mixins/ddl_view.py 66.66% 0 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@                   Coverage Diff                    @@
##           release/v1.0.0.dev20      #56      +/-   ##
========================================================
+ Coverage                 70.34%   70.41%   +0.06%     
========================================================
  Files                       106      110       +4     
  Lines                      9733    10028     +295     
  Branches                   1393     1432      +39     
========================================================
+ Hits                       6847     7061     +214     
- Misses                     2435     2500      +65     
- Partials                    451      467      +16     
Flag Coverage Δ
mysql-9.6 70.41% <83.93%> (+0.06%) ⬆️
python-3.14t 70.41% <83.93%> (+0.06%) ⬆️
unittests 70.41% <83.93%> (+0.06%) ⬆️

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.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@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 a2c36a2 into release/v1.0.0.dev20 Sep 27, 2026
19 of 20 checks passed
@vistart
vistart deleted the feature/activerecord-derived-ddl branch October 1, 2026 03:50
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