Skip to content

feat(sqlserver)!: the catalogue as statements, and the extended-property family - #9

Merged
vistart merged 61 commits into
release/v1.0.0.dev3from
fix/column-constraint-gates
Oct 31, 2026
Merged

vistart merged 61 commits into
release/v1.0.0.dev3from
fix/column-constraint-gates

Conversation

@vistart

@vistart vistart commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Description

Lands fix/column-constraint-gates on release/v1.0.0.dev3.

Scope up front: this branch carries the whole cycle started on 2026-10-11 as it reaches this backend — the statement/expression refactor, the typed literals, and the introspection pass at the tip — not only the last few days of fixes. 59 commits, 6 of them marked breaking.

Headline changes:

  1. Extended properties have a CLI entry, and they reach the column and table comment fields. introspect extended-properties TABLE asks by table name, and the table and column reads join sys.extended_properties so MS_Description is where the type says it belongs (feat(introspection): the extended properties, asked by table name).
  2. A view's definition is read where the catalogue keeps it: the list selects VIEW_DEFINITION and IS_UPDATABLE, and views NAME answers for one view (fix(introspection): a view's definition is read where the catalogue keeps it).
  3. A trigger says what fires it and whether it is on: events comes from sys.trigger_events and status from is_disabled, instead of [] and a constant (fix(introspection): a trigger says what fires it and whether it is on).
  4. A system table is not a base table, and the introspect CLI offers twelve types derived from _TYPE_CAPABILITIES (feat(introspection): a system table is not a base table).

Type of change

  • feat: A new feature
  • refactor: Internal restructuring with no intended behaviour change
  • test: Adding missing tests or correcting existing tests
  • docs: Documentation only
  • perf / chore / ci / build / revert (not relevant)

Breaking Change

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

Impact on End-Users:

  • introspect extended-properties TABLE is new, and introspect columns / tables now carry comments.
  • introspect table is gone; views NAME answers for one view with its definition.

Impact on Backend Developers (Custom Implementations):

  • The backend-only catalogue questions (schemas, routines, sequences, extended properties) are statement classes; parse_* lost its leading underscores.
  • column_info_statement() gained include_hidden, defaulted to False.

Related Repositories/Packages

repository branch tip its role in this cycle
python-activerecord 57439841 the statement layer, the capability vocabulary, and pytest_compat
python-activerecord-testsuite 1991449 the shared contracts, and the two pytest 9.2 shims
python-activerecord-sqlserver d992c02 this branch

Core is merged (rhosocial/python-activerecord#127, squashed as 57439841); this branch's CI now installs core from release/v1.0.0.dev30. The test suite is still referenced by branch, because its pytest_compat shims are not on its release branch yet; that reference is repointed when the test suite merges.

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 across the Python versions the matrix declares.

Test Plan: The full matrix ran green on this exact head (d992c02) as run 41092206630 — every Python version and every service container the backend tests against. A local full run is skipped deliberately: the organisation caps a single task at 180 seconds, and the CI result on this SHA is the stronger evidence. The offline CLI tests (parser surface, NAME rules, capability gate) run in under a second and are part of that run.

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, the CLI guide).
  • My changes generate no new warnings.
  • I have added new tests to cover my changes.
  • I have added a Changelog fragment.
  • I have verified that my changes do not introduce SQL injection vulnerabilities (no statement is spelled outside a dialect format_*).
  • I have checked for potential performance regressions.

Additional Notes

  • No rebase, and no divergence to reconcile. The release branch is an ancestor of this branch (0 behind, everything ahead), so this merges forward with nothing to resolve. The release ruleset requires linear history, so squash is the intended strategy.
  • Review load is front-loaded. Most commits are the statement/expression refactor reaching this backend; the introspection and CLI work is the last dozen.
  • The changelog fragments are in this branch, named after this pull request's number (added once the number existed, in a follow-up commit).

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.
… with getattr

Each of these sits behind a parameter whose annotation names a class, and each
field is in that class's constructor signature. The IndexDefinition and Index
reads in particular: those attributes are what the class was built to carry, so
the default was a claim that a well-formed declaration might arrive without
them, which never happened.

Verified by running the backend suite against both the changed tree and the
tip it came from: the same 24 failures either way, all of them needing a live
SQL Server. 562 tests pass on the type and DDL modules that exercise these
paths without a server.
Wave 1 of the backend conformance sweep: W2 + W3a + W3b + W1, mechanical
renames only. 71 files, no behaviour change.

Statement family (W2).  Core split "an expression that happens to be a
statement" from "a statement", so the class names moved with it:

  QueryExpression              -> QueryStatement
  Insert/Update/Delete         -> Insert/Update/DeleteStatement
  MergeExpression              -> MergeStatement
  SetOperationExpression       -> SetOperationStatement
  BeginTransactionExpression   -> BeginTransactionStatement
  CommitTransactionExpression  -> CommitStatement        (no "Transaction")
  RollbackTransactionExpression -> RollbackStatement
  Savepoint/ReleaseSavepointExpression -> ...SavepointStatement
  Create/Alter/Drop {Table,Index,View,Schema,Sequence,Type,Function,
  Trigger,Domain,FulltextIndex,PropertyGraph,MaterializedView,
  Database}Expression          -> ...Statement
  CreateTableAs/Like/CloneExpression -> ...Statement
  TruncateExpression           -> TruncateStatement
  CommentOnExpression          -> CommentOnStatement
  ExplainExpression            -> ExplainStatement
  RefreshMaterializedViewExpression -> RefreshMaterializedViewStatement

Functional formatter renames (renamed because an override that keeps the old
name stops being an override and silently falls back to core's spelling):

  format_set_operation_expression -> format_set_operation_statement
  format_begin_transaction       -> format_begin_transaction_statement
  format_commit_transaction      -> format_commit_transaction_statement
  format_rollback_transaction    -> format_rollback_transaction_statement
  format_savepoint               -> format_savepoint_statement
  format_release_savepoint       -> format_release_savepoint_statement

CTE family (W1):

  CTEExpression         -> CTEClause
  WithQueryExpression   -> WithStatement
  format_cte_expression -> format_cte_clause
  main_query=           -> query=

Value family (W3b): SQLValueExpression -> SQLValue, the eight
{JSON,Binary,XML,UUID,Numeric,Integer,String,Boolean}ValueExpression ->
*Value, BooleanLogicExpression -> LogicalValue, QualifiedIdentifierExpression
-> QualifiedIdentifier, JSONDocument/JSONTextExpression -> *Value,
ArrayValueExpression -> ArrayValue, WildcardExpression -> AllColumnsWildcard
(its two call sites are select-list positions; the COUNT side is untouched).

Function family (W3b): Cast/Case/Trim/Repeat/Lpad/Rpad/CollateExpression ->
*Function, UUIDCastExpression -> UUIDCastFunction.

Predicate family (W3a): Any/All/Exists/ILIKE/XMLExistsExpression ->
*Predicate, FilterClauseExpression -> FilterClause, FulltextMatchExpression ->
FulltextMatch.

Introspection family (W3b): the Introspection prefix is added, keeping the
Info suffix the old guide table dropped:

  DatabaseInfo/TableInfo/ColumnInfo/ViewInfo/TriggerInfo/ForeignKeyInfo
    -> Introspection*Info

  IndexInfo and IndexColumnInfo keep their names -- verified against
  backend/introspection/types.py, which is what core actually ships.

Not in this commit, by design: the constraint surfaces.  Core's S3 wave
deleted ColumnConstraintType / TableConstraintType /
normalize_{column,table}_constraint_type, and 21 modules in this package
import one of them, so the package does not import yet.  Those surfaces
follow in the next commit.

Verification:

- import smoke over every module in the backend package: 102 imported,
  21 failing (was 69 ok / 24 failing).  All 21 remaining failures are the
  deleted constraint enums; none is a rename miss.
- rg over src + tests: zero residual references to any W1/W2/W3 old name
  and zero to the renamed formatters.
- ruff check on the 71 changed files: 185 findings, identical to the HEAD
  baseline for the same file set (compared per file and per rule).  No new
  finding of any class.

Known gap, and not this repository's to fix: the shared testsuite
feature/ddl contract still imports the deleted ColumnConstraintType, so it
cannot run until the testsuite agent finishes S3.
Core's S3 put the kind inside the clause: ``ColumnConstraint`` and
``TableConstraint`` are productions that carry a **branch**, and the branch is
a node in its own right. ``ColumnConstraintType`` / ``TableConstraintType`` are
gone, and with them ``normalize_column_constraint_type`` /
``normalize_table_constraint_type`` / ``_call_constraint_capability``. There is
no tag left to switch on; every call site now names the alternative it means.

Construction sites migrated (28 files; src 13, tests 15):

    ColumnConstraint(d, ColumnConstraintType.PRIMARY_KEY, is_auto_increment=True)
      -> ColumnConstraint(d, branch=ColumnPrimaryKeyConstraint(d, is_auto_increment=True))
    ColumnConstraint(d, ..., check_condition=p)   -> branch=ColumnCheckConstraint(d, p)
    ColumnConstraint(d, ..., default_value=v)     -> branch=ColumnDefaultConstraint(d, v)
    ColumnConstraint(d, ..., foreign_key_reference=(t, cols)) ->
        branch=ColumnForeignKeyConstraint(d, (t, cols))
    NOT_NULL / NULL / UNIQUE tags ->
        branch=ColumnNotNullConstraint / ColumnNullableConstraint / ColumnUniqueConstraint
    TableConstraint(d, TableConstraintType.PRIMARY_KEY, columns=[...]) ->
        TableConstraint(d, branch=TablePrimaryKeyConstraint(d, [...]))
    GeneratedColumnExpression -> ColumnGeneratedConstraint

The deleted ``normalize_*`` calls at ``dialect.py`` (both the column and the
table half) were the first thing removed: they were imports of symbols core had
already deleted, so the module did not import before this change. No residual
reference to any deleted name remains in ``src`` or ``tests``.

Override points, re-cut against the new API:

* ``SQLServerDialect.format_column_definition`` no longer walks a tag switch.
  PRIMARY KEY and UNIQUE are still spelled here, because ``NONCLUSTERED`` rides
  the clause and the branch formatter is dispatched with a single argument, so
  there is no way to hand it the flag without smuggling it in as dialect state.
  ``IDENTITY(1,1)`` now comes off the PRIMARY KEY **branch**'s own
  ``is_auto_increment`` flag. Everything else (NOT NULL, NULL, DEFAULT, CHECK,
  GENERATED, the column REFERENCES that T-SQL refuses) goes through core's
  production formatter.

  **A defect this commit fixes while it is here.** The first draft of this
  migration let the PK/UNIQUE shortcut bypass ``_refuse_constraint_traits``,
  which meant an explicitly requested ``ENFORCED`` / ``DEFERRABLE`` on a PRIMARY
  KEY or UNIQUE column constraint was **silently dropped** -- the exact failure
  the old ``format_column_definition`` refused with a ``ValueError``. The
  refusal now runs first and unconditionally, before either shortcut or the
  delegation, so no path can drop a clause T-SQL does not parse.

* ``SQLServerDialect.format_table_constraint`` keeps the two reasons it exists:
  the memory-optimized spellings (``PRIMARY KEY NONCLUSTERED`` /
  ``UNIQUE NONCLUSTERED``) and the trait refusals. The branch class says which
  kind this is; the ``CONSTRAINT <name>`` prefix, the branch bodies for CHECK
  and FOREIGN KEY, and the REFERENCES clause with its referential actions are
  core's, reached through ``super()``.

* ``SQLServerTypeDDLMixin`` (``mixins/ddl_type.py``) reads the branch class in
  all four places it used to read the tag: the memory-optimized column and
  table allow-lists, the key-constraint count for the index limit, and the
  "needs a key constraint" check. The old per-kind ``ValueError`` guards for
  cross-wired fields (a key with a check condition, a CHECK with columns) are
  gone with the fat node -- the branch cannot hold a field its kind does not
  have, so those shapes are now unrepresentable rather than checked.

* ``SQLServerConstraintMixin`` (``mixins/constraint.py``) gains the narrowing
  seams and the measured evidence. The trait refusals live at the two public
  seams: ``format_column_constraint`` for the column level and
  ``SQLServerDialect.format_table_constraint`` for the table level, both going
  through one ``_refuse_constraint_traits`` helper so the two cannot drift
  apart. ``format_column_fk_constraint`` refuses the column-level ``REFERENCES``
  clause, which is the one real narrowing: core gates it on
  ``supports_foreign_key_constraint``, the *table-level* probe, and SQL Server
  needs that probe to be True for the table form, so the probe cannot be
  flipped to turn the column level off -- the coupling is recorded in the
  docstring.

  The probes are ``(self)``: ``supports_add_constraint`` / ``supports_drop_constraint``
  are True by measurement, and ``supports_constraint_enforced`` /
  ``supports_deferrable_constraint`` / ``supports_alter_constraint_enforced`` /
  ``supports_validate_constraint`` / ``supports_constraint_novalidate`` /
  ``supports_enable_disable_constraint`` / ``supports_rename_constraint`` /
  ``supports_fk_match`` are False. Every docstring quotes the statement and the
  server's own message number (Msg 102 / 156 for the parser refusals, 3728 /
  3727 / 15248 / 15225 / 15249 for the ALTER-side ones), measured on SQL Server
  2019 15.0.4465.1 at 127.0.0.1:11433. Wave 3 folds this in rather than
  splitting it, because the probes, the ``_require_*``-style narrowing and the
  formatters that call them are one unit (wave guide section 5, detail 3); the
  evidence and its guard test are what the next commit adds.

