Skip to content

Commit e3f4d5b

Browse files
committed
feat(sqlserver): add in-memory OLTP options, indexed views, full-text catalog DDL and T-SQL routine/trigger DDL
1 parent c8e73ce commit e3f4d5b

14 files changed

Lines changed: 1700 additions & 29 deletions

File tree

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

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104
from .mixins.sequence import SQLServerSequenceMixin
105105
from .mixins.pivot import SQLServerPivotMixin
106106
from .mixins.columnstore import SQLServerColumnstoreIndexMixin
107+
from .mixins.memory_optimized import SQLServerMemoryOptimizedMixin
108+
from .mixins.routine import SQLServerRoutineMixin
109+
from .mixins.trigger import SQLServerTriggerDdlMixin
107110
from .mixins.protocol_support import SQLServerProtocolSupportMixin
108111
# SQLServerPartitionMixin is registered lazily in _register_partition_formatters()
109112

@@ -150,6 +153,8 @@
150153
)
151154

152155

156+
SQL_SERVER_2005 = (9, 0, 0)
157+
SQL_SERVER_2008 = (10, 0, 0)
153158
SQL_SERVER_2012 = (11, 0, 0)
154159
SQL_SERVER_2014 = (12, 0, 0)
155160
SQL_SERVER_2016 = (13, 0, 0)
@@ -182,6 +187,9 @@ class SQLServerDialect(
182187
SQLServerSequenceMixin, # NEXT VALUE FOR formatter (2012+)
183188
SQLServerPivotMixin, # PIVOT / UNPIVOT formatters (2005+)
184189
SQLServerColumnstoreIndexMixin, # columnstore index DDL (2012+/2014+/2022+)
190+
SQLServerMemoryOptimizedMixin, # In-Memory OLTP table options (2014+)
191+
SQLServerRoutineMixin, # PROCEDURE / FUNCTION DDL (2005+)
192+
SQLServerTriggerDdlMixin, # TRIGGER DDL (2005+)
185193
DDLColumnMixin,
186194
DDLTypeMixin,
187195
# Feature mixins
@@ -792,7 +800,7 @@ def supports_materialized_view(self) -> bool:
792800

793801
def supports_indexed_view(self) -> bool:
794802
"""SQL Server supports indexed views (similar to materialized views)."""
795-
return True
803+
return self.version >= SQL_SERVER_2005
796804

797805
def supports_if_exists_view(self) -> bool:
798806
"""SQL Server supports DROP VIEW IF EXISTS (2016+)."""
@@ -822,7 +830,10 @@ def format_create_view_statement(
822830
parts.append(f"({cols})")
823831

824832
query_sql, query_params = expr.query.to_sql()
825-
parts.append(f"AS {query_sql}")
833+
as_sql = f"AS {query_sql}"
834+
if expr.options and getattr(expr.options, "schemabinding", False):
835+
as_sql = f"WITH SCHEMABINDING {as_sql}"
836+
parts.append(as_sql)
826837

827838
if expr.options and hasattr(expr.options, 'check_option') and expr.options.check_option:
828839
parts.append(f"WITH {expr.options.check_option.value} CHECK OPTION")
@@ -849,16 +860,16 @@ def format_create_materialized_view_statement(
849860
raise UnsupportedFeatureError(self.name, "CREATE MATERIALIZED VIEW", _SUGGESTION_MATERIALIZED_VIEW)
850861

851862
def supports_trigger(self) -> bool:
852-
"""SQL Server supports triggers."""
853-
return True
854-
863+
"""SQL Server supports triggers (since 2005)."""
864+
return self.version >= SQL_SERVER_2005
865+
855866
def supports_create_trigger(self) -> bool:
856-
"""SQL Server supports CREATE TRIGGER."""
857-
return True
858-
867+
"""SQL Server supports CREATE TRIGGER (since 2005)."""
868+
return self.version >= SQL_SERVER_2005
869+
859870
def supports_drop_trigger(self) -> bool:
860-
"""SQL Server supports DROP TRIGGER."""
861-
return True
871+
"""SQL Server supports DROP TRIGGER (since 2005)."""
872+
return self.version >= SQL_SERVER_2005
862873

863874
def supports_instead_of_trigger(self) -> bool:
864875
"""SQL Server supports INSTEAD OF triggers."""
@@ -1184,6 +1195,12 @@ def format_create_table_statement(
11841195
parts.append(partition_sql)
11851196
all_params.extend(partition_params)
11861197

1198+
# Handle In-Memory OLTP table options (2014+)
1199+
dialect_options = getattr(expr, "dialect_options", {}) or {}
1200+
if dialect_options.get("memory_optimized"):
1201+
durability = dialect_options.get("durability", "SCHEMA_ONLY")
1202+
parts.append(self.format_memory_optimized_option(durability))
1203+
11871204
return ' '.join(parts), tuple(all_params)
11881205

11891206
def _format_column_definition(self, col_def: "ColumnDefinition", ColumnConstraintType) -> Tuple[str, List[Any]]:
@@ -1284,7 +1301,20 @@ def _format_inline_index(self, idx_def: "IndexDefinition") -> str:
12841301
parts.append(self.format_identifier(idx_def.name))
12851302

12861303
cols_str = ', '.join(self.format_identifier(c) for c in idx_def.columns)
1287-
parts.append(f"({cols_str})")
1304+
1305+
idx_options = getattr(idx_def, "dialect_options", None) or {}
1306+
if idx_options.get("hash_index"):
1307+
self.check_feature_support(
1308+
"supports_memory_optimized_tables",
1309+
"NONCLUSTERED HASH index",
1310+
"requires SQL Server 2014+ (memory-optimized tables).",
1311+
)
1312+
bucket_count = idx_options.get("bucket_count")
1313+
if bucket_count is None:
1314+
raise ValueError("bucket_count is required for a NONCLUSTERED HASH index")
1315+
parts.append(f"NONCLUSTERED HASH ({cols_str}) WITH (BUCKET_COUNT = {bucket_count})")
1316+
else:
1317+
parts.append(f"({cols_str})")
12881318

12891319
return ' '.join(parts)
12901320

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@
1717
)
1818
from .pivot import SQLServerPivotExpression, SQLServerUnpivotExpression
1919
from .columnstore import SQLServerColumnstoreIndexExpression
20+
from .fulltext import (
21+
SQLServerCreateFullTextCatalogExpression,
22+
SQLServerDropFullTextCatalogExpression,
23+
SQLServerCreateFullTextIndexExpression,
24+
SQLServerDropFullTextIndexExpression,
25+
)
26+
from .ddl import (
27+
SQLServerCreateProcedureExpression,
28+
SQLServerCreateFunctionExpression,
29+
SQLServerDropRoutineExpression,
30+
SQLServerCreateTriggerExpression,
31+
SQLServerDropTriggerExpression,
32+
)
2033
from .functions import (
2134
SQLServerTryCastExpression,
2235
SQLServerTryConvertExpression,
@@ -45,6 +58,15 @@
4558
"SQLServerPivotExpression",
4659
"SQLServerUnpivotExpression",
4760
"SQLServerColumnstoreIndexExpression",
61+
"SQLServerCreateFullTextCatalogExpression",
62+
"SQLServerDropFullTextCatalogExpression",
63+
"SQLServerCreateFullTextIndexExpression",
64+
"SQLServerDropFullTextIndexExpression",
65+
"SQLServerCreateProcedureExpression",
66+
"SQLServerCreateFunctionExpression",
67+
"SQLServerDropRoutineExpression",
68+
"SQLServerCreateTriggerExpression",
69+
"SQLServerDropTriggerExpression",
4870
"SQLServerTryCastExpression",
4971
"SQLServerTryConvertExpression",
5072
"SQLServerContainsPredicate",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# src/rhosocial/activerecord/backend/impl/sqlserver/expression/ddl/__init__.py
2+
"""SQL Server-specific DDL expression classes."""
3+
4+
from .routine import (
5+
SQLServerCreateProcedureExpression,
6+
SQLServerCreateFunctionExpression,
7+
SQLServerDropRoutineExpression,
8+
)
9+
from .trigger import (
10+
SQLServerCreateTriggerExpression,
11+
SQLServerDropTriggerExpression,
12+
)
13+
14+
__all__ = [
15+
"SQLServerCreateProcedureExpression",
16+
"SQLServerCreateFunctionExpression",
17+
"SQLServerDropRoutineExpression",
18+
"SQLServerCreateTriggerExpression",
19+
"SQLServerDropTriggerExpression",
20+
]
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# src/rhosocial/activerecord/backend/impl/sqlserver/expression/ddl/routine.py
2+
"""SQL Server stored procedure / function DDL expressions.
3+
4+
T-SQL routines use a distinctive syntax compared to SQL/PSM:
5+
6+
CREATE PROCEDURE [dbo].[p] @x INT AS BEGIN SET NOCOUNT ON; SELECT @x + 1; END;
7+
CREATE FUNCTION dbo.f (@x INT) RETURNS INT AS BEGIN RETURN @x * 2; END;
8+
DROP PROCEDURE p; DROP FUNCTION f;
9+
10+
SQL generation and version gating are delegated to the dialect's
11+
``format_create_procedure_statement`` / ``format_create_function_statement``
12+
/ ``format_drop_routine_statement`` formatters. Bodies are passed through
13+
verbatim as raw T-SQL strings.
14+
"""
15+
16+
from typing import Any, List, Optional, TYPE_CHECKING
17+
18+
from rhosocial.activerecord.backend.expression.bases import BaseExpression, SQLQueryAndParams
19+
20+
if TYPE_CHECKING:
21+
from ...dialect import SQLServerDialect
22+
23+
24+
class SQLServerCreateProcedureExpression(BaseExpression):
25+
"""SQL Server CREATE PROCEDURE statement (2005+).
26+
27+
Attributes:
28+
name: Procedure name (may be schema-qualified like ``dbo.p``).
29+
parameters: List of parameters. Each entry is either a raw
30+
``"@x INT"`` string or a ``{"name": "@x", "type": "INT"}`` dict.
31+
body: Raw T-SQL body passed through verbatim.
32+
schema: Optional schema qualifier (rendered as ``[schema].[name]``).
33+
or_alter: Whether to render ``CREATE OR ALTER PROCEDURE``.
34+
35+
Example:
36+
>>> expr = SQLServerCreateProcedureExpression(
37+
... dialect, "p", ["@x INT"], body="SET NOCOUNT ON; SELECT @x + 1;",
38+
... schema="dbo",
39+
... )
40+
>>> sql, params = expr.to_sql()
41+
>>> assert sql == "CREATE PROCEDURE [dbo].[p] @x INT AS BEGIN SET NOCOUNT ON; SELECT @x + 1; END;"
42+
"""
43+
44+
def __init__(
45+
self,
46+
dialect: "SQLServerDialect",
47+
name: str,
48+
parameters: Optional[List[Any]] = None,
49+
body: str = "",
50+
*,
51+
schema: Optional[str] = None,
52+
or_alter: bool = False,
53+
):
54+
super().__init__(dialect)
55+
self.name = name
56+
self.parameters = list(parameters or [])
57+
self.body = body
58+
self.schema = schema
59+
self.or_alter = or_alter
60+
61+
def validate(self, strict: bool = True) -> None:
62+
if not strict:
63+
return
64+
if not self.name:
65+
raise ValueError("procedure name is required")
66+
if not self.body:
67+
raise ValueError("procedure body is required")
68+
69+
def to_sql(self) -> SQLQueryAndParams:
70+
return self.dialect.format_create_procedure_statement(self)
71+
72+
73+
class SQLServerCreateFunctionExpression(BaseExpression):
74+
"""SQL Server CREATE FUNCTION (scalar) statement (2005+).
75+
76+
Attributes:
77+
name: Function name (may be schema-qualified like ``dbo.f``).
78+
parameters: List of parameters (see
79+
``SQLServerCreateProcedureExpression``).
80+
returns: Scalar return data type, e.g. ``INT``.
81+
body: Raw T-SQL body passed through verbatim.
82+
schema: Optional schema qualifier.
83+
or_alter: Whether to render ``CREATE OR ALTER FUNCTION``.
84+
85+
Example:
86+
>>> expr = SQLServerCreateFunctionExpression(
87+
... dialect, "f", ["@x INT"], returns="INT",
88+
... body="RETURN @x * 2;", schema="dbo",
89+
... )
90+
>>> sql, params = expr.to_sql()
91+
>>> assert sql == "CREATE FUNCTION [dbo].[f] (@x INT) RETURNS INT AS BEGIN RETURN @x * 2; END;"
92+
"""
93+
94+
def __init__(
95+
self,
96+
dialect: "SQLServerDialect",
97+
name: str,
98+
parameters: Optional[List[Any]] = None,
99+
returns: Optional[str] = None,
100+
body: str = "",
101+
*,
102+
schema: Optional[str] = None,
103+
or_alter: bool = False,
104+
):
105+
super().__init__(dialect)
106+
self.name = name
107+
self.parameters = list(parameters or [])
108+
self.returns = returns
109+
self.body = body
110+
self.schema = schema
111+
self.or_alter = or_alter
112+
113+
def validate(self, strict: bool = True) -> None:
114+
if not strict:
115+
return
116+
if not self.name:
117+
raise ValueError("function name is required")
118+
if not self.returns:
119+
raise ValueError("RETURNS type is required for a scalar function")
120+
if not self.body:
121+
raise ValueError("function body is required")
122+
123+
def to_sql(self) -> SQLQueryAndParams:
124+
return self.dialect.format_create_function_statement(self)
125+
126+
127+
class SQLServerDropRoutineExpression(BaseExpression):
128+
"""SQL Server DROP PROCEDURE / DROP FUNCTION statement (2005+).
129+
130+
Attributes:
131+
name: Routine name (may be schema-qualified).
132+
kind: Either ``PROCEDURE`` or ``FUNCTION``.
133+
schema: Optional schema qualifier.
134+
if_exists: Render ``IF EXISTS`` (SQL Server 2016+ only).
135+
136+
Example:
137+
>>> expr = SQLServerDropRoutineExpression(dialect, "p", "PROCEDURE")
138+
>>> sql, params = expr.to_sql()
139+
>>> assert sql == "DROP PROCEDURE [p]"
140+
"""
141+
142+
def __init__(
143+
self,
144+
dialect: "SQLServerDialect",
145+
name: str,
146+
kind: str = "PROCEDURE",
147+
*,
148+
schema: Optional[str] = None,
149+
if_exists: bool = False,
150+
):
151+
super().__init__(dialect)
152+
self.name = name
153+
self.kind = kind
154+
self.schema = schema
155+
self.if_exists = if_exists
156+
157+
def validate(self, strict: bool = True) -> None:
158+
if not strict:
159+
return
160+
if not self.name:
161+
raise ValueError("routine name is required")
162+
if self.kind.upper() not in ("PROCEDURE", "FUNCTION"):
163+
raise ValueError("kind must be either PROCEDURE or FUNCTION")
164+
165+
def to_sql(self) -> SQLQueryAndParams:
166+
return self.dialect.format_drop_routine_statement(self)

0 commit comments

Comments
 (0)