Skip to content

Commit 2563a72

Browse files
committed
feat(sqlserver): add NEXT VALUE FOR and MERGE OUTPUT/HOLDLOCK, wire protocol stubs
1 parent a8e9301 commit 2563a72

24 files changed

Lines changed: 890 additions & 29 deletions

File tree

‎src/rhosocial/activerecord/backend/impl/sqlserver/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
SQLServerSystemVersioningClause,
3131
SQLServerOpenJsonExpression,
3232
OpenJsonColumn,
33+
SQLServerNextValueForExpression,
3334
SQLServerTryCastExpression,
3435
SQLServerTryConvertExpression,
3536
SQLServerContainsPredicate,
@@ -88,6 +89,7 @@
8889
"SQLServerSystemVersioningClause",
8990
"SQLServerOpenJsonExpression",
9091
"OpenJsonColumn",
92+
"SQLServerNextValueForExpression",
9193
"SQLServerTryCastExpression",
9294
"SQLServerTryConvertExpression",
9395
"SQLServerContainsPredicate",

‎src/rhosocial/activerecord/backend/impl/sqlserver/dialect.py‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
SQLServerPaginationSupport,
5454
SQLServerMergeSupport,
5555
SQLServerPartitionSupport,
56+
SQLServerOutputSupport,
57+
SQLServerTableHintSupport,
58+
SQLServerTryCastSupport,
59+
SQLServerIdentitySupport,
60+
SQLServerIndexedViewSupport,
5661
)
5762
from rhosocial.activerecord.backend.dialect.mixins import (
5863
CollationMixin,
@@ -96,6 +101,8 @@
96101
from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError
97102
from .collation import validate_sqlserver_collation_name
98103
from .alter_table_modifier import SQLServerAlterColumnModifierMixin
104+
from .mixins.sequence import SQLServerSequenceMixin
105+
from .mixins.protocol_support import SQLServerProtocolSupportMixin
99106
# SQLServerPartitionMixin is registered lazily in _register_partition_formatters()
100107

101108
if TYPE_CHECKING:
@@ -169,6 +176,8 @@ class SQLServerDialect(
169176
DQLMixin,
170177
DMLMixin,
171178
SQLServerAlterColumnModifierMixin, # Before DDLColumnMixin to override format_*_action
179+
SQLServerProtocolSupportMixin, # SQL Server protocol contract implementations
180+
SQLServerSequenceMixin, # NEXT VALUE FOR formatter (2012+)
172181
DDLColumnMixin,
173182
DDLTypeMixin,
174183
# Feature mixins
@@ -203,12 +212,14 @@ class SQLServerDialect(
203212
CTESupport,
204213
FilterClauseSupport,
205214
WindowFunctionSupport,
215+
SQLServerJSONSupport, # Before JSONSupport (subclass must precede its base)
206216
JSONSupport,
207217
ReturningSupport,
208218
AdvancedGroupingSupport,
209219
ArraySupport,
210220
ExplainSupport,
211221
GraphSupport,
222+
SQLServerLockingSupport, # Before LockingSupport
212223
LockingSupport,
213224
MergeSupport,
214225
OrderedSetAggregationSupport,
@@ -224,15 +235,25 @@ class SQLServerDialect(
224235
SchemaSupport,
225236
IndexSupport,
226237
SequenceSupport,
238+
SQLServerTableSupport, # Before TableSupport
227239
TableSupport,
228240
ConstraintSupport,
229241
IntrospectionSupport,
230242
TransactionControlSupport,
231243
SQLFunctionSupport,
232-
# SQL Server-specific protocols (removed from MRO to avoid diamond inheritance)
233-
# SQLServerTableSupport, SQLServerLockingSupport, SQLServerJSONSupport,
234-
# SQLServerTemporalTableSupport, SQLServerSequenceSupport,
235-
# SQLServerFullTextSearchSupport, SQLServerPaginationSupport, SQLServerMergeSupport,
244+
# SQL Server-specific protocols (marker classes for isinstance() checks;
245+
# implementations live in SQLServerProtocolSupportMixin / SQLServerSequenceMixin)
246+
SQLServerOutputSupport,
247+
SQLServerPaginationSupport,
248+
SQLServerFullTextSearchSupport,
249+
SQLServerTryCastSupport,
250+
SQLServerTableHintSupport,
251+
SQLServerTemporalTableSupport,
252+
SQLServerMergeSupport,
253+
SQLServerSequenceSupport,
254+
SQLServerIdentitySupport,
255+
SQLServerIndexedViewSupport,
256+
SQLServerPartitionSupport, # Before PartitionSupport
236257
PartitionSupport,
237258
):
238259
"""
@@ -1434,12 +1455,20 @@ def format_merge_statement(self, expr: "MergeExpression") -> Tuple[str, tuple]:
14341455
on_sql, on_params = expr.on_condition.to_sql()
14351456
all_params.extend(on_params)
14361457

1458+
dialect_options = getattr(expr, "dialect_options", {}) or {}
1459+
output_columns = dialect_options.get("output")
1460+
output_action = dialect_options.get("output_action", False)
1461+
holdlock = dialect_options.get("holdlock", False)
1462+
14371463
parts = [
14381464
f"MERGE INTO {target_sql}",
14391465
f"USING {source_sql}",
14401466
f"ON {on_sql}",
14411467
]
14421468

1469+
if holdlock and self.supports_merge_holdlock():
1470+
parts[0] = f"MERGE INTO {target_sql} WITH (HOLDLOCK)"
1471+
14431472
for action in expr.when_matched:
14441473
action_parts = []
14451474
if action.condition:
@@ -1484,6 +1513,14 @@ def format_merge_statement(self, expr: "MergeExpression") -> Tuple[str, tuple]:
14841513
action_parts.append("THEN INSERT DEFAULT VALUES")
14851514
parts.append(" ".join(action_parts))
14861515

1516+
if output_columns and self.supports_merge_output():
1517+
action_type = "$action" if output_action else None
1518+
output_sql, output_params = self.format_merge_output_clause(
1519+
list(output_columns), action_type=action_type
1520+
)
1521+
parts.append(output_sql)
1522+
all_params.extend(output_params)
1523+
14871524
return " ".join(parts), tuple(all_params)
14881525

14891526
def format_set_transaction(self, expr: "SetTransactionExpression") -> Tuple[str, tuple]:

‎src/rhosocial/activerecord/backend/impl/sqlserver/expression/__init__.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .locking import SQLServerTableHintClause, SQLServerTableHint, SQLServerReadPastHint
66
from .temporal import SQLServerTemporalPeriodDefinition, SQLServerSystemVersioningClause
77
from .openjson import SQLServerOpenJsonExpression, OpenJsonColumn
8+
from .sequence import SQLServerNextValueForExpression
89
from .functions import (
910
SQLServerTryCastExpression,
1011
SQLServerTryConvertExpression,
@@ -22,8 +23,9 @@
2223
"SQLServerSystemVersioningClause",
2324
"SQLServerOpenJsonExpression",
2425
"OpenJsonColumn",
26+
"SQLServerNextValueForExpression",
2527
"SQLServerTryCastExpression",
2628
"SQLServerTryConvertExpression",
2729
"SQLServerContainsPredicate",
2830
"SQLServerFreetextPredicate",
29-
]
31+
]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# src/rhosocial/activerecord/backend/impl/sqlserver/expression/sequence.py
2+
"""SQL Server SEQUENCE value expression.
3+
4+
SQL Server sequences are objects that generate a sequence of numeric values.
5+
Values are obtained with the ``NEXT VALUE FOR`` expression (SQL Server 2012+):
6+
7+
SELECT NEXT VALUE FOR my_seq;
8+
INSERT INTO t (id) VALUES (NEXT VALUE FOR my_seq);
9+
10+
This module defines the expression used to request the next value from a
11+
sequence. SQL generation is delegated to the dialect's
12+
``format_next_value_for`` formatter.
13+
"""
14+
15+
from typing import TYPE_CHECKING
16+
17+
from rhosocial.activerecord.backend.expression.bases import BaseExpression, SQLQueryAndParams
18+
19+
if TYPE_CHECKING:
20+
from ..dialect import SQLServerDialect
21+
22+
23+
class SQLServerNextValueForExpression(BaseExpression):
24+
"""SQL Server NEXT VALUE FOR expression (2012+).
25+
26+
Requests the next value from a sequence object.
27+
28+
Attributes:
29+
sequence_name: Name of the sequence (may be schema-qualified).
30+
31+
Example:
32+
>>> expr = SQLServerNextValueForExpression(dialect, "my_seq")
33+
>>> sql, params = expr.to_sql()
34+
>>> assert sql == "NEXT VALUE FOR [my_seq]"
35+
"""
36+
37+
def __init__(self, dialect: "SQLServerDialect", sequence_name: str):
38+
super().__init__(dialect)
39+
self.sequence_name = sequence_name
40+
41+
def validate(self, strict: bool = True) -> None:
42+
if not strict:
43+
return
44+
if not isinstance(self.sequence_name, str):
45+
raise TypeError(f"sequence_name must be str, got {type(self.sequence_name)}")
46+
47+
def to_sql(self) -> SQLQueryAndParams:
48+
return self.dialect.format_next_value_for(self), ()

‎src/rhosocial/activerecord/backend/impl/sqlserver/mixins/__init__.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from .concurrency import SQLServerConcurrencyMixin
44
from .backend_mixin import SQLServerBackendMixin
55
from .partition import SQLServerPartitionMixin
6+
from .sequence import SQLServerSequenceMixin
7+
from .protocol_support import SQLServerProtocolSupportMixin
68

79
__all__ = [
810
"SQLServerTypeSupportMixin",
911
"SQLServerConcurrencyMixin",
1012
"SQLServerBackendMixin",
1113
"SQLServerPartitionMixin",
12-
]
14+
"SQLServerSequenceMixin",
15+
"SQLServerProtocolSupportMixin",
16+
]

‎src/rhosocial/activerecord/backend/impl/sqlserver/mixins/backend_mixin.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
# src/rhosocial/activerecord/backend/impl/sqlserver/mixins/backend_mixin.py
2-
import logging
3-
from typing import Any, Dict, Optional, Tuple, Type
2+
from typing import Any, Dict, Optional, Tuple, Type, TYPE_CHECKING
43

5-
from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter
6-
7-
from ..dialect import SQLServerDialect
4+
if TYPE_CHECKING:
5+
from ..dialect import SQLServerDialect
86

97

108
class SQLServerBackendMixin:
11-
_dialect: SQLServerDialect
9+
_dialect: "SQLServerDialect"
1210
_connection: Optional[object]
1311

1412
def set_nocount(self, on: bool = True) -> None:
@@ -225,7 +223,6 @@ def _handle_error(self, error: Exception) -> None:
225223
DeadlockError,
226224
IntegrityError,
227225
OperationalError,
228-
QueryError,
229226
)
230227

231228
error_msg = str(error)

0 commit comments

Comments
 (0)