* ``alter_table_modifier.py`` records the live measurement behind the two
  ``IF EXISTS`` switches, and says which of the two is a documentation claim
  rather than a measurement.

Not in this commit, by design: the roundtrip matrix's LIKE/ILIKE and
``xml.PassingClause`` rows, which are the wildcard-split follow-ups and belong
with the wildcard wave (the same split mysql made between ``3d643d6`` and
``a611b65``).

Verification (no live SQL Server in this environment -- Docker is not running,
so 11433/11435 refuse connections; every check below is render-level, and the
measured values come from the draft's recorded server observations):

- import smoke over the backend package: 140 modules imported, 0 failures
  (examples excluded -- they dial a server at import time).
- testsuite shared feature/ddl contract (core worktree): exit 0.
- test_clause_pair_guard_sqlserver.py + test_ddl_source_mixin.py +
  test_master_probes_guard_sqlserver.py: 170 passed, exit 0.
- test_expression_roundtrip_all.py: 782 passed, 8 skipped, 4 failed -- the four
  are the LIKE/ILIKE/PASSING rows deferred to the wildcard commit above, not
  S3 regressions.
- ruff over the 28 changed files: 111 findings (E402 69, F401 36, F541 2,
  F811 3, E501 1), against the same files' 112 at the parent commit (E402 69,
  F401 36, F541 2, F811 3, E501 2). One fewer E501, no new finding of any class.
