Skip to content

feat(sqlserver): adopt the typed-columns column layer - ColumnTypeSupport table, TimestampColumn rename, and the T-SQL TRIM spellings - #8

Merged
vistart merged 14 commits into
release/v1.0.0.dev3from
feature/typed-columns
Oct 11, 2026
Merged

vistart merged 14 commits into
release/v1.0.0.dev3from
feature/typed-columns

Conversation

@vistart

@vistart vistart commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Description

This backend follows the core's column-layer rebuild (python-activerecord #126, merged into release/v1.0.0.dev30): the core now decides which operations a column offers by its class rather than by a universal base, routes every operation through one format_* hook, turns trim / lpad / rpad / repeat into dedicated nodes, renames DateTimeColumn to TimestampColumn, and rebuilds the DDL-source accessors. This branch makes the SQL Server dialect answer each of the new questions the rebuild asks, in this server's own terms.

The column half of ColumnTypeSupport. A new SQLServerColumnTypeMixin carries SQLSERVER_COLUMN_TYPES, an eighteen-entry table answering every one of the framework's common Python types with a column class — str → StringColumn, list/dict/tuple/set/frozenset → JSONColumn (SQL Server has no array type on any version, and a document is text in NVARCHAR(MAX) that the 2016 function family reads), uuid.UUID → UUIDColumn, enum.Enum → StringColumn, datetime.timedelta → NumericColumn (a span here is a DATEDIFF count), and so on. It is composed into the dialect ahead of core's generic ColumnTypeMixin, so its answer is the one a model layer reaches; core supplies no table of its own, so an unanswered type fails at resolution time, by name, rather than being filled in with a guess about a server core has never seen. Every deviation in the table is recorded with its measured evidence (a live-server sweep across 2019 / 2022 / 2025), and the two famous facts that deliberately stay out of the table are stated in the module docstring instead, so their absence reads as a decision: JSON is not a storage type here (the functions make a document), and uniqueidentifier sorts by internal byte layout, which is a rendering obligation (CONVERT(varchar(36), u) restores text order) rather than a missing operation. The dialect now also declares the new SQLServerTypeSupport protocol — the stated shape of the sqlserver_-prefixed type family (is xml real here, is BIT a boolean, is tinyint signed) — positioned in the MRO next to the mixin that carries the behaviour, and ColumnTypeSupport joins SQLSERVER_PROTOCOLS in the conformance test.

