Skip to content

feat(sqlserver): adapt to derived DDL with typed options and strict DDL rejection - #4

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

vistart merged 38 commits into
release/v1.0.0.dev3from
feature/activerecord-derived-ddl

Conversation

@vistart

@vistart vistart commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adapts the SQL Server backend to the derived-DDL model in
rhosocial/python-activerecord#121, with the emphasis on refusing to emit SQL the server
would reject.

Previously the backend accepted a declared option and then dropped it, so a schema the
application asked for could silently differ from what was created. That is now an error:

  • Declared table and column comments raise instead of being silently dropped.
  • Unsupported table declarations are rejected rather than omitted.
  • Declared DDL options are no longer discarded.

Alongside that, options become typed rather than free-form:

  • SQLServerColumnDefinition with typed column options and backend identity syntax.
  • SQLServerCreateTableOptions, with the memory-optimized tests migrated onto it.
  • SQLServerIndexDefinition with typed hash-index fields.
  • Typed ALTER COLUMN options, a graph table expression, and typed MERGE / SELECT INTO
    / partition scheme, plus an explicit signature on SQLServerSelectIntoExpression for
    round-tripping.

CREATE VIEW ... WITH CHECK OPTION is gated on supports_view_check_option, type DDL
support is added, and CommentSupport is classified for SQL Server conformance.
SQLServerDatabaseMixin is wired into the dialect, which retires the need to construct the
dialect directly in tests. Dead code goes: a vestigial format_cte, the reserved
dialect_options bag on data-type value objects and partition clauses, and inherited
Firebird clause parameters that no longer belong on the create-table subclass.

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 API changes. One deliberate behaviour change does affect
applications: a declared table or column comment, or an unsupported table declaration, now
raises instead of being silently ignored. Code that relied on the option being dropped will
start failing — which is the point, since the created schema was not what was asked for.

Impact on Backend Developers (Custom Implementations):

  • The reserved dialect_options bag is removed from data-type value objects and partition
    clauses, and inherited Firebird clause parameters are gone from the create-table subclass.
    Anything reading or writing them must move to a declared option or capability bit.
  • Column classes drop the legacy identity fields in favour of the typed comment clause
    nodes from core.
  • SQLServerDatabaseMixin is now wired into the dialect, so tests construct
    SQLServerDialect directly instead of assembling it from mixins.
  • format_cte is removed as dead code.
  • TYPE_CHECKING-only imports that pointed at modules moved by the expression namespace
    split were repaired so type checkers resolve them.

Related Repositories/Packages

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 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.

Merged or in flight, and still to merge in the same sequence:

Repository PR Feature tip
python-activerecord-mysql #56 91b89e0
python-activerecord-postgres #58 f7dbc39
python-activerecord-mariadb #5 4b2ca8a
python-activerecord-sqlserver this PR 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 ff98cf0 and covers SQL Server 2017, 2019, 2022 and
2025 on Python 3.9 through 3.14, including free-threaded builds on 2019 and 2022. All nine
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

Two commits are housekeeping rather than adapter work 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,
    fix(backend): repair unresolvable TYPE_CHECKING imports and chore(test): register the pytest markers the suite already uses — mechanical repairs required for the hook, type
    checking and lint to run at all after the namespace split.

chore(sqlserver): align pull request template and docs(sqlserver): drop empty dialect_options from examples are documentation-only and affect no runtime behaviour.

Introduce the SQL Server-specific column definition (and options) derived
from the generic ColumnDefinition / ColumnOptions, carrying typed SPARSE /
ROWGUIDCOL attributes with no dialect_options bag. The SQL Server
format_column_definition accepts both the generic and SQL Server column
definitions and renders the SQL Server-only attributes. Export the new
classes and cover them with tests.
Introduce SQLServerCreateTableOptions (deriving the generic CreateTableOptions)
carrying the SQL Server-only MEMORY_OPTIMIZED / DURABILITY options. The SQL
Server CREATE TABLE formatter reads these typed fields instead of the
dialect_options bag. Graph table kind / edge constraints remain for a later
CreateTableExpression-specific pass.
…leOptions