The code side of the constraint-capability reconciliation went in with the S3
migration (`906d31b`, wave guide section 5 detail 3: the probe signatures, the
narrowing and the formatters that call them are one unit, so splitting them
would put a commit on record that contradicts core's API). This commit is the
half that reads as evidence: the module of guards that keeps every declaration
tied to the statement which decided it, plus the live measurement recorded
behind the two ALTER-side ``IF EXISTS`` switches.

New: ``test_sqlserver_constraint_capabilities.py`` walks one table of ten
switches and asserts four separate things.

* **the value** -- what ``sqlserver_2019`` (15.0.4465.1) was measured to do, and
  that 2019 / 2022 / 2025 agree, so no value is a version gate.
* **the locality** -- each switch resolves to a class in *this* package, with an
  MRO-precedence assertion that the answering class precedes core's
  ``ConstraintMixin``. A switch that fell through to core could have its default
  moved by a commit in another repository with nothing failing here.
* **the signature** -- ``(self)`` and nothing else, checked on the bound method
  so a defaulted parameter cannot slip back in. A probe answers one question and
  the kind is not part of it.
* **the evidence** -- each switch's docstring still quotes the server's own
  message number (Msg 102 / 156 for the parser refusals; 3728 / 3727 / 15248 /
  15225 / 15249 for the ALTER-side ones) and names the release it was measured
  on. A verdict that cannot be traced to an observation is a guess.

The render-path tail is asserted through the **two public seams**, so a seam
that stopped consulting the shared ``_refuse_constraint_traits`` helper fails
here rather than quietly dropping a clause:

* column level, ``ColumnDefinition`` path -- the column-level ``REFERENCES``
  refusal (and the table-level probe that stays True, which is why the refusal
  needs an override rather than a probe), plus the six trait spellings on a
  PRIMARY KEY / UNIQUE column. That last pair is the defect ``906d31b`` fixed:
  those two kinds are spelled inside ``format_column_definition`` rather than
  through the production formatter, so they are the path that would drop a
  request without a word.
* table level, ``TableConstraint`` path -- the same six spellings on a table
  PRIMARY KEY, which is a kind that can carry both traits. The node's own
  construction-time refusal of the deferral trait on a CHECK is pinned
  separately so the two layers stay distinguishable.
* the ALTER side -- ``VALIDATE`` / ``ALTER CONSTRAINT`` (both spellings) /
  ``NOT VALID`` refused by name, ``ADD`` / ``DROP CONSTRAINT`` and the
  ``IF EXISTS`` idempotent form rendered.

``alter_table_modifier.py`` gains the measured evidence for its two switches
without changing either value: ``DROP CONSTRAINT IF EXISTS`` was measured
accepted as a silent no-op on 2019 (the plain form is Msg 3728 followed by
3727), while the "since 2016" floor on both switches is a documentation claim
carried rather than a measurement, and says so where it is stated.

Verification (no live SQL Server in this environment -- Docker is not running,
so 11433 / 11434 / 11435 refuse connections; the values come from the draft's
recorded server observations and the guards pin them):

- test_sqlserver_constraint_capabilities.py: 80 passed, exit 0.
- with test_master_probes_guard_sqlserver.py and
  test_clause_pair_guard_sqlserver.py: 207 passed, exit 0.
- ruff over the two changed files: clean.
Core split the single asterisk in 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, which is why they are separate nodes
and not one node with a flag. There was no intermediate ``Wildcard`` to pass
through, so each call site moves straight to its position's node.

Call sites migrated by position, not by name (2 files; src 1, tests 2):

    select-list / RETURNING   WildcardExpression -> AllColumnsWildcard
      tests/.../test_backend_explain.py, tests/.../dialect/test_returning_output.py

Both are the select-list / RETURNING positions, so both take the
all-columns node. No COUNT argument position is built through an expression
node in this backend -- the ``COUNT(*)`` occurrences that remain are raw SQL
strings handed to ``backend.execute`` (``protocol_support.py``'s OUTPUT helper
and the examples), which are not expression nodes and need no migration.

**No backend-specific wildcard class is warranted here**, and the reason is the
one the guide gives for keeping it out of core: ``COUNT(t.*)`` is a
postgreSQL / snowflake affair (a different row-count question under a join),
and SQL Server's own asterisk spelling is ``OUTPUT inserted.*`` / ``deleted.*``
-- a plain string in the OUTPUT column list, not a wildcard node.  SQL Server's
``format_column_definition`` / ``format_returning_clause`` build their
asterisk-bearing fragments from strings, so there is nothing to derive.

No ``format_wildcard`` override existed here (``WildcardSupport`` is inherited
from the core protocol, and the dialect's two wildcard formatters come from
``ExpressionMixin``), so none needed renaming.  The one thing the split did
change in this package is the refusal in ``format_returning_clause``: the
comment claimed both wildcard classes were refused by name while only the
all-columns one was checked.  ``RowCountWildcard`` now shares the refusal, so an
asterisk in an OUTPUT list is named as such instead of falling through to the
generic "only column references" message.

Verification (no live SQL Server in this environment -- Docker is not running,
so 11433 / 11434 / 11435 refuse connections; these are render-level checks):

- import smoke over the backend package: 140 modules imported, 0 failures
  (examples excluded -- they dial a server at import time).
- test_backend_explain.py + dialect/test_returning_output.py: 12 passed,
  7 skipped, exit 0.
- ruff over the three changed files: clean for returning.py and
  dialect/test_returning_output.py; test_backend_explain.py keeps the five F401
  findings it already had at the parent commit (pytest, pytest_asyncio, Column,
  Literal, WhereClause -- none introduced here).
…ip matrix

The roundtrip matrix walks every expression class core and this package define,
so two of the wildcard-split's consequences land here rather than in the
refactor commits.

**LIKE / ILIKE need dedicated constructors.** Core's commit calls it out:
``LikePredicate``'s operator slot is annotated ``str`` and its two operands are
string forward references, while ``ILikePredicate``'s column is annotated
``Any`` -- so the introspective guess hands each of them a plain string and the
instance cannot render (``'str' object has no attribute 'to_sql'``).
``LikePredicate`` is worse than that: the operator is a *parameter*, so the
guess's ``"x"`` would land in the operator slot. Both factories are the ones
core registers in its own dummy2 matrix -- ``LikePredicate(d, "LIKE",
Column(d, "a"), Literal(d, "%x%"))`` and ``ILikePredicate(d, Column(d, "a"),
"%x%")`` -- so the matrices agree on the shape.

**``xml.PassingClause`` joins the declared gaps.** The S1-c clause rides the
SQL/XML querying expressions, and SQL Server declares no ``format_xml*`` method
at all, so the clause's own ``format_passing_clause`` is absent too and the
dispatch refuses it exactly like every other ``xml.*`` row. It is the same
declared gap, recorded in the same entry with the same reason, so the
``test_unmodelled_formatter_list_is_exact`` walk stays exact in both directions.

S3's own matrix side -- the ``ColumnClause`` / ``ColumnConstraintBranch`` /
``TableConstraintBranch`` bases and the ``_TemporalValue`` rename -- went in
with the S3 migration (``906d31b``), because the table has to name the new bases
in the same commit that introduces them.

This is the last piece the guide's wave 6 asks for before pushing: CI runs
``pytest tests/ -n auto -x``, so the first error stops the session and the
matrix has to be green in full, not just on the changed classes.

Verification (no live SQL Server in this environment -- Docker is not running,
so 11433 / 11434 / 11435 refuse connections; the matrix is render-level):

- test_expression_roundtrip_all.py, full file: 786 passed, 8 skipped, exit 0.
- the three new registrations are collected and asserted, not skipped:
  test_to_sql_roundtrip_classified and test_get_params_roundtrip each pick up
  ILikePredicate, LikePredicate and PassingClause (6 cases).
- TestMatrixIntegrity: 8 passed, exit 0 -- both the declared-gap table and the
  unmodelled-formatter table are exact in both directions after the additions.
- ruff over the one changed file: 6 findings, identical to the same file at the
  parent commit (pre-existing F401 / F811; none introduced here).
The shared testsuite contract moved onto the same branch as core and this
backend -- its S3 adaptation, the predicate-placeholder fix and the W2/W3
renames all landed on ``fix/column-constraint-gates`` -- so the CI workflow no
longer installs it from ``release/v1.0.0.dev17``.  Every install line in
``.github/workflows/test.yml`` now names ``@fix/column-constraint-gates`` for
the testsuite, matching the core line above it, which was already pointing at
the branch.

Nine occurrences, all inside the nine ``Install activerecord and testsuite from
PyPI`` steps: the testsuite is installed once per matrix leg (the synchronous,
asynchronous, version-specific and CLI legs).

No test logic changes; the diff is the branch pin alone.

Verification:
- ``rg 'rhosocial-activerecord-testsuite .* @Release' .github/workflows/`` --
  no match remains.
- ``rg 'rhosocial-activerecord-testsuite .* @fix/column-constraint-gates'`` --
  nine matches, one per install step.
- The core install line still names ``@fix/column-constraint-gates`` in all
  nine steps as well, so the two packages come from the same branch.
Carry the W3c class renames, the R1 signature alignment and the R2a arithmetic/concatenation renaming through the backend sources and tests. Set operations take the required op word instead of the retired all_/distinct pair, so the clause-pair guard no longer models them; DDL default values move to the inline-strategy literal class.
The core removed the FQN-keyed deserialization index tonight (kind-registry
rewrite, core 8d474dc8/303dae60): register_expression_class,
registered_expression_classes and auto_register_builtins no longer exist,
which left this backend unimportable against the new baseline.

Migrate the type-DDL node assembly to the new API:
- expression/ddl/type.py registers its seven classes through
  default_registry.register_node (registry.py's process-level instance,
  the assembly point its own docstring gives to dialect packages; the kind
  is derived from the class name, e.g. sql_server_alias_type_definition).
- test_type_expressions_are_explicitly_registered follows: the
  registration assertion resolves each kind through the process registry,
  and the write side is asserted on the bare {kind, params} artifact. The
  three-channel deserialize leg is gone by design: the framework's
  default read whitelist now admits only the packages built into the core
  distribution (_FRAMEWORK_PACKAGES), so reading a backend distribution's
  kinds is a reader-side decision that the upcoming backend registry wave
  will settle; inventing that reader here would chase a moving core.

This is baseline tracking, not one of the four guided waves; it is a
prerequisite for any of them to be tested at all.

Evidence: backend + dialect import OK; targeted run
test_sqlserver_type_ddl.py: 27 passed, 1 skipped in 0.66s (ruff clean).
…attr

Clear every getattr/hasattr probe on an expression receiver in src (form A
direct reads and form B class dispatches, per expression-dialect 11.2 and
the S2 precedent 101f71a5). The acceptance scan
rg '(getattr|hasattr)\(\s*(expr|node|clause|item)\b' src now matches nothing.

21 sites, by file:
- dialect.py (8): table_options.comment read directly off the declared
  options object (None-guarded; the field is on CreateTableOptions);
  edge_constraints / graph_table_kind via isinstance dispatch on
  SQLServerCreateTableExpression (those fields belong to that node, not to
  the generic statement); index_type direct read (declared Optional[str] on
  the core CreateIndexStatement); MERGE output/output_action/holdlock via
  isinstance dispatch on SQLServerMergeExpression; func_name direct read
  (declared on the core FunctionCall).
- mixins/ddl_type.py (5): DROP TYPE cascade/restrict via isinstance
  dispatch on SQLServerDropTypeExpression; definition_kind (twice) and
  action_kind read directly (abstract properties on the TypeDefinition /
  TypeAlterAction ABCs, so every concrete subclass declares them).
- mixins/ddl_view.py (2): options.schemabinding / options.check_option
  read directly (fields of the ViewOptions dataclass; CreateViewStatement
  normalizes options to a non-None instance, so the old None-guard went).
- mixins/partition.py (1): partition_scheme via isinstance dispatch on
  SQLServerPartitionByRangeClause (the generic PartitionClause carries no
  such field).
- mixins/protocol_support.py (1): select_into_table read directly
  (declared on SQLServerSelectIntoExpression).
- mixins/routine.py (1) and mixins/trigger.py (3): _get_routine_name /
  _get_trigger_name read the declared name; create-trigger refuses the
  SQL:1999 statement by class (UnsupportedFeatureError, unchanged) instead
  of probing for body; trigger table read directly.

Behavior tightening (recorded in the roundtrip matrix, committed with this
wave as instructed): the shadowed core CreateFunctionStatement and
DropTriggerStatement carry no name field, so rendering them through this
backend's formatters now fails with AttributeError "has no attribute
'name'" where the probes used to manufacture an empty name and raise
ValueError. Both LEGITIMATE_NON_RENDERS rows are updated to pin the
AttributeError and its message; the matrix module itself currently cannot
execute against tonight's core baseline (the testsuite round-trip harness
it depends on was deleted in testsuite e05edb0), so the two rows were
verified by reading the flow, not by running the file.

Evidence (targeted, with an empty local scenario set so the session
autouse fixture does not spend ~93s dialing three closed ports):
test_sqlserver_type_ddl.py 27 passed 1 skipped;
test_p2_coverage_features.py 63 passed;
test_sqlserver_partition.py 21 passed;
test_sqlserver_ddl_improvements.py 9 passed;
test_p1_coverage_features.py -k "SelectInto or Tablesample" 10 passed;
test_dialect_comprehensive.py -k "TestSQLServerDialectMerge or
TestSQLServerDialectDDL" 4 passed.
ruff on the seven changed src files and the matrix file reports the same
25 pre-existing findings as HEAD (17+1+1+6), zero introduced by this wave.
Correction to da73e38: the process-level default registry that
deserialization actually consults is serialization.default_registry(), the
lazy singleton the artifact reader resolves kinds through. Registering into
registry.default_registry (the bare instance) kept the import fix but fed a
table no read path consults yet, and left the deserialization leg of the
type-DDL coverage untestable.