The storage half. suggested_data_types() is reworked and documented as "concepts SQL Server cannot spell, and what it stores instead": the keys are exactly the core types this dialect does not render (binary / varbinary → BlobType, interval → BigIntType, array → JsonType, enum → VarCharType, jsonb → JsonType, timetz → TimeType, uuid → the backend's own SQLServerUniqueIdentifierType, because T-SQL writes UNIQUEIDENTIFIER and the dispatch key has to be a word this backend can produce), and the docstring records what is deliberately not there: xml and timestamptz, both of which have native types here. A load-order defect surfaced while wiring this up — _type_class_for(name) resolves a name by walking the live DataType subclass tree, so a sqlserver_-prefixed name whose class nothing had imported yet resolved to None and dropped out of the mapping silently; the registration path now imports the backend's own DataType classes first.

The rename. DateTimeColumn → TimestampColumn throughout (core renamed it: the column layer models values, and datetime is a vendor spelling, not a different value), and the type-protocol tests follow the core's rename of DateTimeSupport to TimestampSupport. A new SQLServerUUIDMixin lets this backend's UUID spellings (NEWID, UNIQUEIDENTIFIER) win over core's single fixed spelling, and SQLServerProtocolSupportMixin now declares STRING_CONCATENATION = "CONCAT", because T-SQL reads || as logical OR.

SQLServerTrimMixin, written new. SQL Server's TRIM has no ANSI TRIM(BOTH … FROM …) form at every compatibility level: up to 2019 — and in Azure Synapse Analytics — the only spelling is TRIM([characters FROM] string), which always trims both ends and puts the character set before the operand; 2022 added the LEADING / TRAILING / BOTH keywords, but only at compatibility level 160, so the ANSI default form the core renderer emits is not portable even on the newest server. The mixin keeps the operand order SQL Server had first and encodes the direction in the name: BOTH → TRIM(chars FROM x) / TRIM(x), LEADING → LTRIM(x) / LTRIM(x, chars), TRAILING → RTRIM(x) / RTRIM(x, chars). It deliberately does not version-gate: the dialect has no mechanism for a per-expression capability switch, and the rendering and its limits are recorded in the docstring instead.

The deliberate refusals. SQL Server has no LPAD, RPAD or REPEAT functions at all (pad is spelled REPLICATE with RIGHT / LEFT; repeat is REPLICATE). With those nodes now dedicated core classes, their dispatch reaches formatters no base in this dialect provides — and before this branch, that refusal read as "SQL Server lacks the feature" and passed, indistinguishable from a probe genuinely declining something. The round-trip matrix now pins UNMODELLED_FORMATTERS in both directions, so every dispatch refusal is a row with a stated reason (also covering the SQL/XML family, DOMAIN, core's generic PIVOT and the TableSource root) and a method nobody listed fails the run instead of passing as "unsupported".

The round-trip enumeration. The expression matrix test enumerates every core node class, walking the package deterministically instead of the process-global ExpressionRegistry — whose contents depended on which sibling modules pytest happened to import first — so a missing formatter cannot stay silent, and a class that becomes unconstructible must be a row in the pinned UNCONSTRUCTIBLE tuple rather than an unexamined skip.

Tests follow the core's rebuilt semantics. The DDL-source tests move to the accessor migration (column_type() / columns_type() → column_data_type() / columns_data_type()), the DDL check/index conditions and the predicates previously expressed through the bare Column's operator sugar are built explicitly as nodes, and one parametrisation stops instantiating ColumnBase — now an ABC — substituting a concrete typed column. A new table-type definition round-trip test covers the reworked suggested_data_types() answers.

CI and dependencies. The core install line now points at @release/v1.0.0.dev30 now that typed-columns has merged there; the testsuite install line points at @test/typed-column-contracts. A Python 3.15 leg is deliberately not added: pyodbc 5.3.0 ships no cp315 wheel (source tarball only, no Python :: 3.15 classifier), so a 3.15 leg would fail at install while pip compiles the driver — the README records this, and adding the leg awaits a pyodbc release rather than anything in this repository. For Python 3.13 / 3.13t, requirements-dev.txt pins pydantic 2.12.2 / pydantic-core 2.41.4, mirroring the core's pins: this file is installed before the core package, so an unpinned pydantic there picks a pydantic-core whose only cp313t distribution is an sdist that fails to build on the free-threaded jobs.

Type of change

  • feat: A new feature
  • docs
  • perf
  • test: Adding missing tests and correcting existing tests for the new feature
  • chore
  • ci: Changes to CI configuration files and scripts (core install line now on its release branch; testsuite line)
  • build: Pydantic pinning for Python 3.13 / 3.13t
  • revert

Breaking Change

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

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

Impact on End-Users:

The large breaking surface is the core's (see python-activerecord #126); what is visible through this backend's API is the following, and nothing else:

Change Migration
DateTimeColumn renamed TimestampColumn rename the import; no behaviour change
bare Column lost its operators (core) use a typed column (StringColumn, NumericColumn, …) or build the node explicitly (ComparisonPredicate, InPredicate, BinaryArithmeticExpression)
DDLSource.column_type() is now a declaration accessor (core) SQL-type reads move to column_data_type() / columns_data_type(); columns_type is gone
trim/lpad/rpad/repeat return nodes, not function calls (core) no action — the column methods still render the same SQL
a function with a recorded version floor refuses when the server is older (core) catch UnsupportedFeatureError, or target a server that has the function
the supports_data_type_* probe family is retired (core) consult supports_data_types(), which now answers from formatter presence

Impact on Backend Developers (Custom Implementations):

  • A dialect that renders the new nodes composes the mixins whose operations it supports (TrimMixin / LpadMixin / RepeatMixin / RpadMixin) or writes a format_* override where its spelling differs — SQLServerTrimMixin here is the worked example, including a non-ANSI operand order that the shared default does not produce.
  • ColumnTypeSupport is a new protocol a backend must implement; the conformance surface (SQLSERVER_PROTOCOLS) must classify it, and the backend must declare the shape of its own type family (SQLServerTypeSupport here).
  • The storage half (suggested_data_types()) must answer only with classes the dialect really renders, and must not name a key it can produce under its own word (uuid → SQLServerUniqueIdentifierType here).

Related Repositories/Packages

Repository Branch State Required before this merge?
python-activerecord release/v1.0.0.dev30 #126 merged 2026-10-11; this branch's CI installs the core from it Yes — already landed
python-activerecord-testsuite test/typed-column-contracts exists; no PR yet Yes, and it must land before the backends
python-activerecord-{postgres,mysql,mariadb,oracle,firebird,clickhouse,snowflake,bigquery} feature/typed-columns in flight, each ahead of its own release Yes, after core and testsuite

Note on the install references: this branch's release CI installs the core from @release/v1.0.0.dev30 (re-pointed in the last commit now that #126 has merged) and the testsuite from @test/typed-column-contracts. Once the testsuite branch lands on its release branch, that line has to be re-pointed too — otherwise the release branch keeps depending on a branch that will be deleted.

Testing

  • I have added new tests to cover my changes.
  • I have updated existing tests to reflect my changes.
  • All existing tests pass — CI is the gate here, see Additional Notes.
  • Tested against SQL Server versions 2019 / 2022 / 2025 (the measured sweep the column-type table is built from).

Test Plan:

# the column-type table and the protocol classification
pytest tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_sqlserver_column_type.py
pytest tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_sqlserver_protocol_conformance.py

# the type-protocol battery (TimestampSupport rename, suggested_data_types)
pytest tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_sqlserver_type_protocol.py

# the node-class enumeration matrix (UNMODELLED_FORMATTERS / UNCONSTRUCTIBLE pinning)
pytest tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_expression_roundtrip_all.py

# the DDL-source accessor migration and the table-type round-trip
pytest tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_ddl_source_mixin.py
pytest tests/rhosocial/activerecord_sqlserver_test/feature/backend/test_table_type_definition_roundtrip.py

Full-suite verification is left to CI — the project forbids local full-suite runs, so no pytest tests/ was executed locally.

Checklist

  • My code follows the project's code style guidelines in .claude/code_style.md.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas (the trim mapping, the column-type table's measured evidence and deliberate omissions, and the load-order fix all carry their reasoning in-place).
  • I have made corresponding changes to the documentation (docstrings, README.md — the 3.15-leg rationale is recorded there).
  • My changes generate no new warnings.
  • Changelog fragment — not yet written. By this runbook's PR-first rule the fragment is created after the PR exists, named with the real PR number (Steps 14–15).
  • I have verified that my changes do not introduce SQL injection vulnerabilities (all rendering stays parameterised; the trim mixin carries the operands' parameters through in order).
  • I have checked for potential performance regressions (the column-type table returns a fresh dict per call; no per-render work is added).

Additional Notes

Deliberately out of scope for this branch (recorded so reviewers do not expect it):

  • A Python 3.15 CI leg. Awaiting pyodbc; the README records why.
  • Version-gating LTRIM's two-argument form (2022 only, at compatibility level 160). The dialect has no mechanism for a per-expression capability switch, so the mixin records the rendering and its limits instead of gating — a future per-expression capability hook would let it narrow.
  • SQL Server 2025's native json type. Not modelled yet: the catalogue answers json with CHARACTER_MAXIMUM_LENGTH = -1 and the parser returns CustomType('json') while format_data_type_json still renders NVARCHAR(MAX). The column answer is unaffected (a document carries the same operations wherever it is kept), and this is documented in the column-type module docstring.
  • The remaining format_* annotation sweep org-wide, and the rest of the DataType four-table rework, are follow-up work.

Verified before drafting: the release tip is an ancestor of the branch (merge-base = release tip, 0 behind), so no rebase is needed on merge.

vistart added 13 commits October 9, 2026 16:13
…as merged

While these branches were in development, CI installed the core from
`@feature/typed-columns`, because the release branch did not carry the rebuilt
column layer yet. That branch has now merged into `release/v1.0.0.dev30`
(PR #126), so the feature reference is both stale and about to disappear with
branch deletion.

Point the core dependency at `release/v1.0.0.dev30` instead — the same branch
these backends are merging into, which is what the release-side CI already
used. The testsuite reference is deliberately left alone: that branch
(`test/typed-column-contracts`) has not merged into its release yet, and
pointing at a release that lacks the new contracts would break every backend
at once. It moves when the testsuite merges.

ClickHouse expresses the dependency as a `ref:` on `actions/checkout` rather
than as a pip requirement; that one is switched the same way.
@vistart vistart added the enhancement New feature or request label Oct 11, 2026
@vistart vistart self-assigned this Oct 11, 2026
@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 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.19718% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.57%. Comparing base (610cd87) to head (8bd366a).

Files with missing lines Patch % Lines
...activerecord/backend/impl/sqlserver/mixins/trim.py 47.61% 8 Missing and 3 partials ⚠️
...cord/backend/impl/sqlserver/protocols/data_type.py 58.33% 10 Missing ⚠️
...ctiverecord/backend/impl/sqlserver/mixins/types.py 97.43% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@                   Coverage Diff                   @@
##           release/v1.0.0.dev3       #8      +/-   ##
=======================================================
+ Coverage                73.97%   74.57%   +0.59%     
=======================================================
  Files                      135      139       +4     
  Lines                     7605     7783     +178     
  Branches                  1261     1298      +37     
=======================================================
+ Hits                      5626     5804     +178     
- Misses                    1540     1541       +1     
+ Partials                   439      438       -1     
Flag Coverage Δ
python-3.14 74.57% <91.19%> (+0.59%) ⬆️
sqlserver-2025 74.57% <91.19%> (+0.59%) ⬆️
unittests 74.57% <91.19%> (+0.59%) ⬆️

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 3b655b1 into release/v1.0.0.dev3 Oct 11, 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