The SQL Server formatter now reads typed memory_optimized/durability instead
of the dialect_options bag.
…ex fields

Move hash_index/bucket_count off the dialect_options bag onto a typed
SQLServerIndexDefinition subclass read by format_index_definition.
…ssion

Add SQLServerAlterColumn (data_type/collate/not_null/sparse/masked_function/
drop_masked) and SQLServerCreateTableExpression (graph_table_kind/
edge_constraints), replacing the dialect_options bag reads.
Add SQLServerMergeExpression (output/output_action/holdlock) and
SQLServerSelectIntoExpression (select_into_table); give
SQLServerPartitionByRangeClause a typed partition_scheme field.
…roundtrip

The subclass used *args/**kwargs, so introspection-based get_params()
dropped 'select' and serialization round-trip failed to reconstruct the
query. Mirror QueryExpression's explicit parameter list so get_params()
reflects every constructor argument.
…e-table subclass

The generic CreateTableExpression no longer carries on_commit_delete /
external_file; stop threading them through the SQL Server subclass.
Core DataType no longer carries a dialect_options bag; backend type
subclasses stop forwarding it. Update the type-protocol tests.
…heck_option

The view formatter rendered WITH CHECK OPTION unconditionally, leaving the
capability bit inert.
It called a non-existent super().format_cte and had no callers; CTE
formatting goes through format_cte_expression now.
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.
…lently dropping

SQL Server has no inline comment mechanism (its native mechanism is
sp_addextendedproperty, not implemented); a declared comment raises
UnsupportedFeatureError instead of being silently dropped.
- CREATE INDEX IF NOT EXISTS / TABLESPACE now raise
- CREATE SEQUENCE IF NOT EXISTS / OWNED BY now raise
- unsupported inline-index options now raise
- multi-action ALTER TABLE emits one statement per action
- CREATE TABLE IF NOT EXISTS renders the native IF OBJECT_ID guard
- advertise the gated ALTER action capabilities
Verify SQL Server 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 8e12401, 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 66.14509% with 238 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.36%. Comparing base (8e12401) to head (339ea11).

Files with missing lines Patch % Lines
...verecord/backend/impl/sqlserver/mixins/ddl_type.py 57.68% 86 Missing and 60 partials ⚠️
...cord/backend/impl/sqlserver/expression/ddl/type.py 70.53% 17 Missing and 16 partials ⚠️
...ial/activerecord/backend/impl/sqlserver/dialect.py 63.09% 17 Missing and 14 partials ⚠️
...iverecord/backend/impl/sqlserver/protocols/type.py 63.15% 7 Missing ⚠️
...tiverecord/backend/impl/sqlserver/async_backend.py 55.55% 2 Missing and 2 partials ⚠️
...ial/activerecord/backend/impl/sqlserver/backend.py 55.55% 3 Missing and 1 partial ⚠️
...ord/backend/impl/sqlserver/mixins/backend_mixin.py 0.00% 4 Missing ⚠️
...cord/backend/impl/sqlserver/functions/aggregate.py 0.00% 1 Missing ⚠️
...record/backend/impl/sqlserver/functions/bitwise.py 0.00% 1 Missing ⚠️
...ecord/backend/impl/sqlserver/functions/datetime.py 0.00% 1 Missing ⚠️
... and 6 more
Additional details and impacted files
@@                   Coverage Diff                   @@
##           release/v1.0.0.dev3       #4      +/-   ##
=======================================================
- Coverage                73.98%   73.36%   -0.63%     
=======================================================
  Files                      124      133       +9     
  Lines                     6715     7332     +617     
  Branches                  1019     1171     +152     
=======================================================
+ Hits                      4968     5379     +411     
- Misses                    1432     1548     +116     
- Partials                   315      405      +90     
Flag Coverage Δ
python-3.14 73.36% <66.14%> (-0.63%) ⬇️
sqlserver-2025 73.36% <66.14%> (-0.63%) ⬇️
unittests 73.36% <66.14%> (-0.63%) ⬇️

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 029b5ba into release/v1.0.0.dev3 Sep 27, 2026
10 of 13 checks passed
@vistart
vistart deleted the feature/activerecord-derived-ddl branch October 1, 2026 03:52
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