Follow the same target the mysql harness settled on (mysql 8961ea1): the
classes -- and the module's enum, which an artifact encodes under its short
class name -- go into default_registry(), where re-registering a class the
framework already populated is a no-op and the backend's kinds are the
addition.

The three-channel round-trip leg of
test_type_expressions_are_explicitly_registered is restored accordingly
and passes through the default path again.

Evidence: test_sqlserver_type_ddl.py 27 passed, 1 skipped in 0.39s
(exit 0, ruff clean on both files).
…spellings

Wire the core TypedValueMixin into the dialect and spell the kinds where
T-SQL's grammar disagrees with the portable defaults (guide wave V, per
core 473b8e37 + 5ba491d5 + 1549380b; implementation mirrors the SQLite
reference mixin and the in-flight PostgreSQL one).

New mixins/typed_value.py, composed ahead of SQLDialectBase in the dialect
base list (that position is what makes the intercept win):
- format_binary_value: 0x0102, the unquoted hex constant (data position
  still binds bytes; only the DDL position spells).
- format_uuid_value: the bare quoted canonical text -- UNIQUEIDENTIFIER
  converts string literals; no cast syntax to add.
- format_interval_value: deliberately absent. T-SQL has no interval
  literal, so the core mixin's explicit NotImplementedError is the
  shipped answer; the wiring test pins that resolution.
- format_literal: the boolean branch is intercepted to 1/0, because
  T-SQL's literal grammar has no TRUE/FALSE keywords -- unquoted TRUE
  parses as an identifier, and BIT's constants are numeric. This is the
  tier-1 audit's outcome: the default spelling does NOT apply here, so
  the single spelling point (format_literal) is overridden, which keeps
  the typed node's inline branch, raw-scalar DEFAULT positions and CHECK
  conditions in agreement; the data position is untouched (pyodbc binds
  True/False to BIT natively). Quoted ISO dates and the other tier-1
  kinds were audited against the documented T-SQL grammar and keep the
  base spelling.

Not yet server-verified: the scenario ports (11433/11434/11435) were
refused all session (~31 s per dial), so the spellings rest on the
documented grammar and the module header says so; the live round-trip
class (sentinel + CREATE TABLE with embedded DEFAULTs + INSERT ... DEFAULT
VALUES read-back, following the core execution_testing discipline) rides
for CI's live scenarios. Follow-up: measure the docstring answers against
a reachable server and record the versions.

New test_sqlserver_typed_values.py pins both positions of every spelled
kind -- the DDL position through test-local subclasses declaring
inline_params = True (the same class-level strategy flip DdlLiteral applies
to Literal; the core has shipped no DDL variants of the value nodes and a
dialect must not invent nodes) -- plus the wiring (MRO resolution, the
interval refusal, the format_literal intercept) and the live class.

Evidence (offline, empty local scenario set): the new file -k "not Live"
15 passed, 2 deselected, exit 0; feature/backend -k "(typed or literal)
and not Live" 28 passed, 5 skipped, exit 0 (the two harness-importing
roundtrip files excluded from collection -- wave IV vendors the retired
testsuite harness, mysql 8961ea1 precedent). ruff clean on the new files;
dialect.py carries the same 17 pre-existing findings as HEAD.
… VALUES cells

Follow the core waves this backend had not digested (guide wave M1, per core
9b559030 + c5210245 + 8e2e4d82):

Classed entries -- the standard functions no longer ride FunctionCall:
- functions/aggregate.py: max_/min_/avg build MaxFunction / MinFunction /
  AvgFunction, whose kinds dispatch to their own formatters.
- functions/utility.py: coalesce builds CoalesceFunction; the factory's
  argument conversion (a bare string names a column, a number becomes a
  literal) is kept, so only the node changed.
- functions/string.py: trim builds TrimExpression, which fixes a real bug:
  the old two-argument FunctionCall("TRIM", [expr, chars]) rendered
  TRIM(expr, chars) -- a shape T-SQL does not have -- where the node's
  formatter spells TRIM(chars FROM expr), the form SQL Server 2017+ parses.
- format_char_length_function is overridden on the dialect body and spells
  LEN: SQL Server counts UTF-16 code units and excludes trailing blanks,
  which is why the core docstring names this backend as needing the
  override. It lives on the body (not a late mixin) because the core method
  rides into the MRO early through ExpressionMixin -> FunctionCallMixin,
  ahead of every SQL Server mixin block; a test pins that resolution.
- mixins/datetime.py: NOW / CURRENT_DATE / CURRENT_TIME keep their T-SQL
  spellings through the classed formatters format_now_function /
  format_current_date_function / format_current_time_function
  (SYSDATETIME() / CAST(GETDATE() AS DATE) / CAST(GETDATE() AS TIME)).
  A precision argument has no T-SQL spelling on any of the three, so it is
  refused explicitly instead of dropped. CURRENT_TIMESTAMP keeps the
  standard niladic keyword the core already spells.
- dialect.py: the _NILADIC_FUNCTION_EQUIVALENTS and
  _FUNCTION_NAME_EQUIVALENTS tables and the format_function_call override
  are deleted -- every entry in them was a classed function's rename, and
  FunctionCall is the UDF escape hatch now, rendering through the core
  default. The now-unused _clone_with_func_name helper and copy import go
  with them.

Bare VALUES cells -- mixins/dml.py's INSERT override delegates its VALUES
branch to the core's format_insert_values_source, which owns the per-cell
decision (core 8e2e4d82): an expression renders its own SQL, a bare host
value becomes a bind parameter. The hand-rolled loop used to call
val.to_sql() unconditionally, so a bare scalar cell failed with
AttributeError instead of binding. The T-SQL OUTPUT / DEFAULT VALUES /
SELECT-source paths are unchanged.

New test_classed_function_entries.py pins all of it offline: the classed
factories, the LEN spelling and its MRO resolution, the niladic spellings
and their precision refusals, the trim node's operand order, the kept
coalesce conversion, the UDF passthrough, and INSERT VALUES rows mixing
bare and expression cells.

Evidence (offline, empty local scenario set):
test_classed_function_entries.py 19 passed;
test_functions.py + test_dialect_function_support.py 40 passed;
test_dialect_comprehensive.py -k "TestSQLServerDialectDML or
TestSQLServerDialectMerge" 6 passed;
test_p1_coverage_features.py + test_p2_coverage_features.py 118 passed.
ruff: the changed files introduce no new findings (dialect.py keeps the
same 17 pre-existing ones as HEAD; datetime.py keeps its 1).
Core replaced the TypedValueMixin and the twelve *Value classes with a single
Literal(dialect, value, inline=) node and folded the tier-2 spellings into
format_literal (239678e5, "give a literal its job and nothing else"). Wave 2's
committed TypedValueMixin form therefore no longer imports; this is that
form's replacement, not a revert.

