Repository navigation
Commit ce1db88
authored
feat(sqlserver)!: the catalogue as statements, and the extended-property family (#9)
* fix(types): refuse a non-boolean unsigned when the type is rendered
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.
* refactor(sqlserver): read declared fields directly instead of probing 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.
* refactor(sqlserver): follow the statement and expression renames
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.
* refactor(sqlserver): migrate constraints onto the two-level branch nodes
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.
* test(sqlserver): pin the constraint switches to their measurements
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.
* refactor(sqlserver): split the wildcard by grammar position
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).
* test(sqlserver): carry the wildcard-split follow-ups into the roundtrip 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).
* ci: point testsuite at the matching branch
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.
* docs(sqlserver): carry the renames into the README and docs
* refactor(expression): follow the core renames and the split registry
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.
* fix(sqlserver): register the type nodes with the kind registry
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).
* refactor(sqlserver): read declared fields instead of probing with getattr
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.
* fix(sqlserver): register the type nodes with the read-path registry
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).
* feat(sqlserver): adopt the typed value nodes with SQL Server literal 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.
* fix(sqlserver): follow the classed function entry points and the bare 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).
* refactor(sqlserver): follow the literal architecture the core settled
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.
* refactor(sqlserver): read each node's own parameters and follow the renames 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).
* refactor(sqlserver): audit the kinds this backend overrides and owns
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).
* test(sqlserver): reconcile the roundtrip matrix with the plain-JSON artifact
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.
* test(sqlserver): move live-connection checks out of expression tests
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.
* test(sqlserver): load the round-trip harness by path, not by name
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.
* test(sqlserver): keep the literal spellings file whole when it loses 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.
* refactor(sqlserver): name a lock strength by the phrase SQL would say
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.
* test(sqlserver): read a UNIQUEIDENTIFIER back as the value it is
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.
* refactor(sqlserver): drop the imports the matrix no longer names
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.
* fix(sqlserver): pass the LIKE operator as its enum, not the SQL keyword
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.
* refactor(sqlserver): build introspection queries from nodes, not text
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.
* chore: ignore .worktree/, the branch toolchain directories
* fix(sqlserver): refuse the CTAS spelling SQL Server has no statement for
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.
* test(sqlserver): drop the artifact-gap rows whose gap has closed
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.
* fix(sqlserver): the dialect surface, and the type positions it decides
The dialect's surface is two kinds of member: capability switches
(``supports_*()``) and formatters (``format_*(self, expr)``). This change
makes SQL Server's code match that, and settles what a type position holds
while it is there.
**A formatter owns its production.** Core's dialect mixins held six private
renderers serving families of thin ``format_*(self, expr)`` wrappers. The
wrappers were formatters; the bodies were not, and being private they could
not be overridden. SQL Server's ``LEN`` now owns its production.
**A type-name position holds a type.** The positions that carried a raw
string guarded it with a character class in the base class, which neither
matched what the engines accept nor kept the value inside its syntactic
position. A position now holds a type node; a name the framework does not
model becomes the framework's name node, whose name the grammar validates.
The ALTER COLUMN type is claimed the same way, and no longer stringified.
**Binding is the caller's call.** A DDL position that cannot bind says so
through SQL Server's own capability switch, and a value the caller chose to
inline is inlined.
**The backend's own answers are the effective ones.** SQL Server's mixin
precedes the core mixin it overrides, and a test holds that in place.
**The mixin list says what it is for.** The base-list convention is
written down where the list is, and the function examples reach their
constructors through ``functions as F``.
* refactor(expression)!: follow core's statement rename
Core renamed the four statement-shaped nodes it still called *Expression:
TransactionExpression -> TransactionStatement
SetTransactionExpression -> SetTransactionStatement
SQLiteAttachExpression -> SQLiteAttachStatement
SQLiteDetachExpression -> SQLiteDetachStatement
This repository refers to them in its dialect signatures and its tests, so
the same names follow here. Nothing changes but the names.
* refactor(ddl)!: the SQL Server create-table node is a statement
``SQLServerCreateTableExpression`` derives from ``CreateTableStatement`` and
renders a whole statement; only its name still said expression.
Nothing changes but the class name.
* refactor(expression)!: name statement nodes, kinds and formatters alike
The nodes that render a whole statement are now ``*Statement``, and the
fragments have formatters ending in the ``_expression`` their class
names carry: the JSON family, SET literal, FIND_IN_SET, the SET
membership check, the spatial literal, PIVOT/UNPIVOT, NEXT VALUE FOR,
AS GRAPH TABLE, SHORTEST PATH, and the partition function and scheme.
FULLTEXT is one word in the SQL Server statement names it appears in,
but it is two words in the identifiers here (``full_text``), and the
class names have said ``FullText`` all along. The catalog and index
formatters now spell it the same way.
This also frees four core capability names. The backend's full-text
formatters are string-taking helpers that had taken over the *core*
node formatter names, so core's generic ``CreateFulltextIndexStatement``
and ``DropFulltextIndexStatement`` dispatched into signatures they could
not satisfy and raised ``TypeError``. Renaming the helpers to their own
spelling lets those nodes reach core's renderers, which is why the
matrix's pins for them are gone.
``format_pivot`` / ``format_unpivot`` stay as they are: they are core's
capabilities, and ``SQLServerPivotExpression`` reuses the shared
renderer the way ``IntegerColumn`` reuses ``format_column``.
* refactor(backend)!: call the core helpers by their public names
Core moved ten cross-package helpers out from behind a leading
underscore: they are called from this backend, so the underscore was
claiming a scope the names never had. Each old name had exactly one new
name, checked against every public definition and call site in the ten
repositories first, so nothing is shadowed by the rename.
* fix(introspection): take the cache from the manager, not the mixin
* fix(introspection): call the introspector by the names core defines
The introspect subcommand asked get_tables/get_views/get_columns (and the
rest of the …1 parent 50a03ff commit ce1db88
174 files changed
Lines changed: 11511 additions & 3107 deletions
File tree
- .github/workflows
- changelog.d
- src/rhosocial/activerecord/backend/impl/sqlserver
- backend
- cli
- examples
- connection
- ddl
- functions
- insert
- named_expressions
- query
- schema_diff
- transaction
- types
- expression
- ddl
- functions
- introspection
- mixins
- protocols
- show
- tests
- plugins
- providers
- fixtures
- rhosocial/activerecord_sqlserver_test/feature
- backend
- cli
- dialect
- expression
- sqlserver
- basic/transaction
- query
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
| 63 | + | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| |||
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
153 | | - | |
| 153 | + | |
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| |||
240 | 240 | | |
241 | 241 | | |
242 | 242 | | |
243 | | - | |
| 243 | + | |
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
| |||
330 | 330 | | |
331 | 331 | | |
332 | 332 | | |
333 | | - | |
| 333 | + | |
334 | 334 | | |
335 | 335 | | |
336 | 336 | | |
| |||
420 | 420 | | |
421 | 421 | | |
422 | 422 | | |
423 | | - | |
| 423 | + | |
424 | 424 | | |
425 | 425 | | |
426 | 426 | | |
| |||
510 | 510 | | |
511 | 511 | | |
512 | 512 | | |
513 | | - | |
| 513 | + | |
514 | 514 | | |
515 | 515 | | |
516 | 516 | | |
| |||
601 | 601 | | |
602 | 602 | | |
603 | 603 | | |
604 | | - | |
| 604 | + | |
605 | 605 | | |
606 | 606 | | |
607 | 607 | | |
| |||
691 | 691 | | |
692 | 692 | | |
693 | 693 | | |
694 | | - | |
| 694 | + | |
695 | 695 | | |
696 | 696 | | |
697 | 697 | | |
| |||
790 | 790 | | |
791 | 791 | | |
792 | 792 | | |
793 | | - | |
| 793 | + | |
794 | 794 | | |
795 | 795 | | |
796 | 796 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
53 | 52 | | |
| 53 | + | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
82 | | - | |
| 82 | + | |
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
88 | | - | |
89 | | - | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
90 | 92 | | |
91 | 93 | | |
92 | 94 | | |
| |||
145 | 147 | | |
146 | 148 | | |
147 | 149 | | |
148 | | - | |
| 150 | + | |
149 | 151 | | |
150 | 152 | | |
151 | 153 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
9 | 18 | | |
10 | 19 | | |
11 | 20 | | |
12 | 21 | | |
13 | | - | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
34 | 40 | | |
35 | 41 | | |
36 | 42 | | |
37 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
38 | 60 | | |
39 | 61 | | |
40 | 62 | | |
| |||
112 | 134 | | |
113 | 135 | | |
114 | 136 | | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
115 | 140 | | |
116 | 141 | | |
117 | 142 | | |
118 | | - | |
| 143 | + | |
119 | 144 | | |
120 | 145 | | |
121 | 146 | | |
| |||
0 commit comments