* mixins/literal.py replaces mixins/typed_value.py: SQLServerLiteralValueMixin
  overrides format_literal and claims exactly the three types whose inline
  spelling differs from the portable ones -- bool as 1/0 (T-SQL's literal
  grammar has no TRUE/FALSE keywords and BIT's constants are numeric), bytes
  as the unquoted 0x hexadecimal constant, UUID as the quoted canonical text a
  UNIQUEIDENTIFIER converts. Everything else falls through to
  SQLDialectBase.format_literal, which is where the three-level construction,
  the escape helper and the finite-number guard live; the escaping is not
  copied here.
* INTERVAL is deliberately absent. T-SQL has no interval type and no interval
  literal, so a timedelta at an inline position reaches the base's ladder and
  fails loudly naming its type -- the structural absence is the answer, and
  the mixin's docstring says so.
* The mixin is the dialect's first base, ahead of SQLDialectBase: a mixin
  placed after the class it overrides never wins. The old position (after the
  core's TypedValueMixin, which no longer exists) is gone with it.
* The wave-2 test file follows: the DDL-position subclasses that flipped
  inline_params per class are replaced by Literal(..., inline=True), the
  inline flag now being the caller's per-node decision. The data-position
  assertions, the tier-1 spellings, the MRO pin and the live round-trip
  class are unchanged in substance; the live cases' server-measured notes are
  carried over as written (not server-verified: the scenario servers were
  unreachable this session, so CI's live scenarios exercise that class).

Verified: import smoke (dialect imports, format_literal(True) == "1",
format_literal(b"\x01\x02") == "0x0102"); test_sqlserver_typed_values.py
12 passed, 6 live cases deselected (no reachable server); ruff clean on the
new and changed files.
…enames the core settled

Four core-side waves reached this backend's node contracts and had not been
followed through here; each is a reading of a field the node declares, not a
probe.

* ``SQLServerTable`` named its identity parameters through a ``**slots``
  catch-all. ``get_params()`` now reads every constructor parameter under its
  own name, so a name a ``**kwargs`` accepted without declaring cannot travel
  in an artifact: the parameters are declared and passed through, and the
  docstring says why they are named.
* ``mixins/collation.py`` read ``expr.collation_options`` -- a catch-all bag the
  core node no longer has. ``CollateFunction`` declares ``collation`` (a string
  or an enum member) and ``schema``, so the mixin reads those two and unwraps a
  member through its value; the schema-qualified refusal that used to be one
  key of the bag stays a refusal.
* ``SQLServerCollation`` becomes a ``(str, Enum)`` with ``__str__ = str.__str__``.
  A member rides inside ``CollateFunction``'s params, and a param that travels
  in a plain-JSON artifact must be a scalar: a plain ``Enum`` is the one thing
  the serializer names and refuses. MySQL and ClickHouse spell their enums the
  same way.
* ``SQLServerTypeNullability``'s registration is gone with the enum
  sub-registry: the class normalises the declaration to a bool at construction,
  so no member ever reaches an artifact, and ``register_enum`` itself no longer
  exists in core.
* The action renames ("name an action by what it is"): ``AlterColumn`` ->
  ``AlterColumnAction`` in ``expression/alter_column.py`` and the
  ``examples/schema_diff/add_column.py`` that constructs it, and the
  ``AddColumn`` / ``AddTableConstraint`` / ``AlterConstraint`` /
  ``DropTableConstraint`` / ``ValidateConstraint`` call sites in the tests that
  imported the retired spellings.
* ``CollateFunction`` no longer takes a ``keyword`` option (the options bag is
  gone), so ``DATABASE_DEFAULT`` is passed as the plain collation name it
  always was, and the keyword-refusal case becomes the name-refusal case the
  whitelist gives: one validator, one message.
* ``SetTransactionExpression.isolation_level`` was renamed
  ``with_isolation_level``; the transaction test follows.
* Value nodes no longer carry an operation surface, so a collation off a
  ``Literal`` goes through the module-level ``collate`` factory, and the
  type-definition CHECK builds its condition as the ``ComparisonPredicate``
  node with a ``ComparisonOperator`` member rather than as an operator between
  two values.

Verified: test_collation_expression.py, test_alter_table_if_exists_backend.py,
test_sqlserver_constraint_capabilities.py, test_dialect_comprehensive.py,
test_table_type_definition_roundtrip.py -- 5 files, 410 passed, 5 skipped (the
skipped ones need a reachable SQL Server, which this repository has none of;
the literal round-trip class is offline-pinned in the next wave's matrix).
The lineage audit (core's ``scripts/expression_lineage.py``, run against a
temporary copy that registers this backend's package and dialect class -- the
script reads the running code, and adding a backend to it is a core-repo change,
reported rather than made here) settles two facts:

* **SQL Server overrides no kind core ships**: 0 overrides, 58 customs. That is
  the healthy shape -- a kind pointing at two classes is the one defect the
  audit exists to find, because deserialization could not then decide which
  class a name means. Every SQL Server kind is a name core has never issued, and
  every one of them starts with the backend's name, as the rule requires.
* The **custom kinds spelled the backend's name as two words**:
  ``sql_server_bit_type``, ``sql_server_table``, ... The generic
  consecutive-capital rule reads ``SQLServer`` as ``SQL`` + ``Server``, which is
  the same defect SQLite had (``sq_lite_blob_type``) and MySQL and ClickHouse
  cleaned out (``my_sql_*``, ``click_house_*``). The fix is the same *data* fix
  they used: ``KIND_RESERVED = ("SQLServer",)`` on every node in this package.
  No class overrides ``kind()``, so one naming rule holds for the whole backend
  and the kinds now read ``sqlserver_bit_type``, ``sqlserver_table``,
  ``sqlserver_partition_by_range_clause`` -- parallel to core's
  ``sqlite_integer_type`` / ``mysql_big_int_type`` form, with the ``_type``
  suffix kept on both sides of the core/backend seam.

Three of the backend's own enums ride inside those nodes' parameters, and an
enum parameter is the other thing the audit caught: ``SQLServerGraphDirection``,
``SQLServerGraphTableKind`` and ``SQLServerPartitionRangeDirection`` were plain
``Enum`` members, and a plain enum is the one thing a plain-JSON artifact cannot
carry -- the serializer names the type and stops. Each becomes a ``(str, Enum)``
with ``__str__ = str.__str__`` (the spelling both MySQL and ClickHouse use), and
each node's constructor normalises what it is handed: an artifact hands back the
word the enum's value is, the formatter reads a member, and one spelling lives
inside the node either way. The refusal for an unknown word names the closed set
instead of guessing.

Verified: the audit re-run now reports 0 overrides / 58 customs, all of them
``sqlserver_*``; the round-trip matrix, the graph and partition renderers and
the DDL type tests all pass against the new kinds (the matrix resolves every
class it covers through the kind whitelist in both directions).
…rtifact

Reconciled against core HEAD 550ae740. Three things had drifted, and each is
what the matrix is for: the shared harness it imported no longer exists, the
artifact stopped carrying markers, and the columns it classifies changed shape.

* **The harness is this repository's own now.** The testsuite dropped
  ``utils/expression`` in ``e05edb0`` with "round-trip coverage moves to the
  owning repository", which left ``testsuite.utils.parse_contract`` -- and with
  it the type-protocol sweep -- importing a deleted module. Both are vendored
  beside this repo's own copy (``expression_roundtrip_harness.py``,
  ``expression_parse_contract.py``), with the drift fixed deliberately rather
  than re-imported: the harness's ``assert_params_equal`` reads an enum member
  through its *value* (a plain-JSON artifact carries the value, and a
  constructor may hand back either the member or the word) and tolerates a
  tuple that came back as an array.
* **The artifact is bare ``{kind, params}``.** The enum and dataclass
  sub-registries the matrix filled existed only to resolve ``__enum__`` /
  ``__dataclass__`` markers; both are gone, so those registrations go with them
  and the matrix says why. ``DdlLiteral`` is gone too: a DDL position passes
  ``inline=True`` to the one ``Literal`` class instead, and the two test files
  that used the old spelling follow.
* **The columns are re-derived.** ``values.*`` left with the typed-value nodes
  and ``Literal`` came in with the sixteen value kinds, so the matrix now pins
  every kind's artifact shape as a flat JSON scalar -- including the two kinds
  for NaN and +-Infinity, whose payloads travel as words precisely so they
  never reach SQL text. The declared-result annotations, core's generic PIVOT /
  UNPIVOT (which render here now), the ``format_edge_table`` seam, and the three
  pad/repeat methods are re-decided class by class: renders, pinned non-render
  with the exception type and message fragment, or unmodelled formatter with
  the feature that is absent.
* **What the artifact cannot carry is named, not swallowed.** A node whose
  parameter is a plain ``Enum`` or a dataclass cannot be written as JSON at all
  -- the serializer names the type and stops -- and one channel can fail where
  the other two succeed: the XML codec spells an enum member as ``Class.MEMBER``
  and no reader turns that back into a member since the enum sub-registry went.
  :data:`ARTIFACT_GAPS` records each such class by channel, type and message
  fragment, and the matrix pins that table in both directions: a class that
  starts travelling fails until its row goes, and a class that stops travelling
  with no row fails the per-class assertion. The rows that are *this* backend's
  fault (``SQLServerGraphDirection``, ``SQLServerGraphTableKind``,
  ``SQLServerPartitionRangeDirection``, ``SQLServerCollation``) were fixed in
  the kind audit; the rest are core-side and are recorded with the channel so
  the day core spells them, the rows go.
* Constructors replace guesses that cost a real assertion: a
  ``ComparisonPredicate`` whose operator is a member rather than a word the
  constructor normalises, CHECK / generated constraints comparing two columns
  (a DDL condition may not carry a bind parameter), a foreign key whose
  referential actions are words, and the ``inline=True`` literal the DDL
  position now wants.

The matrix runs 955 cases: 951 pass, 4 skip -- the skips are the two XML
spelling rows' per-channel assertions, which are pinned in ARTIFACT_GAPS rather
than passed.
The org test rules (expression-dialect §12) ask for three things of the
expression area: no real connection in an expression test, parallel by
default, and a directory the rules can name. This commit makes all three true,
and the third one first -- the rules talk about "the expression test
directory", which did not exist here until now.

* ``feature/backend/expression/`` is that directory. It holds the round-trip
  matrix, the literal spellings test, the SQL Server expression-formatting
  tests, and the two harness modules the matrix and the type sweep load. The
  live round-trip class -- the one that creates a table, inserts a default row
  and reads it back -- leaves the literal test file and becomes
  ``feature/backend/test_literal_default_execution.py``, beside the
  scenario-driven integration tests, which is where a live connection belongs.
  Its docstring still says what was written and that no reachable server has
  confirmed it.
* Two test files outside the area load the harness modules. They now load them
  by path -- the same technique the type-protocol file already used for the
  matrix -- so a move inside the tree does not turn a passing import into a
  NameError, and neither file depends on a sys.path entry the run did not
  promise.
* ``tests/plugins/expression_xdist.py`` is the parallel default. xdist reads
  the worker count before any conftest loads, so a conftest cannot inject
  ``-n``; the plugin is registered from ``pytest.ini`` with ``-p`` and appends
  ``-n auto --dist=loadgroup`` only when every argument names a path inside the
  expression area. A whole-repository run, a mixed run, and any other directory
  keep the serial default; an explicit ``-n`` is never overridden, and
  ``--dist=loadscope/loadfile`` are refused outright because they bind whole
  modules together and hide case-to-case dependencies. The plugin enables
  nothing at all when xdist is not installed.
* ``pytest.ini`` also sets ``pythonpath = tests``. That is load-bearing, not
  decoration: a ``-p`` plugin is imported *by name*, and pytest applies
  ``pythonpath`` immediately before it imports those plugins -- without it the
  registration only works for an invocation that already put ``tests`` on
  ``PYTHONPATH`` (as CI does, and as a local run often does not). With it a
  bare ``pytest`` works too. ``pytest-xdist`` joins the dev dependencies, since
  the plugin's narrowness includes declining to install itself.

Verified: the plugin's scoping was checked case by case -- expression directory
and single file inside it get ``-n auto --dist=loadgroup``; explicit ``-n 4``
keeps its count and gains ``loadgroup``; explicit ``loadscope`` is refused;
the parent directory and a mixed run are left alone. End to end, the matrix now
runs under the plugin as `created: 16/16 workers`, `16 workers [955 items]`,
951 passed and 4 pinned skips -- the same verdicts as the serial run, so the
parallel default changed nothing about what is asserted.
The harness moved into the expression area with the tests that use it, and
two of its callers load the *test module* by path rather than letting pytest
collect it: the type-protocol sweep imports the matrix to read its registry,
and the matrix and the parse contract import their sibling harness. A
by-name import only resolves when the sibling's directory happens to be on
``sys.path`` -- true for a collected module, false for one loaded by path --
so the move turned a passing import into ``ModuleNotFoundError`` on that path.

Loading the sibling by path is what the type-protocol file already did for the
matrix; the same technique now covers the harness, from both the matrix and the
parse contract, and each module keeps one identity no matter which caller
reaches it first.

Verified: the type-protocol file and the table-type round-trip file -- 221
passed, 3 skipped (the sweep covers the whole type registry through the
vendored parse contract); the matrix -- 951 passed, 4 pinned skips.
…its live half

The previous commit cut this file at the first decorative rule it found instead
of at the live section's own header, which took the offline assertions with it.
Restored from the version the matrix commit holds, and the split is now made at
the section that actually moved: the file keeps the data-position bindings, the
inline spellings and the interval refusal, its docstring points at the live
half's new home instead of describing cases it no longer contains, and the
``execution_testing`` import those cases used goes with them.

Verified: 12 offline cases pass (the wiring pin, the five data-position
bindings, the six inline spellings), and the live file collects its two
scenario-parameterised cases beside the integration tests.
The FOR UPDATE refusal read ``clause.strength.value`` -- the artifact word --
so the message said "does not support no_key_update", which is a serialization
spelling nobody reads SQL in. Core split the word from the SQL text when it
made the comparison operator an enum (the artifact carries the word, the phrase
carries what the clause says), and the same split applies here: the phrase is
what a refusal names. The UPDATE branch is unaffected -- SQL Server spells it
as table hints and never prints the strength.

Verified: each strength's message now reads the phrase (FOR NO KEY UPDATE, FOR
SHARE, FOR KEY SHARE, LOCK IN SHARE MODE) and the UPDATE branch still renders
``WITH (UPDLOCK, ROWLOCK)``; the dialect test files that exercise the clause
pass.
The live leg of the literal spellings ran for the first time in CI, on nine
legs, and each leg's only failure was this one assertion: the server hands a
``UNIQUEIDENTIFIER`` back in its own canonical spelling, which is upper-case
hex, and comparing it as text against a ``uuid.UUID`` -- whose ``str`` is the
lower-case form -- failed on the casing rather than on the value. The answer is
parsed instead of compared as text, so what travels is the value.

Everything else in the run is green: the round-trip matrix, the value-kind
table, the kind registry and the testsuite integration pass on all nine legs.
The plain-JSON reconciliation left six names behind in the matrix: the kind
audit turned the partition enums into words and the artifact stops naming
classes at all, so `Tuple`/`Type` from typing, `Function` from the
catalogue objects and the three sibling partition node classes have no reader
left in the file. Ruff flags them (F401 twice, F811 once for `Type`
shadowed by the objects import, F401 three times in the partition
constructor), and the lint gate is part of the finishing steps.

Verified: the matrix plus the table-type round-trip file re-run green after
the removal (951 passed, 4 pinned skips in the matrix; 8 passed in the
table-type file), and ruff on the expression area, the plugin, the live
execution file and the two harness callers is clean.
The LIKE family's operator became an enum (core f2bdf386): the parameter is
LikeOperator, the keyword is a spelling the dialect owns, and the constructor
coerces words and keywords alike (core 4774c502) -- but the wire form is the
member. These call sites still passed the SQL keyword as a string, so they
built whichever shape the coercion happened to produce; they now pass
LikeOperator.LIKE, which is what the artifact carries.

Verified: the LIKE predicate renders on this backend's dialect through the new
enum (left LIKE placeholder, the pattern bound); ruff on the touched files
matches the repository baseline.
Every catalogue question this backend asked was spelled as text: nine
``SELECT`` statements inline in ``introspection/introspector.py``, twenty in
``introspection/status_introspector.py`` (ten shapes, twice over for the sync
and async halves), and the probes in ``backend/backend.py``,
``backend/async_backend.py``, ``mixins/backend_mixin.py`` and
``mixins/concurrency.py``. A SQL string is a formatting artefact, so it
belongs to the dialect formatter, not to the layer that executes the query
(org guide 16.2): the node collects the parameters, the formatter writes the
text, and the caller only constructs and executes.

New ``query_nodes.py`` states each construction once -- a column reference, a
value, a catalogue relation, a conjunction, a projection into a SELECT -- and
both families of caller use it: ``SQLServerNodeBuilderMixin`` for the
introspectors, which reach their dialect through the core introspector mixin,
and the module-level functions for the backend classes, which do not share a
base with them. All twelve schema builders and all eight status queries are
now ``QueryStatement`` trees over ``INFORMATION_SCHEMA`` / ``sys`` relations,
joined with ``JoinClause`` and filtered with ``ComparisonPredicate``,
``InPredicate``, ``IsNullPredicate`` and ``LogicalPredicate``.

Baseline diff (twelve ``_build_*_sql`` shapes, captured before and after):

  - projection order, aliases and parameter tuples are identical in every
    one of the twelve; the only differences are the dialect's spelling
    (``[TABLE_NAME]`` for ``TABLE_NAME``, ``[INFORMATION_SCHEMA].[TABLES]``
    for ``INFORMATION_SCHEMA.TABLES``) and the parenthesisation the join
    node adds around a left-deep chain, which is the same tree.
  - ``COLUMNPROPERTY(OBJECT_ID(...))`` keeps its 3-argument shape; the
    catalog's one-string object name is an arithmetic ``add`` because SQL
    Server spells concatenation with ``+``.

Values kept where the text kept them: ``'BASE TABLE'``, ``'PRIMARY KEY'``,
``'IsIdentity'``, ``'dbo'``, ``'max worker threads'``, ``'user connections'``,
``'sysadmin'`` and the ``IN ('S','U','G')`` list stay inline literals, so
callers keep the parameter tuples they already had; schema and table names
stay bound.

One behavioural point, stated rather than hidden: the performance-counter
query capped its rows with ``SELECT TOP 50``, which has no node -- TOP lives
in the SELECT head and this dialect spells it ``format_top_n_clause``, not a
clause the query statement carries. The cap is now the pagination node
(``OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY``), which returns the same rows in
the same order but needs 2012+. The limit is therefore chosen against the
dialect version: ``None`` before 2012, where the old statement worked and the
new spelling would raise and be swallowed into an empty report. Both shapes
are the dialect's; the choice between them is this backend's.

The SHOW module builds no SQL and never did: it is the inbound half of the
MySQL vocabulary -- MySQL's row names translated into the typed results the
shared ``show()`` API returns. It keeps that translation and its docstrings
now say so, because the deliverable there is a row shape, not a statement.

Offline evidence: ``test_introspection_sql_builders.py`` re-asserts the
parameterization against the rendered statement (26 shape/parse tests) and
gains ten status-query tests covering each projection, the pagination cap and
its 2012 gate, and the niladic ``SYSTEM_USER``. All 36 pass. Live-database
legs are not runnable here (no local SQL Server): they go to CI.
SQL Server has no CREATE TABLE ... AS statement. Its population spelling
is SELECT ... INTO <table>, which is a different statement shape, so the
portable one cannot be rendered in a form the server accepts.

The dialect inherited supports_create_table_as() from core, where it
answers True, so the framework rendered the portable spelling and the
server refused it -- "Incorrect syntax near the keyword 'SELECT'", with
the statement rejected whole. Measured on SQL Server 2022 (port 11434):

  CREATE TABLE [t] AS SELECT 1 AS c              rejected
  CREATE TABLE [t] AS SELECT 1 AS c WITH DATA    rejected
  SELECT 1 AS c INTO [t]                         accepted

The probe is now declared here and answers False, so the request is
refused by the statement name instead of rendered in a form the server
rejects.

The guard file's sentinel -- "a genuinely inherited probe must not pass
the check" -- used supports_create_table_as as its example and said this
backend "renders SQL Server CTAS through SELECT INTO instead". It does
not: no SELECT INTO renderer exists here, so the portable form is what
was being rendered. The sentinel now uses supports_create_table_clone,
which is still inherited, so the check stays able to fail.

The two CTAS entries in the load-bearing walk are gone with it: with
CTAS refused as a statement, the WITH [NO] DATA gate has no consumer
for this dialect, exactly as materialized view create and refresh
already had none. A clause-level refusal for a statement that does not
exist would be a fiction. The measurement stays declared and is pinned
by its own case, so it cannot drift.
The registry says a row goes "the day core spells it as the value", and
that day has come: core spells an enum parameter as its own value on
every channel now, so the XML codec no longer carries ``Class.MEMBER``
where the constructor wants the word. Eleven rows were pinned on the XML
channel for exactly that -- the EXTRACT family, the interval arithmetic
and the two XML document-type expressions -- and every one of them was
reporting that the channel had started travelling:

  ARTIFACT_GAPS pins ValueError on 'xml' but the xml channel carried the
  node -- the row is stale

Twelve cases were failing on this branch before this change: the eleven
rows above and the integrity check that the registry matches reality.
The rows are gone, so the classes assert normally on all three channels.

Nothing here is about SQL Server's SQL. It is the registry catching up
with a core-side change, which is what the file's own rule asks for --
"a class that starts travelling fails until its row goes".

The comment above the registry is rewritten to say what happened rather
than describe a gap that no longer exists: the XML channel used to spell
an enum parameter as ``Class.MEMBER`` while the dict and JSON channels
spelled it as the value, and the rows recorded that until core closed
it.
The introspect subcommand asked get_tables/get_views/get_columns (and the
rest of the get_* family), which exist nowhere in the backend: core spells
those questions list_tables/list_views/list_columns. Every advertised type
therefore failed against a live server while the offline fakes answered the
misspelled names. The type-to-method mapping now lives in _steps alone, and
is guarded twice: the offline dispatch test checks every type against the
real introspector classes, and a live test runs the command against SQL
Server. table and database <name> render through get_table_info, whose one
query already fills columns, indexes and foreign keys; triggers no longer
iterate every table swallowing each one's error, since list_triggers takes
an optional table.

Alongside the fix, the catalogue questions core has no shape for get their
own methods on both managers with identical signatures: list_schemas,
list_extended_properties (MS_Description and friends, sql_variant value
cast to text), list_procedures/list_functions (one builder parameterized by
the sys.objects type codes) and list_sequences (sql_variant bounds cast to
text and parsed back to exact Decimal). Every query is built from
expression nodes and rendered by the dialect -- no string SQL -- and each
family names itself in its cache key so two families never answer each
other.
The introspect subcommand accepted a list of types and nothing in the tree said what each one reads or where its answer comes from. This adds a README to each backend's cli package: the questions with the catalogue each reads, the options, the notes that keep an answer from being misread (estimate lengths, upper-casing, version gates, capabilities that are refused rather than empty), and examples.

The tables are generated from the code, not from memory -- every entry was checked against the statement it dispatches to.
A capability switch that says False for a backend which fully introspects is
a false negative, and this backend produced eight of them: it never declared
the shared introspection switches, so they resolved to core's default of
False. `supports_introspection()` returning False while the CLI offers twelve
introspection types is not a cosmetic mismatch -- a caller gating on the
switches would have concluded SQL Server has no introspection at all.

The default is conservative on purpose and stays: a backend that cannot
answer says so, and that is not an error. What was wrong is a backend that
can answer but says it cannot, which is the direction that silently removes a
working capability from whoever asks.

Declared one per family, each answering what SQL Server actually reads:
catalog views for tables and columns, sys.indexes with sys.index_columns for
the ordering, OBJECT_DEFINITION for routine and trigger bodies, sys.views for
views with their definitions, sys.foreign_keys for constraints.

Placed in a mixin of its own and registered alongside the other backend
answers, before the core bundle each replaces -- the ordering the dialect file
already documents, because a mixin placed after the class it overrides never
wins.

Tests: the introspection test files pass.
`supports_generator_introspection` on the sequence mixin, beside the
statements that create them; `supports_domain_introspection` on the
introspection mixin, since a domain's descriptor is the same catalogue row a
column's type reads.
The CLI called `available_introspect_types(SQLServerDialect(),
INTROSPECT_TYPES)` to have core trim the list. That module is gone -- core
deleted `backend/cli/introspect_capabilities.py` because a shared name table
makes core the arbiter of a vocabulary it does not own, and cannot express a
per-backend difference (structurally absent, version-gated, differently scoped)
anyway.

The parser is built before any connection exists, so it cannot read the version
either. The candidate list is therefore what it always was: this backend's
hand-written `INTROSPECT_TYPES`, twelve entries, each one a family SQL Server
really answers. `source` now imports again.

`--schema` goes with it. A run names one database and the connection fixes the
scope; SQL Server's default schema is `dbo`, and a login that changes it changes
it for the connection. Asking a second time asked the user to restate what the
connection already decided. Every call site passes `schema=None`, which the
introspector already accepted as "resolve it the way the connection does".
The flag was removed rather than defaulted-and-hidden, so a stale command line
fails at parse time instead of quietly reading a different scope.

Also: `supports_procedure_introspection` was defined twice, once in the
introspection mixin and once in the routine mixin, from two separate edits. MRO
picked the routine one silently -- correct by luck, not by construction. It
stays in `routine.py` beside `supports_function_introspection` and the routine
DDL switches, because the same catalogue row answers whether this engine has
procedures at all; `introspection.py` keeps the eight standard switches and the
schema switch, which is the one place "what can I ask" is read.

Tests: the parser test that asserted the flag's default and override now asserts
it is refused; the dispatch tests assert `schema=None` reaches the introspector.
Inserting supports_schema_introspection and supports_sequence_introspection
right after their neighbours' docstrings pushed the neighbours' returns into
the new bodies, so supports_catalog and supports_sequence_as_data_type fell
through to None. None reads as false, which made supports_catalog lie about
the catalog level SQL Server has and left both switches untrustworthy.
…tract

The row parsers are this backend's implementation of the base introspection
contract: the base declares them and every backend must supply them, so the
leading underscore described a visibility the code never had. It is also how
the next private helper gets justified, which is what this branch is trying
to stop.

default_schema becomes a property rather than a method, beside dialect and
cache: it states a fact about the database, it does not perform an action.

No aliases: the old names are gone.

SQL Server side: twelve row parsers -- schemas, extended properties,
routines and sequences among them -- plus default_schema and its twelve
call sites. The SQL-builder test that calls the parsers directly follows.
The introspector still spelled eight query families itself -- table and view
lists, columns, indexes, foreign keys, view info, triggers, database info --
while the statement classes that name them already existed in core. MySQL,
MariaDB and PostgreSQL had each moved those renderings down into the dialect;
SQL Server was the last backend where the question and its spelling lived in
the same method of the introspector.

Each ``format_*_query_statement`` now sits in ``SQLServerIntrospectionMixin``
beside the ``supports_*`` answers it belongs to, and the introspector builds
the statement and executes what comes back. The parameter positions are the
ones the concatenated form had: a caller-supplied schema or table name stays
bound, and a value that is part of the question ('BASE TABLE', 'PRIMARY KEY',
'IsIdentity') stays an inline literal, so every caller keeps the parameter
tuple it already had. That is what ``test_introspection_sql_builders`` pins --
it asserts both halves, so a rendering that quietly changes either one fails
there rather than at the first live server.

``SQLServerNodeBuilderMixin`` now binds its nodes to ``_node_dialect``, which
defaults to ``self.dialect`` for the introspector and is overridden to
``self`` where the dialect itself is the object building the tree. Without
that seam the mixin would hand every node the introspector, whose ``format_*``
methods belong to a different family.

``TableInfoQueryStatement`` and ``TriggerInfoQueryStatement`` have no caller
that renders them: the table-detail path assembles its answer from the column,
index and foreign-key statements, and triggers reach the server through a
family core does not model. They join MariaDB's precedent in
``LEGITIMATE_NON_RENDERS`` rather than being given a formatter nothing calls.

``DEFAULT_SCHEMA`` moves into the mixin module so the introspector's
``default_schema`` and the statements cannot disagree about which schema an
unqualified name resolves in.

Two tests carried assumptions this change makes false: ``test_cli`` asserted a
positional object filter the CLI no longer takes, and the sequence probe
list did not know ``supports_sequence_introspection`` is a capability rather
than an option to probe.
"table" answered several catalogue questions at once -- the relation's row,
its columns, its indexes and its foreign keys. This backend builds that answer
in two places: ``_steps`` mapped the type to ``get_table_info`` (and let
``database <name>`` reach the same method), and ``_value_sections`` expanded
the returned object into three sections. Both go with the type, and so does
the alias: ``database`` answers the database's own facts and no longer reads a
name at all.

The types that remain each map to one introspector method, which is what the
existing "every type maps to a real method" guard asserts -- it now covers a
list without the aggregate.
Two of them still did: the offline dispatch test expected the recorded call
that ``database <name>`` used to make, and the live test asserted the
three-document rendering of ``table``. The first now checks the payload (a
name changes nothing), and the second goes with the type; the helper that
split three documents out of one stream was only there for it.
Deleting the document-splitting helper also deleted the ``@pytest.fixture``
decorator that preceded the probe fixture, so every live test errored at setup
with "fixture 'probed_backend' not found" -- and the import of pytest went with
it as unused. Both are back; the helper it replaced was the only user of the
three-document shape.
`INFORMATION_SCHEMA.TABLES` spells the kind as "BASE TABLE" or "VIEW" and
omits every `is_ms_shipped` row, so a system table could not be told from a
user table and the CLI's `--include-system` flag changed nothing: the parser
had one distinction to make and the question never carried it. The table list
now reads `sys.objects` joined to `sys.schemas`, projects `type` (`U` user
table, `V` view, `S` system table) and honours `include_system` by admitting
`S` and dropping the `is_ms_shipped` filter. `parse_tables` maps the code onto
`TableType`, so the CLI's filter and its output now agree.

The database report takes its version from `server_version` (never raises,
reads the adapted dialect or the backend's cache) instead of refusing to
describe a database nothing had adapted yet, and spells an unknown version as
"unknown" with `(0, 0, 0)`. The statement answers no size or table count, so
those stay None rather than becoming a second query.

Capability predicates now ask `version_at_least(...)` instead of comparing
`self.version`, which raised before adaptation and told the caller nothing it
could act on; the renderers that raise `UnsupportedFeatureError` keep the
strict property, because a formatter gating SQL must know the version. The
introspect CLI adapts right after connecting, before the first question.
The shim already imports the shared tests, so it followed the contract change
(testsuite 3289103) by itself; only its docstring still promised a UserWarning.
…eeps it

`format_view_list_query_statement` projected `TABLE_NAME` alone and
`parse_views` therefore wrote `definition=None` on every row, so
`introspect views` answered with a null definition for every view while the
CLI README promised "views, with their definitions". The single-view question
right below it had always projected `VIEW_DEFINITION` and `IS_UPDATABLE`, and
nothing called it: the type accepted a NAME and ignored it.

The list now projects the same two columns, and `views NAME` is wired to
`get_view_info` -- one named view is a different question from the list, not a
filter over it. Both read `INFORMATION_SCHEMA.VIEWS`, so the two answers
cannot disagree; the formatter documents that the column is NULL when the
caller may not read the view's text, which is where it differs from
`OBJECT_DEFINITION()`.

A name that names no view leaves with 1 rather than printing an empty list.
`format_trigger_list_query_statement` selected `is_disabled` and
`parse_triggers` never read it, so every trigger reported the dataclass
default `status = "ENABLED"` -- including the disabled ones. Its `events` were
`[]` for the same kind of reason: SQL Server keeps the firing events one table
over, in `sys.trigger_events`, and nothing joined them.

The list now left-joins `sys.trigger_events` on the trigger's `object_id` and
projects `type_desc` as `event`, ordered by trigger name and then event so a
trigger's rows are contiguous. `parse_triggers` folds those rows back into one
trigger per name, collecting the events and reading `status` from
`is_disabled`. The join is a left join because a trigger row without an event
of its own is still a trigger: dropping it would turn an empty event list into
a missing trigger.

A `NULL` event from that join is the absence of an event, not an event, so it
does not enter the list.
`sys.extended_properties` is the only place SQL Server keeps an object's
comment, and nothing asked it: `list_extended_properties` took an `object_id`
that the CLI had no way to obtain from a name. The family therefore had no
entry point at all, and every comment in every other output was null.

The statement now names its object one of two ways, and exactly one of them:
`major_id`, as before, or `table_name`, which the dialect resolves through
`sys.tables`/`sys.schemas` in the same query -- one round trip, not "find the
id, then read". Both selections share one projection, so they cannot drift.
A table name only names `OBJECT_OR_COLUMN` properties; any other class is
refused rather than answered with no rows, matching how the formatter already
refuses a schema on a family that has none.

`supports_extended_property_introspection()` declares the family, beside the
other capability switches rather than folded into the table one: "can this
server list tables" and "can it read the properties hanging off them" are two
questions.

The CLI type `extended-properties NAME` requires the table name, like
`columns`/`indexes`/`foreign-keys`. An empty answer is checked against the
catalogue before it is reported, so a table with no properties prints nothing
and a name that names nothing leaves with 1 -- the two used to be
indistinguishable from the query alone.
…ey live

SQL Server has no COMMENT clause: a comment is an `MS_Description` row in
`sys.extended_properties`. `TableInfo.comment` and `ColumnInfo.comment` were
therefore permanently None, because neither read joined that catalogue --
declared fields no backend could fill.

The table list now left-joins the object-level property (`minor_id = 0`, so a
column's description cannot be mistaken for the table's) and projects it as
`table_comment`. The column query joins `sys.columns` to reach the object id
and column id the property is keyed by -- `INFORMATION_SCHEMA.COLUMNS` carries
neither -- and adds a second left join for the column-level row, projected as
`column_comment`. Both are left joins: a table or column without a comment
keeps its row, answering NULL rather than disappearing.

Both statements keep their existing parameter tuples: the property name and
class are part of the question, so they stay inline literals. `parse_tables`
and `parse_columns` read the new aliases, and `get_table_info` inherits the
table comment from `list_tables` without another query.
The advertised type list was a hand-written list with nothing behind it: a
family could be added to the command and left undeclared on the dialect, or
declared and never offered, and no test noticed. `_TYPE_CAPABILITIES` now maps
each type to the dialect switch that answers for it, and `INTROSPECT_TYPES` is
derived from that mapping, so the two cannot drift. The gate runs after
`introspect_and_adapt()` -- the switch answers for the server in front of us,
not for the newest release the backend knows -- and a family the server does
not answer is refused with exit 2 and
"Error: <type> introspection is not supported by this server", rather than run
and answered with an empty list.

`NAME` is now decided per type instead of being left to each branch. It is
required for `columns`/`indexes`/`foreign-keys`/`extended-properties`,
optional for `views` (a view name) and `triggers` (a table name), and refused
for the rest: `tables users` exits 2 instead of listing every table, because a
silently ignored name reads as a filter that matched everything. The refusal
happens before a connection is opened -- whether a name belongs to the type is
a question about the request, not the server.

`get_supported_introspection_scopes()` is overridden: core's default is `[]`,
which told a caller this backend introspects nothing while it answers eleven
families. It now names the eight core scopes the catalogue covers, `SEQUENCE`
included.
The type table still taught `table`, which was removed on 2026-10-27, and it
had no column for `NAME` -- the one part of the interface that differs per
type, and the part a reader has to guess. `views` claimed definitions the list
did not carry (it does now), and `tables` named `sys.tables` where the query
reads `sys.objects`.

The table now lists the twelve types with a `NAME` column (required, optional
or refused), the catalogue each reads, and what the row carries --
`extended-properties`, the trigger events and status, and the view definition
among them. The prose below it spells out the three rules, that `views` takes
a view name while `triggers` takes a table name, and which exit code each kind
of refusal leaves with. The examples lose `table` and gain the types that
replaced what it used to answer.
… skipped

The round-trip matrix pins the set of expression classes the generic
constructor cannot build, in both directions: a class that starts failing to
build without being named fails the run rather than disappearing into a skip.
The new extended-properties statement joined that set -- it names its object one
of two ways, a catalogue id or a table name, and refuses a statement that names
neither -- so the matrix went red on a class nobody had registered.

Registered rather than pinned unconstructible, because it constructs fine once
its own slot is filled: the table-name form is what a caller holding only a name
uses, and it is the form the CLI asks. That also brings the statement into the
round-trip coverage it would otherwise never have had.
`fix/column-constraint-gates` was squashed into `release/v1.0.0.dev30` as
`57439841`, so the branch reference this workflow used to resolve core through
no longer names anything ahead of the release. Pointing at the release branch
makes each run test what a user installs.

The testsuite stays on `fix/column-constraint-gates` on purpose: its own branch
carries the two `pytest_compat` shims that keep pytest 9.2 working (the
benchmark fixture's `_nodeid` read and xdist's loadgroup suffix), and those are
not on the testsuite's release branch yet. That line is repointed when the
testsuite merges.
Named after the pull request once it had a number, which is the order
the fragment check requires: 4 entries, one per kind of change.
@vistart vistart added the enhancement New feature or request label Oct 31, 2026
@vistart vistart self-assigned this Oct 31, 2026
The branch already held fragments named after an older pull request number that
never merged (10 on SQL Server, 21 on Snowflake). The numbering rule reads every
fragment a pull request adds and requires each to carry that pull request's
number, so the check failed on fragments describing changes that ship here all
the same.

Their text is folded into this pull request's own entries rather than dropped:
it is the detail behind the same work -- the catalogue questions core has no
shape for, and the CLI that used to call method names no introspector has.
@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

@codecov

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.15929% with 147 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.87%. Comparing base (50a03ff) to head (8b0c7bd).

Files with missing lines Patch % Lines
...ckend/impl/sqlserver/introspection/introspector.py 87.01% 17 Missing and 10 partials ⚠️
...verecord/backend/impl/sqlserver/mixins/ddl_type.py 51.72% 5 Missing and 9 partials ⚠️
...ial/activerecord/backend/impl/sqlserver/dialect.py 80.00% 4 Missing and 8 partials ⚠️
...mpl/sqlserver/introspection/status_introspector.py 80.00% 10 Missing ⚠️
...ord/backend/impl/sqlserver/mixins/backend_mixin.py 50.00% 10 Missing ⚠️
...iverecord/backend/impl/sqlserver/protocols/json.py 0.00% 8 Missing and 1 partial ⚠️
...activerecord/backend/impl/sqlserver/query_nodes.py 92.30% 9 Missing ⚠️
...iverecord/backend/impl/sqlserver/cli/introspect.py 93.38% 3 Missing and 5 partials ⚠️
...ctiverecord/backend/impl/sqlserver/mixins/types.py 68.42% 3 Missing and 3 partials ⚠️
...erecord/backend/impl/sqlserver/expression/graph.py 80.95% 1 Missing and 3 partials ⚠️
... and 18 more
Additional details and impacted files
@@                   Coverage Diff                   @@
##           release/v1.0.0.dev3       #9      +/-   ##
=======================================================
+ Coverage                74.57%   77.87%   +3.29%     
=======================================================
  Files                      139      144       +5     
  Lines                     7783     8306     +523     
  Branches                  1298     1289       -9     
=======================================================
+ Hits                      5804     6468     +664     
+ Misses                    1541     1408     -133     
+ Partials                   438      430       -8     
Flag Coverage Δ
python-3.14 77.87% <89.15%> (+3.29%) ⬆️
sqlserver-2025 77.87% <89.15%> (+3.29%) ⬆️
unittests 77.87% <89.15%> (+3.29%) ⬆️

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 merged commit ce1db88 into release/v1.0.0.dev3 Oct 31, 2026
12 of 13 checks passed
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