SQLAlchemy 2.1.0rc1__cp315-cp315-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. sqlalchemy/__init__.py +299 -0
  2. sqlalchemy/connectors/__init__.py +18 -0
  3. sqlalchemy/connectors/aioodbc.py +171 -0
  4. sqlalchemy/connectors/asyncio.py +476 -0
  5. sqlalchemy/connectors/pyodbc.py +248 -0
  6. sqlalchemy/dialects/__init__.py +62 -0
  7. sqlalchemy/dialects/_typing.py +29 -0
  8. sqlalchemy/dialects/mssql/__init__.py +88 -0
  9. sqlalchemy/dialects/mssql/aioodbc.py +63 -0
  10. sqlalchemy/dialects/mssql/base.py +4833 -0
  11. sqlalchemy/dialects/mssql/information_schema.py +345 -0
  12. sqlalchemy/dialects/mssql/json.py +140 -0
  13. sqlalchemy/dialects/mssql/mssqlpython.py +242 -0
  14. sqlalchemy/dialects/mssql/provision.py +196 -0
  15. sqlalchemy/dialects/mssql/pymssql.py +130 -0
  16. sqlalchemy/dialects/mssql/pyodbc.py +697 -0
  17. sqlalchemy/dialects/mysql/__init__.py +106 -0
  18. sqlalchemy/dialects/mysql/_mariadb_shim.py +312 -0
  19. sqlalchemy/dialects/mysql/aiomysql.py +260 -0
  20. sqlalchemy/dialects/mysql/asyncmy.py +241 -0
  21. sqlalchemy/dialects/mysql/base.py +3896 -0
  22. sqlalchemy/dialects/mysql/cymysql.py +107 -0
  23. sqlalchemy/dialects/mysql/dml.py +279 -0
  24. sqlalchemy/dialects/mysql/enumerated.py +277 -0
  25. sqlalchemy/dialects/mysql/expression.py +146 -0
  26. sqlalchemy/dialects/mysql/json.py +92 -0
  27. sqlalchemy/dialects/mysql/mariadb.py +67 -0
  28. sqlalchemy/dialects/mysql/mariadbconnector.py +314 -0
  29. sqlalchemy/dialects/mysql/mysqlconnector.py +291 -0
  30. sqlalchemy/dialects/mysql/mysqldb.py +318 -0
  31. sqlalchemy/dialects/mysql/provision.py +153 -0
  32. sqlalchemy/dialects/mysql/pymysql.py +188 -0
  33. sqlalchemy/dialects/mysql/pyodbc.py +157 -0
  34. sqlalchemy/dialects/mysql/reflection.py +724 -0
  35. sqlalchemy/dialects/mysql/reserved_words.py +570 -0
  36. sqlalchemy/dialects/mysql/types.py +845 -0
  37. sqlalchemy/dialects/oracle/__init__.py +85 -0
  38. sqlalchemy/dialects/oracle/base.py +3847 -0
  39. sqlalchemy/dialects/oracle/cx_oracle.py +1736 -0
  40. sqlalchemy/dialects/oracle/dictionary.py +507 -0
  41. sqlalchemy/dialects/oracle/json.py +157 -0
  42. sqlalchemy/dialects/oracle/oracledb.py +898 -0
  43. sqlalchemy/dialects/oracle/provision.py +288 -0
  44. sqlalchemy/dialects/oracle/types.py +367 -0
  45. sqlalchemy/dialects/oracle/vector.py +366 -0
  46. sqlalchemy/dialects/postgresql/__init__.py +170 -0
  47. sqlalchemy/dialects/postgresql/_psycopg_common.py +232 -0
  48. sqlalchemy/dialects/postgresql/array.py +534 -0
  49. sqlalchemy/dialects/postgresql/asyncpg.py +1318 -0
  50. sqlalchemy/dialects/postgresql/base.py +5935 -0
  51. sqlalchemy/dialects/postgresql/bitstring.py +327 -0
  52. sqlalchemy/dialects/postgresql/dml.py +360 -0
  53. sqlalchemy/dialects/postgresql/ext.py +599 -0
  54. sqlalchemy/dialects/postgresql/hstore.py +422 -0
  55. sqlalchemy/dialects/postgresql/json.py +411 -0
  56. sqlalchemy/dialects/postgresql/named_types.py +535 -0
  57. sqlalchemy/dialects/postgresql/operators.py +129 -0
  58. sqlalchemy/dialects/postgresql/pg8000.py +655 -0
  59. sqlalchemy/dialects/postgresql/pg_catalog.py +345 -0
  60. sqlalchemy/dialects/postgresql/provision.py +202 -0
  61. sqlalchemy/dialects/postgresql/psycopg.py +800 -0
  62. sqlalchemy/dialects/postgresql/psycopg2.py +860 -0
  63. sqlalchemy/dialects/postgresql/psycopg2cffi.py +62 -0
  64. sqlalchemy/dialects/postgresql/ranges.py +1002 -0
  65. sqlalchemy/dialects/postgresql/types.py +388 -0
  66. sqlalchemy/dialects/sqlite/__init__.py +59 -0
  67. sqlalchemy/dialects/sqlite/aiosqlite.py +375 -0
  68. sqlalchemy/dialects/sqlite/base.py +3103 -0
  69. sqlalchemy/dialects/sqlite/dml.py +314 -0
  70. sqlalchemy/dialects/sqlite/json.py +134 -0
  71. sqlalchemy/dialects/sqlite/provision.py +237 -0
  72. sqlalchemy/dialects/sqlite/pysqlcipher.py +166 -0
  73. sqlalchemy/dialects/sqlite/pysqlite.py +959 -0
  74. sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
  75. sqlalchemy/engine/__init__.py +62 -0
  76. sqlalchemy/engine/_processors_cy.cp315-win_arm64.pyd +0 -0
  77. sqlalchemy/engine/_processors_cy.py +92 -0
  78. sqlalchemy/engine/_result_cy.cp315-win_arm64.pyd +0 -0
  79. sqlalchemy/engine/_result_cy.py +711 -0
  80. sqlalchemy/engine/_row_cy.cp315-win_arm64.pyd +0 -0
  81. sqlalchemy/engine/_row_cy.py +232 -0
  82. sqlalchemy/engine/_util_cy.cp315-win_arm64.pyd +0 -0
  83. sqlalchemy/engine/_util_cy.py +136 -0
  84. sqlalchemy/engine/base.py +3357 -0
  85. sqlalchemy/engine/characteristics.py +155 -0
  86. sqlalchemy/engine/create.py +877 -0
  87. sqlalchemy/engine/cursor.py +2425 -0
  88. sqlalchemy/engine/default.py +2627 -0
  89. sqlalchemy/engine/events.py +965 -0
  90. sqlalchemy/engine/interfaces.py +3636 -0
  91. sqlalchemy/engine/mock.py +133 -0
  92. sqlalchemy/engine/processors.py +83 -0
  93. sqlalchemy/engine/reflection.py +2141 -0
  94. sqlalchemy/engine/result.py +2012 -0
  95. sqlalchemy/engine/row.py +397 -0
  96. sqlalchemy/engine/strategies.py +16 -0
  97. sqlalchemy/engine/url.py +922 -0
  98. sqlalchemy/engine/util.py +164 -0
  99. sqlalchemy/event/__init__.py +26 -0
  100. sqlalchemy/event/api.py +220 -0
  101. sqlalchemy/event/attr.py +675 -0
  102. sqlalchemy/event/base.py +473 -0
  103. sqlalchemy/event/legacy.py +259 -0
  104. sqlalchemy/event/registry.py +391 -0
  105. sqlalchemy/events.py +17 -0
  106. sqlalchemy/exc.py +939 -0
  107. sqlalchemy/ext/__init__.py +10 -0
  108. sqlalchemy/ext/associationproxy.py +2073 -0
  109. sqlalchemy/ext/asyncio/__init__.py +29 -0
  110. sqlalchemy/ext/asyncio/base.py +281 -0
  111. sqlalchemy/ext/asyncio/engine.py +1487 -0
  112. sqlalchemy/ext/asyncio/exc.py +21 -0
  113. sqlalchemy/ext/asyncio/result.py +994 -0
  114. sqlalchemy/ext/asyncio/scoping.py +1679 -0
  115. sqlalchemy/ext/asyncio/session.py +2006 -0
  116. sqlalchemy/ext/automap.py +1702 -0
  117. sqlalchemy/ext/baked.py +558 -0
  118. sqlalchemy/ext/compiler.py +601 -0
  119. sqlalchemy/ext/declarative/__init__.py +65 -0
  120. sqlalchemy/ext/declarative/extensions.py +561 -0
  121. sqlalchemy/ext/horizontal_shard.py +481 -0
  122. sqlalchemy/ext/hybrid.py +1877 -0
  123. sqlalchemy/ext/indexable.py +364 -0
  124. sqlalchemy/ext/instrumentation.py +450 -0
  125. sqlalchemy/ext/mutable.py +1081 -0
  126. sqlalchemy/ext/orderinglist.py +440 -0
  127. sqlalchemy/ext/serializer.py +184 -0
  128. sqlalchemy/future/__init__.py +17 -0
  129. sqlalchemy/future/engine.py +15 -0
  130. sqlalchemy/inspection.py +188 -0
  131. sqlalchemy/log.py +279 -0
  132. sqlalchemy/orm/__init__.py +176 -0
  133. sqlalchemy/orm/_orm_constructors.py +2694 -0
  134. sqlalchemy/orm/_typing.py +180 -0
  135. sqlalchemy/orm/attributes.py +2868 -0
  136. sqlalchemy/orm/base.py +991 -0
  137. sqlalchemy/orm/bulk_persistence.py +2168 -0
  138. sqlalchemy/orm/clsregistry.py +630 -0
  139. sqlalchemy/orm/collections.py +1569 -0
  140. sqlalchemy/orm/context.py +3475 -0
  141. sqlalchemy/orm/decl_api.py +2283 -0
  142. sqlalchemy/orm/decl_base.py +2320 -0
  143. sqlalchemy/orm/dependency.py +1306 -0
  144. sqlalchemy/orm/descriptor_props.py +1183 -0
  145. sqlalchemy/orm/dynamic.py +306 -0
  146. sqlalchemy/orm/evaluator.py +378 -0
  147. sqlalchemy/orm/events.py +3387 -0
  148. sqlalchemy/orm/exc.py +237 -0
  149. sqlalchemy/orm/identity.py +302 -0
  150. sqlalchemy/orm/instrumentation.py +749 -0
  151. sqlalchemy/orm/interfaces.py +1595 -0
  152. sqlalchemy/orm/loading.py +1712 -0
  153. sqlalchemy/orm/mapped_collection.py +557 -0
  154. sqlalchemy/orm/mapper.py +4465 -0
  155. sqlalchemy/orm/path_registry.py +907 -0
  156. sqlalchemy/orm/persistence.py +1790 -0
  157. sqlalchemy/orm/properties.py +972 -0
  158. sqlalchemy/orm/query.py +3528 -0
  159. sqlalchemy/orm/relationships.py +3608 -0
  160. sqlalchemy/orm/scoping.py +2233 -0
  161. sqlalchemy/orm/session.py +5468 -0
  162. sqlalchemy/orm/state.py +1175 -0
  163. sqlalchemy/orm/state_changes.py +196 -0
  164. sqlalchemy/orm/strategies.py +3552 -0
  165. sqlalchemy/orm/strategy_options.py +2648 -0
  166. sqlalchemy/orm/sync.py +164 -0
  167. sqlalchemy/orm/unitofwork.py +797 -0
  168. sqlalchemy/orm/util.py +2461 -0
  169. sqlalchemy/orm/writeonly.py +701 -0
  170. sqlalchemy/pool/__init__.py +41 -0
  171. sqlalchemy/pool/base.py +1540 -0
  172. sqlalchemy/pool/events.py +375 -0
  173. sqlalchemy/pool/impl.py +583 -0
  174. sqlalchemy/py.typed +0 -0
  175. sqlalchemy/schema.py +75 -0
  176. sqlalchemy/sql/__init__.py +156 -0
  177. sqlalchemy/sql/_annotated_cols.py +402 -0
  178. sqlalchemy/sql/_cache_key_cy.cp315-win_arm64.pyd +0 -0
  179. sqlalchemy/sql/_cache_key_cy.py +363 -0
  180. sqlalchemy/sql/_dml_constructors.py +132 -0
  181. sqlalchemy/sql/_elements_constructors.py +2190 -0
  182. sqlalchemy/sql/_orm_types.py +19 -0
  183. sqlalchemy/sql/_selectable_constructors.py +840 -0
  184. sqlalchemy/sql/_typing.py +500 -0
  185. sqlalchemy/sql/_util_cy.cp315-win_arm64.pyd +0 -0
  186. sqlalchemy/sql/_util_cy.pxd +11 -0
  187. sqlalchemy/sql/_util_cy.py +127 -0
  188. sqlalchemy/sql/annotation.py +590 -0
  189. sqlalchemy/sql/base.py +2702 -0
  190. sqlalchemy/sql/cache_key.py +915 -0
  191. sqlalchemy/sql/coercions.py +1373 -0
  192. sqlalchemy/sql/compiler.py +8453 -0
  193. sqlalchemy/sql/crud.py +1816 -0
  194. sqlalchemy/sql/ddl.py +1962 -0
  195. sqlalchemy/sql/default_comparator.py +660 -0
  196. sqlalchemy/sql/dml.py +2018 -0
  197. sqlalchemy/sql/elements.py +6057 -0
  198. sqlalchemy/sql/events.py +458 -0
  199. sqlalchemy/sql/expression.py +171 -0
  200. sqlalchemy/sql/functions.py +2380 -0
  201. sqlalchemy/sql/lambdas.py +1442 -0
  202. sqlalchemy/sql/naming.py +204 -0
  203. sqlalchemy/sql/operators.py +2909 -0
  204. sqlalchemy/sql/roles.py +332 -0
  205. sqlalchemy/sql/schema.py +7075 -0
  206. sqlalchemy/sql/selectable.py +7634 -0
  207. sqlalchemy/sql/sqltypes.py +4130 -0
  208. sqlalchemy/sql/traversals.py +1041 -0
  209. sqlalchemy/sql/type_api.py +2450 -0
  210. sqlalchemy/sql/util.py +1496 -0
  211. sqlalchemy/sql/visitors.py +1153 -0
  212. sqlalchemy/testing/__init__.py +97 -0
  213. sqlalchemy/testing/assertions.py +1007 -0
  214. sqlalchemy/testing/assertsql.py +519 -0
  215. sqlalchemy/testing/asyncio.py +128 -0
  216. sqlalchemy/testing/cancellation.py +237 -0
  217. sqlalchemy/testing/config.py +440 -0
  218. sqlalchemy/testing/engines.py +482 -0
  219. sqlalchemy/testing/entities.py +117 -0
  220. sqlalchemy/testing/exclusions.py +501 -0
  221. sqlalchemy/testing/fixtures/__init__.py +30 -0
  222. sqlalchemy/testing/fixtures/base.py +426 -0
  223. sqlalchemy/testing/fixtures/mypy.py +247 -0
  224. sqlalchemy/testing/fixtures/orm.py +227 -0
  225. sqlalchemy/testing/fixtures/sql.py +538 -0
  226. sqlalchemy/testing/pickleable.py +155 -0
  227. sqlalchemy/testing/plugin/__init__.py +6 -0
  228. sqlalchemy/testing/plugin/bootstrap.py +50 -0
  229. sqlalchemy/testing/plugin/plugin_base.py +828 -0
  230. sqlalchemy/testing/plugin/pytestplugin.py +896 -0
  231. sqlalchemy/testing/profiles_file.py +350 -0
  232. sqlalchemy/testing/profiling.py +294 -0
  233. sqlalchemy/testing/provision.py +633 -0
  234. sqlalchemy/testing/requirements.py +1971 -0
  235. sqlalchemy/testing/schema.py +198 -0
  236. sqlalchemy/testing/suite/__init__.py +19 -0
  237. sqlalchemy/testing/suite/test_cte.py +237 -0
  238. sqlalchemy/testing/suite/test_ddl.py +420 -0
  239. sqlalchemy/testing/suite/test_dialect.py +776 -0
  240. sqlalchemy/testing/suite/test_insert.py +630 -0
  241. sqlalchemy/testing/suite/test_reflection.py +3815 -0
  242. sqlalchemy/testing/suite/test_results.py +660 -0
  243. sqlalchemy/testing/suite/test_rowcount.py +258 -0
  244. sqlalchemy/testing/suite/test_select.py +2112 -0
  245. sqlalchemy/testing/suite/test_sequence.py +317 -0
  246. sqlalchemy/testing/suite/test_table_via_select.py +686 -0
  247. sqlalchemy/testing/suite/test_types.py +2271 -0
  248. sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
  249. sqlalchemy/testing/suite/test_update_delete.py +139 -0
  250. sqlalchemy/testing/util.py +575 -0
  251. sqlalchemy/testing/warnings.py +52 -0
  252. sqlalchemy/types.py +75 -0
  253. sqlalchemy/util/__init__.py +165 -0
  254. sqlalchemy/util/_collections.py +688 -0
  255. sqlalchemy/util/_collections_cy.cp315-win_arm64.pyd +0 -0
  256. sqlalchemy/util/_collections_cy.pxd +8 -0
  257. sqlalchemy/util/_collections_cy.py +516 -0
  258. sqlalchemy/util/_has_cython.py +48 -0
  259. sqlalchemy/util/_immutabledict_cy.cp315-win_arm64.pyd +0 -0
  260. sqlalchemy/util/_immutabledict_cy.py +240 -0
  261. sqlalchemy/util/compat.py +298 -0
  262. sqlalchemy/util/concurrency.py +272 -0
  263. sqlalchemy/util/cython.py +95 -0
  264. sqlalchemy/util/deprecations.py +401 -0
  265. sqlalchemy/util/langhelpers.py +2797 -0
  266. sqlalchemy/util/preloaded.py +153 -0
  267. sqlalchemy/util/queue.py +304 -0
  268. sqlalchemy/util/tool_support.py +202 -0
  269. sqlalchemy/util/topological.py +120 -0
  270. sqlalchemy/util/typing.py +709 -0
  271. sqlalchemy-2.1.0rc1.dist-info/METADATA +270 -0
  272. sqlalchemy-2.1.0rc1.dist-info/RECORD +276 -0
  273. sqlalchemy-2.1.0rc1.dist-info/WHEEL +5 -0
  274. sqlalchemy-2.1.0rc1.dist-info/licenses/AUTHORS +30 -0
  275. sqlalchemy-2.1.0rc1.dist-info/licenses/LICENSE +19 -0
  276. sqlalchemy-2.1.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,4833 @@
1
+ # dialects/mssql/base.py
2
+ # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of SQLAlchemy and is released under
6
+ # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
+ # mypy: ignore-errors
8
+
9
+ """
10
+ .. dialect:: mssql
11
+ :name: Microsoft SQL Server
12
+ :normal_support: 2012+
13
+ :best_effort: 2005+
14
+
15
+ .. _mssql_external_dialects:
16
+
17
+ External Dialects
18
+ -----------------
19
+
20
+ In addition to the above DBAPI layers with native SQLAlchemy support, there
21
+ are third-party dialects for other DBAPI layers that are compatible
22
+ with SQL Server. See the "External Dialects" list on the
23
+ :ref:`dialect_toplevel` page.
24
+
25
+ .. _mssql_identity:
26
+
27
+ Auto Increment Behavior / IDENTITY Columns
28
+ ------------------------------------------
29
+
30
+ SQL Server provides so-called "auto incrementing" behavior using the
31
+ ``IDENTITY`` construct, which can be placed on any single integer column in a
32
+ table. SQLAlchemy considers ``IDENTITY`` within its default "autoincrement"
33
+ behavior for an integer primary key column, described at
34
+ :paramref:`_schema.Column.autoincrement`. This means that by default,
35
+ the first integer primary key column in a :class:`_schema.Table` will be
36
+ considered to be the identity column - unless it is associated with a
37
+ :class:`.Sequence` - and will generate DDL as such::
38
+
39
+ from sqlalchemy import Table, MetaData, Column, Integer
40
+
41
+ m = MetaData()
42
+ t = Table(
43
+ "t",
44
+ m,
45
+ Column("id", Integer, primary_key=True),
46
+ Column("x", Integer),
47
+ )
48
+ m.create_all(engine)
49
+
50
+ The above example will generate DDL as:
51
+
52
+ .. sourcecode:: sql
53
+
54
+ CREATE TABLE t (
55
+ id INTEGER NOT NULL IDENTITY,
56
+ x INTEGER NULL,
57
+ PRIMARY KEY (id)
58
+ )
59
+
60
+ For the case where this default generation of ``IDENTITY`` is not desired,
61
+ specify ``False`` for the :paramref:`_schema.Column.autoincrement` flag,
62
+ on the first integer primary key column::
63
+
64
+ m = MetaData()
65
+ t = Table(
66
+ "t",
67
+ m,
68
+ Column("id", Integer, primary_key=True, autoincrement=False),
69
+ Column("x", Integer),
70
+ )
71
+ m.create_all(engine)
72
+
73
+ To add the ``IDENTITY`` keyword to a non-primary key column, specify
74
+ ``True`` for the :paramref:`_schema.Column.autoincrement` flag on the desired
75
+ :class:`_schema.Column` object, and ensure that
76
+ :paramref:`_schema.Column.autoincrement`
77
+ is set to ``False`` on any integer primary key column::
78
+
79
+ m = MetaData()
80
+ t = Table(
81
+ "t",
82
+ m,
83
+ Column("id", Integer, primary_key=True, autoincrement=False),
84
+ Column("x", Integer, autoincrement=True),
85
+ )
86
+ m.create_all(engine)
87
+
88
+ .. versionchanged:: 1.4 Added :class:`_schema.Identity` construct
89
+ in a :class:`_schema.Column` to specify the start and increment
90
+ parameters of an IDENTITY. These replace
91
+ the use of the :class:`.Sequence` object in order to specify these values.
92
+
93
+ .. deprecated:: 1.4
94
+
95
+ The ``mssql_identity_start`` and ``mssql_identity_increment`` parameters
96
+ to :class:`_schema.Column` are deprecated and should we replaced by
97
+ an :class:`_schema.Identity` object. Specifying both ways of configuring
98
+ an IDENTITY will result in a compile error.
99
+ These options are also no longer returned as part of the
100
+ ``dialect_options`` key in :meth:`_reflection.Inspector.get_columns`.
101
+ Use the information in the ``identity`` key instead.
102
+
103
+ .. versionchanged:: 1.4 Removed the ability to use a :class:`.Sequence`
104
+ object to modify IDENTITY characteristics. :class:`.Sequence` objects
105
+ now only manipulate true T-SQL SEQUENCE types.
106
+
107
+ .. note::
108
+
109
+ There can only be one IDENTITY column on the table. When using
110
+ ``autoincrement=True`` to enable the IDENTITY keyword, SQLAlchemy does not
111
+ guard against multiple columns specifying the option simultaneously. The
112
+ SQL Server database will instead reject the ``CREATE TABLE`` statement.
113
+
114
+ .. note::
115
+
116
+ An INSERT statement which attempts to provide a value for a column that is
117
+ marked with IDENTITY will be rejected by SQL Server. In order for the
118
+ value to be accepted, a session-level option "SET IDENTITY_INSERT" must be
119
+ enabled. The SQLAlchemy SQL Server dialect will perform this operation
120
+ automatically when using a core :class:`_expression.Insert`
121
+ construct; if the
122
+ execution specifies a value for the IDENTITY column, the "IDENTITY_INSERT"
123
+ option will be enabled for the span of that statement's invocation.However,
124
+ this scenario is not high performing and should not be relied upon for
125
+ normal use. If a table doesn't actually require IDENTITY behavior in its
126
+ integer primary key column, the keyword should be disabled when creating
127
+ the table by ensuring that ``autoincrement=False`` is set.
128
+
129
+ Controlling "Start" and "Increment"
130
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
131
+
132
+ Specific control over the "start" and "increment" values for
133
+ the ``IDENTITY`` generator are provided using the
134
+ :paramref:`_schema.Identity.start` and :paramref:`_schema.Identity.increment`
135
+ parameters passed to the :class:`_schema.Identity` object::
136
+
137
+ from sqlalchemy import Table, Integer, Column, Identity
138
+
139
+ test = Table(
140
+ "test",
141
+ metadata,
142
+ Column(
143
+ "id", Integer, primary_key=True, Identity(start=100, increment=10)
144
+ ),
145
+ Column("name", String(20)),
146
+ )
147
+
148
+ The CREATE TABLE for the above :class:`_schema.Table` object would be:
149
+
150
+ .. sourcecode:: sql
151
+
152
+ CREATE TABLE test (
153
+ id INTEGER NOT NULL IDENTITY(100,10) PRIMARY KEY,
154
+ name VARCHAR(20) NULL,
155
+ )
156
+
157
+ .. note::
158
+
159
+ The :class:`_schema.Identity` object supports many other parameter in
160
+ addition to ``start`` and ``increment``. These are not supported by
161
+ SQL Server and will be ignored when generating the CREATE TABLE ddl.
162
+
163
+
164
+ Using IDENTITY with Non-Integer numeric types
165
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
166
+
167
+ SQL Server also allows ``IDENTITY`` to be used with ``NUMERIC`` columns. To
168
+ implement this pattern smoothly in SQLAlchemy, the primary datatype of the
169
+ column should remain as ``Integer``, however the underlying implementation
170
+ type deployed to the SQL Server database can be specified as ``Numeric`` using
171
+ :meth:`.TypeEngine.with_variant`::
172
+
173
+ from sqlalchemy import Column
174
+ from sqlalchemy import Integer
175
+ from sqlalchemy import Numeric
176
+ from sqlalchemy import String
177
+ from sqlalchemy.ext.declarative import declarative_base
178
+
179
+ Base = declarative_base()
180
+
181
+
182
+ class TestTable(Base):
183
+ __tablename__ = "test"
184
+ id = Column(
185
+ Integer().with_variant(Numeric(10, 0), "mssql"),
186
+ primary_key=True,
187
+ autoincrement=True,
188
+ )
189
+ name = Column(String)
190
+
191
+ In the above example, ``Integer().with_variant()`` provides clear usage
192
+ information that accurately describes the intent of the code. The general
193
+ restriction that ``autoincrement`` only applies to ``Integer`` is established
194
+ at the metadata level and not at the per-dialect level.
195
+
196
+ When using the above pattern, the primary key identifier that comes back from
197
+ the insertion of a row, which is also the value that would be assigned to an
198
+ ORM object such as ``TestTable`` above, will be an instance of ``Decimal()``
199
+ and not ``int`` when using SQL Server. The numeric return type of the
200
+ :class:`_types.Numeric` type can be changed to return floats by passing False
201
+ to :paramref:`_types.Numeric.asdecimal`. To normalize the return type of the
202
+ above ``Numeric(10, 0)`` to return Python ints (which also support "long"
203
+ integer values in Python 3), use :class:`_types.TypeDecorator` as follows::
204
+
205
+ from sqlalchemy import TypeDecorator
206
+
207
+
208
+ class NumericAsInteger(TypeDecorator):
209
+ "normalize floating point return values into ints"
210
+
211
+ impl = Numeric(10, 0, asdecimal=False)
212
+ cache_ok = True
213
+
214
+ def process_result_value(self, value, dialect):
215
+ if value is not None:
216
+ value = int(value)
217
+ return value
218
+
219
+
220
+ class TestTable(Base):
221
+ __tablename__ = "test"
222
+ id = Column(
223
+ Integer().with_variant(NumericAsInteger, "mssql"),
224
+ primary_key=True,
225
+ autoincrement=True,
226
+ )
227
+ name = Column(String)
228
+
229
+ .. _mssql_insert_behavior:
230
+
231
+ INSERT behavior
232
+ ^^^^^^^^^^^^^^^^
233
+
234
+ Handling of the ``IDENTITY`` column at INSERT time involves two key
235
+ techniques. The most common is being able to fetch the "last inserted value"
236
+ for a given ``IDENTITY`` column, a process which SQLAlchemy performs
237
+ implicitly in many cases, most importantly within the ORM.
238
+
239
+ The process for fetching this value has several variants:
240
+
241
+ * In the vast majority of cases, RETURNING is used in conjunction with INSERT
242
+ statements on SQL Server in order to get newly generated primary key values:
243
+
244
+ .. sourcecode:: sql
245
+
246
+ INSERT INTO t (x) OUTPUT inserted.id VALUES (?)
247
+
248
+ As of SQLAlchemy 2.0, the :ref:`engine_insertmanyvalues` feature is also
249
+ used by default to optimize many-row INSERT statements; for SQL Server
250
+ the feature takes place for both RETURNING and-non RETURNING
251
+ INSERT statements.
252
+
253
+ .. versionchanged:: 2.0.10 The :ref:`engine_insertmanyvalues` feature for
254
+ SQL Server was temporarily disabled for SQLAlchemy version 2.0.9 due to
255
+ issues with row ordering. As of 2.0.10 the feature is re-enabled, with
256
+ special case handling for the unit of work's requirement for RETURNING to
257
+ be ordered.
258
+
259
+ * When RETURNING is not available or has been disabled via
260
+ ``implicit_returning=False``, either the ``scope_identity()`` function or
261
+ the ``@@identity`` variable is used; behavior varies by backend:
262
+
263
+ * when using PyODBC, the phrase ``; select scope_identity()`` will be
264
+ appended to the end of the INSERT statement; a second result set will be
265
+ fetched in order to receive the value. Given a table as::
266
+
267
+ t = Table(
268
+ "t",
269
+ metadata,
270
+ Column("id", Integer, primary_key=True),
271
+ Column("x", Integer),
272
+ implicit_returning=False,
273
+ )
274
+
275
+ an INSERT will look like:
276
+
277
+ .. sourcecode:: sql
278
+
279
+ INSERT INTO t (x) VALUES (?); select scope_identity()
280
+
281
+ * Other dialects such as pymssql will call upon
282
+ ``SELECT scope_identity() AS lastrowid`` subsequent to an INSERT
283
+ statement. If the flag ``use_scope_identity=False`` is passed to
284
+ :func:`_sa.create_engine`,
285
+ the statement ``SELECT @@identity AS lastrowid``
286
+ is used instead.
287
+
288
+ A table that contains an ``IDENTITY`` column will prohibit an INSERT statement
289
+ that refers to the identity column explicitly. The SQLAlchemy dialect will
290
+ detect when an INSERT construct, created using a core
291
+ :func:`_expression.insert`
292
+ construct (not a plain string SQL), refers to the identity column, and
293
+ in this case will emit ``SET IDENTITY_INSERT ON`` prior to the insert
294
+ statement proceeding, and ``SET IDENTITY_INSERT OFF`` subsequent to the
295
+ execution. Given this example::
296
+
297
+ m = MetaData()
298
+ t = Table(
299
+ "t", m, Column("id", Integer, primary_key=True), Column("x", Integer)
300
+ )
301
+ m.create_all(engine)
302
+
303
+ with engine.begin() as conn:
304
+ conn.execute(t.insert(), {"id": 1, "x": 1}, {"id": 2, "x": 2})
305
+
306
+ The above column will be created with IDENTITY, however the INSERT statement
307
+ we emit is specifying explicit values. In the echo output we can see
308
+ how SQLAlchemy handles this:
309
+
310
+ .. sourcecode:: sql
311
+
312
+ CREATE TABLE t (
313
+ id INTEGER NOT NULL IDENTITY(1,1),
314
+ x INTEGER NULL,
315
+ PRIMARY KEY (id)
316
+ )
317
+
318
+ COMMIT
319
+ SET IDENTITY_INSERT t ON
320
+ INSERT INTO t (id, x) VALUES (?, ?)
321
+ ((1, 1), (2, 2))
322
+ SET IDENTITY_INSERT t OFF
323
+ COMMIT
324
+
325
+
326
+
327
+ This is an auxiliary use case suitable for testing and bulk insert scenarios.
328
+
329
+ SEQUENCE support
330
+ ----------------
331
+
332
+ The :class:`.Sequence` object creates "real" sequences, i.e.,
333
+ ``CREATE SEQUENCE``:
334
+
335
+ .. sourcecode:: pycon+sql
336
+
337
+ >>> from sqlalchemy import Sequence
338
+ >>> from sqlalchemy.schema import CreateSequence
339
+ >>> from sqlalchemy.dialects import mssql
340
+ >>> print(
341
+ ... CreateSequence(Sequence("my_seq", start=1)).compile(
342
+ ... dialect=mssql.dialect()
343
+ ... )
344
+ ... )
345
+ {printsql}CREATE SEQUENCE my_seq START WITH 1
346
+
347
+ For integer primary key generation, SQL Server's ``IDENTITY`` construct should
348
+ generally be preferred vs. sequence.
349
+
350
+ .. tip::
351
+
352
+ The default start value for T-SQL is ``-2**63`` instead of 1 as
353
+ in most other SQL databases. Users should explicitly set the
354
+ :paramref:`.Sequence.start` to 1 if that's the expected default::
355
+
356
+ seq = Sequence("my_sequence", start=1)
357
+
358
+ .. versionadded:: 1.4 added SQL Server support for :class:`.Sequence`
359
+
360
+ .. versionchanged:: 2.0 The SQL Server dialect will no longer implicitly
361
+ render "START WITH 1" for ``CREATE SEQUENCE``, which was the behavior
362
+ first implemented in version 1.4.
363
+
364
+ MAX on VARCHAR / NVARCHAR
365
+ -------------------------
366
+
367
+ SQL Server supports the special string "MAX" within the
368
+ :class:`_types.VARCHAR` and :class:`_types.NVARCHAR` datatypes,
369
+ to indicate "maximum length possible". The dialect currently handles this as
370
+ a length of "None" in the base type, rather than supplying a
371
+ dialect-specific version of these types, so that a base type
372
+ specified such as ``VARCHAR(None)`` can assume "unlengthed" behavior on
373
+ more than one backend without using dialect-specific types.
374
+
375
+ To build a SQL Server VARCHAR or NVARCHAR with MAX length, use None::
376
+
377
+ my_table = Table(
378
+ "my_table",
379
+ metadata,
380
+ Column("my_data", VARCHAR(None)),
381
+ Column("my_n_data", NVARCHAR(None)),
382
+ )
383
+
384
+ Collation Support
385
+ -----------------
386
+
387
+ Character collations are supported by the base string types,
388
+ specified by the string argument "collation"::
389
+
390
+ from sqlalchemy import VARCHAR
391
+
392
+ Column("login", VARCHAR(32, collation="Latin1_General_CI_AS"))
393
+
394
+ When such a column is associated with a :class:`_schema.Table`, the
395
+ CREATE TABLE statement for this column will yield:
396
+
397
+ .. sourcecode:: sql
398
+
399
+ login VARCHAR(32) COLLATE Latin1_General_CI_AS NULL
400
+
401
+ LIMIT/OFFSET Support
402
+ --------------------
403
+
404
+ MSSQL has added support for LIMIT / OFFSET as of SQL Server 2012, via the
405
+ "OFFSET n ROWS" and "FETCH NEXT n ROWS" clauses. SQLAlchemy supports these
406
+ syntaxes automatically if SQL Server 2012 or greater is detected.
407
+
408
+ .. versionchanged:: 1.4 support added for SQL Server "OFFSET n ROWS" and
409
+ "FETCH NEXT n ROWS" syntax.
410
+
411
+ For statements that specify only LIMIT and no OFFSET, all versions of SQL
412
+ Server support the TOP keyword. This syntax is used for all SQL Server
413
+ versions when no OFFSET clause is present. A statement such as::
414
+
415
+ select(some_table).limit(5)
416
+
417
+ will render similarly to:
418
+
419
+ .. sourcecode:: sql
420
+
421
+ SELECT TOP 5 col1, col2.. FROM table
422
+
423
+ For versions of SQL Server prior to SQL Server 2012, a statement that uses
424
+ LIMIT and OFFSET, or just OFFSET alone, will be rendered using the
425
+ ``ROW_NUMBER()`` window function. A statement such as::
426
+
427
+ select(some_table).order_by(some_table.c.col3).limit(5).offset(10)
428
+
429
+ will render similarly to:
430
+
431
+ .. sourcecode:: sql
432
+
433
+ SELECT anon_1.col1, anon_1.col2 FROM (SELECT col1, col2,
434
+ ROW_NUMBER() OVER (ORDER BY col3) AS
435
+ mssql_rn FROM table WHERE t.x = :x_1) AS
436
+ anon_1 WHERE mssql_rn > :param_1 AND mssql_rn <= :param_2 + :param_1
437
+
438
+ Note that when using LIMIT and/or OFFSET, whether using the older
439
+ or newer SQL Server syntaxes, the statement must have an ORDER BY as well,
440
+ else a :class:`.CompileError` is raised.
441
+
442
+ .. _mssql_comment_support:
443
+
444
+ DDL Comment Support
445
+ --------------------
446
+
447
+ Comment support, which includes DDL rendering for attributes such as
448
+ :paramref:`_schema.Table.comment` and :paramref:`_schema.Column.comment`, as
449
+ well as the ability to reflect these comments, is supported assuming a
450
+ supported version of SQL Server is in use. If a non-supported version such as
451
+ Azure Synapse is detected at first-connect time (based on the presence
452
+ of the ``fn_listextendedproperty`` SQL function), comment support including
453
+ rendering and table-comment reflection is disabled, as both features rely upon
454
+ SQL Server stored procedures and functions that are not available on all
455
+ backend types.
456
+
457
+ To force comment support to be on or off, bypassing autodetection, set the
458
+ parameter ``supports_comments`` within :func:`_sa.create_engine`::
459
+
460
+ e = create_engine("mssql+pyodbc://u:p@dsn", supports_comments=False)
461
+
462
+ .. versionadded:: 2.0 Added support for table and column comments for
463
+ the SQL Server dialect, including DDL generation and reflection.
464
+
465
+ .. _mssql_isolation_level:
466
+
467
+ Transaction Isolation Level
468
+ ---------------------------
469
+
470
+ All SQL Server dialects support setting of transaction isolation level
471
+ both via a dialect-specific parameter
472
+ :paramref:`_sa.create_engine.isolation_level`
473
+ accepted by :func:`_sa.create_engine`,
474
+ as well as the :paramref:`.Connection.execution_options.isolation_level`
475
+ argument as passed to
476
+ :meth:`_engine.Connection.execution_options`.
477
+ This feature works by issuing the
478
+ command ``SET TRANSACTION ISOLATION LEVEL <level>`` for
479
+ each new connection.
480
+
481
+ To set isolation level using :func:`_sa.create_engine`::
482
+
483
+ engine = create_engine(
484
+ "mssql+pyodbc://scott:tiger@ms_2008", isolation_level="REPEATABLE READ"
485
+ )
486
+
487
+ To set using per-connection execution options::
488
+
489
+ connection = engine.connect()
490
+ connection = connection.execution_options(isolation_level="READ COMMITTED")
491
+
492
+ Valid values for ``isolation_level`` include:
493
+
494
+ * ``AUTOCOMMIT`` - pyodbc / pymssql-specific
495
+ * ``READ COMMITTED``
496
+ * ``READ UNCOMMITTED``
497
+ * ``REPEATABLE READ``
498
+ * ``SERIALIZABLE``
499
+ * ``SNAPSHOT`` - specific to SQL Server
500
+
501
+ There are also more options for isolation level configurations, such as
502
+ "sub-engine" objects linked to a main :class:`_engine.Engine` which each apply
503
+ different isolation level settings. See the discussion at
504
+ :ref:`dbapi_autocommit` for background.
505
+
506
+ .. seealso::
507
+
508
+ :ref:`dbapi_autocommit`
509
+
510
+ .. _mssql_reset_on_return:
511
+
512
+ Temporary Table / Resource Reset for Connection Pooling
513
+ -------------------------------------------------------
514
+
515
+ The :class:`.QueuePool` connection pool implementation used
516
+ by the SQLAlchemy :class:`.Engine` object includes
517
+ :ref:`reset on return <pool_reset_on_return>` behavior that will invoke
518
+ the DBAPI ``.rollback()`` method when connections are returned to the pool.
519
+ While this rollback will clear out the immediate state used by the previous
520
+ transaction, it does not cover a wider range of session-level state, including
521
+ temporary tables as well as other server state such as prepared statement
522
+ handles and statement caches. An undocumented SQL Server procedure known
523
+ as ``sp_reset_connection`` is known to be a workaround for this issue which
524
+ will reset most of the session state that builds up on a connection, including
525
+ temporary tables.
526
+
527
+ To install ``sp_reset_connection`` as the means of performing reset-on-return,
528
+ the :meth:`.PoolEvents.reset` event hook may be used, as demonstrated in the
529
+ example below. The :paramref:`_sa.create_engine.pool_reset_on_return` parameter
530
+ is set to ``None`` so that the custom scheme can replace the default behavior
531
+ completely. The custom hook implementation calls ``.rollback()`` in any case,
532
+ as it's usually important that the DBAPI's own tracking of commit/rollback
533
+ will remain consistent with the state of the transaction::
534
+
535
+ from sqlalchemy import create_engine
536
+ from sqlalchemy import event
537
+
538
+ mssql_engine = create_engine(
539
+ "mssql+pyodbc://scott:tiger^5HHH@mssql2017:1433/test?driver=ODBC+Driver+17+for+SQL+Server",
540
+ # disable default reset-on-return scheme
541
+ pool_reset_on_return=None,
542
+ )
543
+
544
+
545
+ @event.listens_for(mssql_engine, "reset")
546
+ def _reset_mssql(dbapi_connection, connection_record, reset_state):
547
+ if not reset_state.terminate_only:
548
+ dbapi_connection.execute("{call sys.sp_reset_connection}")
549
+
550
+ # so that the DBAPI itself knows that the connection has been
551
+ # reset
552
+ dbapi_connection.rollback()
553
+
554
+ .. versionchanged:: 2.0.0b3 Added additional state arguments to
555
+ the :meth:`.PoolEvents.reset` event and additionally ensured the event
556
+ is invoked for all "reset" occurrences, so that it's appropriate
557
+ as a place for custom "reset" handlers. Previous schemes which
558
+ use the :meth:`.PoolEvents.checkin` handler remain usable as well.
559
+
560
+ .. seealso::
561
+
562
+ :ref:`pool_reset_on_return` - in the :ref:`pooling_toplevel` documentation
563
+
564
+ Nullability
565
+ -----------
566
+ MSSQL has support for three levels of column nullability. The default
567
+ nullability allows nulls and is explicit in the CREATE TABLE
568
+ construct:
569
+
570
+ .. sourcecode:: sql
571
+
572
+ name VARCHAR(20) NULL
573
+
574
+ If ``nullable=None`` is specified then no specification is made. In
575
+ other words the database's configured default is used. This will
576
+ render:
577
+
578
+ .. sourcecode:: sql
579
+
580
+ name VARCHAR(20)
581
+
582
+ If ``nullable`` is ``True`` or ``False`` then the column will be
583
+ ``NULL`` or ``NOT NULL`` respectively.
584
+
585
+ Date / Time Handling
586
+ --------------------
587
+ DATE and TIME are supported. Bind parameters are converted
588
+ to datetime.datetime() objects as required by most MSSQL drivers,
589
+ and results are processed from strings if needed.
590
+ The DATE and TIME types are not available for MSSQL 2005 and
591
+ previous - if a server version below 2008 is detected, DDL
592
+ for these types will be issued as DATETIME.
593
+
594
+ .. _mssql_large_type_deprecation:
595
+
596
+ Large Text/Binary Type Deprecation
597
+ ----------------------------------
598
+
599
+ Per
600
+ `SQL Server 2012/2014 Documentation <https://technet.microsoft.com/en-us/library/ms187993.aspx>`_,
601
+ the ``NTEXT``, ``TEXT`` and ``IMAGE`` datatypes are to be removed from SQL
602
+ Server in a future release. SQLAlchemy normally relates these types to the
603
+ :class:`.UnicodeText`, :class:`_expression.TextClause` and
604
+ :class:`.LargeBinary` datatypes.
605
+
606
+ In order to accommodate this change, a new flag ``deprecate_large_types``
607
+ is added to the dialect, which will be automatically set based on detection
608
+ of the server version in use, if not otherwise set by the user. The
609
+ behavior of this flag is as follows:
610
+
611
+ * When this flag is ``True``, the :class:`.UnicodeText`,
612
+ :class:`_expression.TextClause` and
613
+ :class:`.LargeBinary` datatypes, when used to render DDL, will render the
614
+ types ``NVARCHAR(max)``, ``VARCHAR(max)``, and ``VARBINARY(max)``,
615
+ respectively. This is a new behavior as of the addition of this flag.
616
+
617
+ * When this flag is ``False``, the :class:`.UnicodeText`,
618
+ :class:`_expression.TextClause` and
619
+ :class:`.LargeBinary` datatypes, when used to render DDL, will render the
620
+ types ``NTEXT``, ``TEXT``, and ``IMAGE``,
621
+ respectively. This is the long-standing behavior of these types.
622
+
623
+ * The flag begins with the value ``None``, before a database connection is
624
+ established. If the dialect is used to render DDL without the flag being
625
+ set, it is interpreted the same as ``False``.
626
+
627
+ * On first connection, the dialect detects if SQL Server version 2012 or
628
+ greater is in use; if the flag is still at ``None``, it sets it to ``True``
629
+ or ``False`` based on whether 2012 or greater is detected.
630
+
631
+ * The flag can be set to either ``True`` or ``False`` when the dialect
632
+ is created, typically via :func:`_sa.create_engine`::
633
+
634
+ eng = create_engine(
635
+ "mssql+pymssql://user:pass@host/db", deprecate_large_types=True
636
+ )
637
+
638
+ * Complete control over whether the "old" or "new" types are rendered is
639
+ available in all SQLAlchemy versions by using the UPPERCASE type objects
640
+ instead: :class:`_types.NVARCHAR`, :class:`_types.VARCHAR`,
641
+ :class:`_types.VARBINARY`, :class:`_types.TEXT`, :class:`_mssql.NTEXT`,
642
+ :class:`_mssql.IMAGE`
643
+ will always remain fixed and always output exactly that
644
+ type.
645
+
646
+ .. _multipart_schema_names:
647
+
648
+ Multipart Schema Names
649
+ ----------------------
650
+
651
+ SQL Server schemas sometimes require multiple parts to their "schema"
652
+ qualifier, that is, including the database name and owner name as separate
653
+ tokens, such as ``mydatabase.dbo.some_table``. These multipart names can be set
654
+ at once using the :paramref:`_schema.Table.schema` argument of
655
+ :class:`_schema.Table`::
656
+
657
+ Table(
658
+ "some_table",
659
+ metadata,
660
+ Column("q", String(50)),
661
+ schema="mydatabase.dbo",
662
+ )
663
+
664
+ When performing operations such as table or component reflection, a schema
665
+ argument that contains a dot will be split into separate
666
+ "database" and "owner" components in order to correctly query the SQL
667
+ Server information schema tables, as these two values are stored separately.
668
+ Additionally, when rendering the schema name for DDL or SQL, the two
669
+ components will be quoted separately for case sensitive names and other
670
+ special characters. Given an argument as below::
671
+
672
+ Table(
673
+ "some_table",
674
+ metadata,
675
+ Column("q", String(50)),
676
+ schema="MyDataBase.dbo",
677
+ )
678
+
679
+ The above schema would be rendered as ``[MyDataBase].dbo``, and also in
680
+ reflection, would be reflected using "dbo" as the owner and "MyDataBase"
681
+ as the database name.
682
+
683
+ To control how the schema name is broken into database / owner,
684
+ specify brackets (which in SQL Server are quoting characters) in the name.
685
+ Below, the "owner" will be considered as ``MyDataBase.dbo`` and the
686
+ "database" will be None::
687
+
688
+ Table(
689
+ "some_table",
690
+ metadata,
691
+ Column("q", String(50)),
692
+ schema="[MyDataBase.dbo]",
693
+ )
694
+
695
+ To individually specify both database and owner name with special characters
696
+ or embedded dots, use two sets of brackets::
697
+
698
+ Table(
699
+ "some_table",
700
+ metadata,
701
+ Column("q", String(50)),
702
+ schema="[MyDataBase.Period].[MyOwner.Dot]",
703
+ )
704
+
705
+ .. _legacy_schema_rendering:
706
+
707
+ Legacy Schema Mode
708
+ ------------------
709
+
710
+ Very old versions of the MSSQL dialect introduced the behavior such that a
711
+ schema-qualified table would be auto-aliased when used in a
712
+ SELECT statement; given a table::
713
+
714
+ account_table = Table(
715
+ "account",
716
+ metadata,
717
+ Column("id", Integer, primary_key=True),
718
+ Column("info", String(100)),
719
+ schema="customer_schema",
720
+ )
721
+
722
+ this legacy mode of rendering would assume that "customer_schema.account"
723
+ would not be accepted by all parts of the SQL statement, as illustrated
724
+ below:
725
+
726
+ .. sourcecode:: pycon+sql
727
+
728
+ >>> eng = create_engine("mssql+pymssql://mydsn", legacy_schema_aliasing=True)
729
+ >>> print(account_table.select().compile(eng))
730
+ {printsql}SELECT account_1.id, account_1.info
731
+ FROM customer_schema.account AS account_1
732
+
733
+ This mode of behavior is now off by default, as it appears to have served
734
+ no purpose; however in the case that legacy applications rely upon it,
735
+ it is available using the ``legacy_schema_aliasing`` argument to
736
+ :func:`_sa.create_engine` as illustrated above.
737
+
738
+ .. deprecated:: 1.4
739
+
740
+ The ``legacy_schema_aliasing`` flag is now
741
+ deprecated and will be removed in a future release.
742
+
743
+ .. _mssql_indexes:
744
+
745
+ Clustered Index Support
746
+ -----------------------
747
+
748
+ The MSSQL dialect supports clustered indexes (and primary keys) via the
749
+ ``mssql_clustered`` option. This option is available to :class:`.Index`,
750
+ :class:`.UniqueConstraint`. and :class:`.PrimaryKeyConstraint`.
751
+ For indexes this option can be combined with the ``mssql_columnstore`` one
752
+ to create a clustered columnstore index.
753
+
754
+ To generate a clustered index::
755
+
756
+ Index("my_index", table.c.x, mssql_clustered=True)
757
+
758
+ which renders the index as ``CREATE CLUSTERED INDEX my_index ON table (x)``.
759
+
760
+ To generate a clustered primary key use::
761
+
762
+ Table(
763
+ "my_table",
764
+ metadata,
765
+ Column("x", ...),
766
+ Column("y", ...),
767
+ PrimaryKeyConstraint("x", "y", mssql_clustered=True),
768
+ )
769
+
770
+ which will render the table, for example, as:
771
+
772
+ .. sourcecode:: sql
773
+
774
+ CREATE TABLE my_table (
775
+ x INTEGER NOT NULL,
776
+ y INTEGER NOT NULL,
777
+ PRIMARY KEY CLUSTERED (x, y)
778
+ )
779
+
780
+ Similarly, we can generate a clustered unique constraint using::
781
+
782
+ Table(
783
+ "my_table",
784
+ metadata,
785
+ Column("x", ...),
786
+ Column("y", ...),
787
+ PrimaryKeyConstraint("x"),
788
+ UniqueConstraint("y", mssql_clustered=True),
789
+ )
790
+
791
+ To explicitly request a non-clustered primary key (for example, when
792
+ a separate clustered index is desired), use::
793
+
794
+ Table(
795
+ "my_table",
796
+ metadata,
797
+ Column("x", ...),
798
+ Column("y", ...),
799
+ PrimaryKeyConstraint("x", "y", mssql_clustered=False),
800
+ )
801
+
802
+ which will render the table, for example, as:
803
+
804
+ .. sourcecode:: sql
805
+
806
+ CREATE TABLE my_table (
807
+ x INTEGER NOT NULL,
808
+ y INTEGER NOT NULL,
809
+ PRIMARY KEY NONCLUSTERED (x, y)
810
+ )
811
+
812
+ Columnstore Index Support
813
+ -------------------------
814
+
815
+ The MSSQL dialect supports columnstore indexes via the ``mssql_columnstore``
816
+ option. This option is available to :class:`.Index`. It be combined with
817
+ the ``mssql_clustered`` option to create a clustered columnstore index.
818
+
819
+ To generate a columnstore index::
820
+
821
+ Index("my_index", table.c.x, mssql_columnstore=True)
822
+
823
+ which renders the index as ``CREATE COLUMNSTORE INDEX my_index ON table (x)``.
824
+
825
+ To generate a clustered columnstore index provide no columns::
826
+
827
+ idx = Index("my_index", mssql_clustered=True, mssql_columnstore=True)
828
+ # required to associate the index with the table
829
+ table.append_constraint(idx)
830
+
831
+ the above renders the index as
832
+ ``CREATE CLUSTERED COLUMNSTORE INDEX my_index ON table``.
833
+
834
+ .. versionadded:: 2.0.18
835
+
836
+ MSSQL-Specific Index Options
837
+ -----------------------------
838
+
839
+ In addition to clustering, the MSSQL dialect supports other special options
840
+ for :class:`.Index`.
841
+
842
+ INCLUDE
843
+ ^^^^^^^
844
+
845
+ The ``mssql_include`` option renders INCLUDE(colname) for the given string
846
+ names::
847
+
848
+ Index("my_index", table.c.x, mssql_include=["y"])
849
+
850
+ would render the index as ``CREATE INDEX my_index ON table (x) INCLUDE (y)``
851
+
852
+ .. _mssql_index_where:
853
+
854
+ Filtered Indexes
855
+ ^^^^^^^^^^^^^^^^
856
+
857
+ The ``mssql_where`` option renders WHERE(condition) for the given string
858
+ names::
859
+
860
+ Index("my_index", table.c.x, mssql_where=table.c.x > 10)
861
+
862
+ would render the index as ``CREATE INDEX my_index ON table (x) WHERE x > 10``.
863
+
864
+ Index ordering
865
+ ^^^^^^^^^^^^^^
866
+
867
+ Index ordering is available via functional expressions, such as::
868
+
869
+ Index("my_index", table.c.x.desc())
870
+
871
+ would render the index as ``CREATE INDEX my_index ON table (x DESC)``
872
+
873
+ .. seealso::
874
+
875
+ :ref:`schema_indexes_functional`
876
+
877
+ Compatibility Levels
878
+ --------------------
879
+ MSSQL supports the notion of setting compatibility levels at the
880
+ database level. This allows, for instance, to run a database that
881
+ is compatible with SQL2000 while running on a SQL2005 database
882
+ server. ``server_version_info`` will always return the database
883
+ server version information (in this case SQL2005) and not the
884
+ compatibility level information. Because of this, if running under
885
+ a backwards compatibility mode SQLAlchemy may attempt to use T-SQL
886
+ statements that are unable to be parsed by the database server.
887
+
888
+ .. _mssql_triggers:
889
+
890
+ Triggers
891
+ --------
892
+
893
+ SQLAlchemy by default uses OUTPUT INSERTED to get at newly
894
+ generated primary key values via IDENTITY columns or other
895
+ server side defaults. MS-SQL does not
896
+ allow the usage of OUTPUT INSERTED on tables that have triggers.
897
+ To disable the usage of OUTPUT INSERTED on a per-table basis,
898
+ specify ``implicit_returning=False`` for each :class:`_schema.Table`
899
+ which has triggers::
900
+
901
+ Table(
902
+ "mytable",
903
+ metadata,
904
+ Column("id", Integer, primary_key=True),
905
+ # ...,
906
+ implicit_returning=False,
907
+ )
908
+
909
+ Declarative form::
910
+
911
+ class MyClass(Base):
912
+ # ...
913
+ __table_args__ = {"implicit_returning": False}
914
+
915
+ .. _mssql_rowcount_versioning:
916
+
917
+ Rowcount Support / ORM Versioning
918
+ ---------------------------------
919
+
920
+ The SQL Server drivers may have limited ability to return the number
921
+ of rows updated from an UPDATE or DELETE statement.
922
+
923
+ As of this writing, the PyODBC driver is not able to return a rowcount when
924
+ OUTPUT INSERTED is used. Previous versions of SQLAlchemy therefore had
925
+ limitations for features such as the "ORM Versioning" feature that relies upon
926
+ accurate rowcounts in order to match version numbers with matched rows.
927
+
928
+ SQLAlchemy 2.0 now retrieves the "rowcount" manually for these particular use
929
+ cases based on counting the rows that arrived back within RETURNING; so while
930
+ the driver still has this limitation, the ORM Versioning feature is no longer
931
+ impacted by it. As of SQLAlchemy 2.0.5, ORM versioning has been fully
932
+ re-enabled for the pyodbc driver.
933
+
934
+ .. versionchanged:: 2.0.5 ORM versioning support is restored for the pyodbc
935
+ driver. Previously, a warning would be emitted during ORM flush that
936
+ versioning was not supported.
937
+
938
+
939
+ Enabling Snapshot Isolation
940
+ ---------------------------
941
+
942
+ SQL Server has a default transaction
943
+ isolation mode that locks entire tables, and causes even mildly concurrent
944
+ applications to have long held locks and frequent deadlocks.
945
+ Enabling snapshot isolation for the database as a whole is recommended
946
+ for modern levels of concurrency support. This is accomplished via the
947
+ following ALTER DATABASE commands executed at the SQL prompt:
948
+
949
+ .. sourcecode:: sql
950
+
951
+ ALTER DATABASE MyDatabase SET ALLOW_SNAPSHOT_ISOLATION ON
952
+
953
+ ALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON
954
+
955
+ Background on SQL Server snapshot isolation is available at
956
+ https://msdn.microsoft.com/en-us/library/ms175095.aspx.
957
+
958
+ """ # noqa
959
+
960
+ from __future__ import annotations
961
+
962
+ import codecs
963
+ import datetime
964
+ from functools import lru_cache
965
+ import operator
966
+ import re
967
+ from typing import Any
968
+ from typing import Literal
969
+ from typing import overload
970
+ from typing import TYPE_CHECKING
971
+ from uuid import UUID as _python_UUID
972
+
973
+ from . import information_schema as ischema
974
+ from .json import JSON
975
+ from .json import JSONIndexType
976
+ from .json import JSONPathType
977
+ from ... import exc
978
+ from ... import Identity
979
+ from ... import schema as sa_schema
980
+ from ... import Sequence
981
+ from ... import sql
982
+ from ... import text
983
+ from ... import util
984
+ from ...engine import cursor as _cursor
985
+ from ...engine import default
986
+ from ...engine import ObjectKind
987
+ from ...engine import ObjectScope
988
+ from ...engine import reflection
989
+ from ...engine.reflection import ReflectionDefaults
990
+ from ...sql import coercions
991
+ from ...sql import compiler
992
+ from ...sql import elements
993
+ from ...sql import expression
994
+ from ...sql import quoted_name
995
+ from ...sql import roles
996
+ from ...sql import sqltypes
997
+ from ...sql import try_cast as try_cast # noqa: F401
998
+ from ...sql import util as sql_util
999
+ from ...sql._typing import is_sql_compiler
1000
+ from ...sql.compiler import AggregateOrderByStyle
1001
+ from ...sql.compiler import InsertmanyvaluesSentinelOpts
1002
+ from ...sql.elements import TryCast as TryCast # noqa: F401
1003
+ from ...types import BIGINT
1004
+ from ...types import BINARY
1005
+ from ...types import CHAR
1006
+ from ...types import DATE
1007
+ from ...types import DATETIME
1008
+ from ...types import DECIMAL
1009
+ from ...types import FLOAT
1010
+ from ...types import INTEGER
1011
+ from ...types import NCHAR
1012
+ from ...types import NUMERIC
1013
+ from ...types import NVARCHAR
1014
+ from ...types import SMALLINT
1015
+ from ...types import TEXT
1016
+ from ...types import VARCHAR
1017
+ from ...util import update_wrapper
1018
+
1019
+ if TYPE_CHECKING:
1020
+ from ...sql.ddl import DropIndex
1021
+ from ...sql.dml import DMLState
1022
+ from ...sql.selectable import TableClause
1023
+
1024
+ # https://sqlserverbuilds.blogspot.com/
1025
+ MS_2017_VERSION = (14,)
1026
+ MS_2016_VERSION = (13,)
1027
+ MS_2014_VERSION = (12,)
1028
+ MS_2012_VERSION = (11,)
1029
+ MS_2008_VERSION = (10,)
1030
+ MS_2005_VERSION = (9,)
1031
+ MS_2000_VERSION = (8,)
1032
+
1033
+ RESERVED_WORDS = {
1034
+ "add",
1035
+ "all",
1036
+ "alter",
1037
+ "and",
1038
+ "any",
1039
+ "as",
1040
+ "asc",
1041
+ "authorization",
1042
+ "backup",
1043
+ "begin",
1044
+ "between",
1045
+ "break",
1046
+ "browse",
1047
+ "bulk",
1048
+ "by",
1049
+ "cascade",
1050
+ "case",
1051
+ "check",
1052
+ "checkpoint",
1053
+ "close",
1054
+ "clustered",
1055
+ "coalesce",
1056
+ "collate",
1057
+ "column",
1058
+ "commit",
1059
+ "compute",
1060
+ "constraint",
1061
+ "contains",
1062
+ "containstable",
1063
+ "continue",
1064
+ "convert",
1065
+ "create",
1066
+ "cross",
1067
+ "current",
1068
+ "current_date",
1069
+ "current_time",
1070
+ "current_timestamp",
1071
+ "current_user",
1072
+ "cursor",
1073
+ "database",
1074
+ "dbcc",
1075
+ "deallocate",
1076
+ "declare",
1077
+ "default",
1078
+ "delete",
1079
+ "deny",
1080
+ "desc",
1081
+ "disk",
1082
+ "distinct",
1083
+ "distributed",
1084
+ "double",
1085
+ "drop",
1086
+ "dump",
1087
+ "else",
1088
+ "end",
1089
+ "errlvl",
1090
+ "escape",
1091
+ "except",
1092
+ "exec",
1093
+ "execute",
1094
+ "exists",
1095
+ "exit",
1096
+ "external",
1097
+ "fetch",
1098
+ "file",
1099
+ "fillfactor",
1100
+ "for",
1101
+ "foreign",
1102
+ "freetext",
1103
+ "freetexttable",
1104
+ "from",
1105
+ "full",
1106
+ "function",
1107
+ "goto",
1108
+ "grant",
1109
+ "group",
1110
+ "having",
1111
+ "holdlock",
1112
+ "identity",
1113
+ "identity_insert",
1114
+ "identitycol",
1115
+ "if",
1116
+ "in",
1117
+ "index",
1118
+ "inner",
1119
+ "insert",
1120
+ "intersect",
1121
+ "into",
1122
+ "is",
1123
+ "join",
1124
+ "key",
1125
+ "kill",
1126
+ "left",
1127
+ "like",
1128
+ "lineno",
1129
+ "load",
1130
+ "merge",
1131
+ "national",
1132
+ "nocheck",
1133
+ "nonclustered",
1134
+ "not",
1135
+ "null",
1136
+ "nullif",
1137
+ "of",
1138
+ "off",
1139
+ "offsets",
1140
+ "on",
1141
+ "open",
1142
+ "opendatasource",
1143
+ "openquery",
1144
+ "openrowset",
1145
+ "openxml",
1146
+ "option",
1147
+ "or",
1148
+ "order",
1149
+ "outer",
1150
+ "over",
1151
+ "percent",
1152
+ "pivot",
1153
+ "plan",
1154
+ "precision",
1155
+ "primary",
1156
+ "print",
1157
+ "proc",
1158
+ "procedure",
1159
+ "public",
1160
+ "raiserror",
1161
+ "read",
1162
+ "readtext",
1163
+ "reconfigure",
1164
+ "references",
1165
+ "replication",
1166
+ "restore",
1167
+ "restrict",
1168
+ "return",
1169
+ "revert",
1170
+ "revoke",
1171
+ "right",
1172
+ "rollback",
1173
+ "rowcount",
1174
+ "rowguidcol",
1175
+ "rule",
1176
+ "save",
1177
+ "schema",
1178
+ "securityaudit",
1179
+ "select",
1180
+ "session_user",
1181
+ "set",
1182
+ "setuser",
1183
+ "shutdown",
1184
+ "some",
1185
+ "statistics",
1186
+ "system_user",
1187
+ "table",
1188
+ "tablesample",
1189
+ "textsize",
1190
+ "then",
1191
+ "to",
1192
+ "top",
1193
+ "tran",
1194
+ "transaction",
1195
+ "trigger",
1196
+ "truncate",
1197
+ "tsequal",
1198
+ "union",
1199
+ "unique",
1200
+ "unpivot",
1201
+ "update",
1202
+ "updatetext",
1203
+ "use",
1204
+ "user",
1205
+ "values",
1206
+ "varying",
1207
+ "view",
1208
+ "waitfor",
1209
+ "when",
1210
+ "where",
1211
+ "while",
1212
+ "with",
1213
+ "writetext",
1214
+ }
1215
+
1216
+
1217
+ class REAL(sqltypes.REAL):
1218
+ """the SQL Server REAL datatype."""
1219
+
1220
+ def __init__(self, **kw):
1221
+ # REAL is a synonym for FLOAT(24) on SQL server.
1222
+ # it is only accepted as the word "REAL" in DDL, the numeric
1223
+ # precision value is not allowed to be present
1224
+ kw.setdefault("precision", 24)
1225
+ super().__init__(**kw)
1226
+
1227
+
1228
+ class DOUBLE_PRECISION(sqltypes.DOUBLE_PRECISION):
1229
+ """the SQL Server DOUBLE PRECISION datatype.
1230
+
1231
+ .. versionadded:: 2.0.11
1232
+
1233
+ """
1234
+
1235
+ def __init__(self, **kw):
1236
+ # DOUBLE PRECISION is a synonym for FLOAT(53) on SQL server.
1237
+ # it is only accepted as the word "DOUBLE PRECISION" in DDL,
1238
+ # the numeric precision value is not allowed to be present
1239
+ kw.setdefault("precision", 53)
1240
+ super().__init__(**kw)
1241
+
1242
+
1243
+ class TINYINT(sqltypes.Integer):
1244
+ __visit_name__ = "TINYINT"
1245
+
1246
+
1247
+ # MSSQL DATE/TIME types have varied behavior, sometimes returning
1248
+ # strings. MSDate/TIME check for everything, and always
1249
+ # filter bind parameters into datetime objects (required by pyodbc,
1250
+ # not sure about other dialects).
1251
+
1252
+
1253
+ class _MSDate(sqltypes.Date):
1254
+ def bind_processor(self, dialect):
1255
+ def process(value):
1256
+ if type(value) == datetime.date:
1257
+ return datetime.datetime(value.year, value.month, value.day)
1258
+ else:
1259
+ return value
1260
+
1261
+ return process
1262
+
1263
+ _reg = re.compile(r"(\d+)-(\d+)-(\d+)")
1264
+
1265
+ def result_processor(self, dialect, coltype):
1266
+ def process(value):
1267
+ if isinstance(value, datetime.datetime):
1268
+ return value.date()
1269
+ elif isinstance(value, str):
1270
+ m = self._reg.match(value)
1271
+ if not m:
1272
+ raise ValueError(
1273
+ "could not parse %r as a date value" % (value,)
1274
+ )
1275
+ return datetime.date(*[int(x or 0) for x in m.groups()])
1276
+ else:
1277
+ return value
1278
+
1279
+ return process
1280
+
1281
+
1282
+ class TIME(sqltypes.TIME):
1283
+ def __init__(self, precision=None, **kwargs):
1284
+ self.precision = precision
1285
+ super().__init__()
1286
+
1287
+ __zero_date = datetime.date(1900, 1, 1)
1288
+
1289
+ def bind_processor(self, dialect):
1290
+ def process(value):
1291
+ if isinstance(value, datetime.datetime):
1292
+ value = datetime.datetime.combine(
1293
+ self.__zero_date, value.time()
1294
+ )
1295
+ elif isinstance(value, datetime.time):
1296
+ """issue #5339
1297
+ per: https://github.com/mkleehammer/pyodbc/wiki/Tips-and-Tricks-by-Database-Platform#time-columns
1298
+ pass TIME value as string
1299
+ """ # noqa
1300
+ value = str(value)
1301
+ return value
1302
+
1303
+ return process
1304
+
1305
+ _reg = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d{0,6}))?")
1306
+
1307
+ def result_processor(self, dialect, coltype):
1308
+ def process(value):
1309
+ if isinstance(value, datetime.datetime):
1310
+ return value.time()
1311
+ elif isinstance(value, str):
1312
+ m = self._reg.match(value)
1313
+ if not m:
1314
+ raise ValueError(
1315
+ "could not parse %r as a time value" % (value,)
1316
+ )
1317
+ return datetime.time(*[int(x or 0) for x in m.groups()])
1318
+ else:
1319
+ return value
1320
+
1321
+ return process
1322
+
1323
+
1324
+ _MSTime = TIME
1325
+
1326
+
1327
+ class _BASETIMEIMPL(TIME):
1328
+ __visit_name__ = "_BASETIMEIMPL"
1329
+
1330
+
1331
+ class _DateTimeBase:
1332
+ def bind_processor(self, dialect):
1333
+ def process(value):
1334
+ if type(value) == datetime.date:
1335
+ return datetime.datetime(value.year, value.month, value.day)
1336
+ else:
1337
+ return value
1338
+
1339
+ return process
1340
+
1341
+
1342
+ class _MSDateTime(_DateTimeBase, sqltypes.DateTime):
1343
+ pass
1344
+
1345
+
1346
+ class SMALLDATETIME(_DateTimeBase, sqltypes.DateTime):
1347
+ __visit_name__ = "SMALLDATETIME"
1348
+
1349
+
1350
+ class DATETIME2(_DateTimeBase, sqltypes.DateTime):
1351
+ __visit_name__ = "DATETIME2"
1352
+
1353
+ def __init__(self, precision=None, **kw):
1354
+ super().__init__(**kw)
1355
+ self.precision = precision
1356
+
1357
+
1358
+ class DATETIMEOFFSET(_DateTimeBase, sqltypes.DateTime):
1359
+ __visit_name__ = "DATETIMEOFFSET"
1360
+
1361
+ def __init__(self, precision=None, **kw):
1362
+ super().__init__(**kw)
1363
+ self.precision = precision
1364
+
1365
+
1366
+ class _UnicodeLiteral:
1367
+ def literal_processor(self, dialect):
1368
+ def process(value):
1369
+ value = value.replace("'", "''")
1370
+
1371
+ if dialect.identifier_preparer._double_percents:
1372
+ value = value.replace("%", "%%")
1373
+
1374
+ return "N'%s'" % value
1375
+
1376
+ return process
1377
+
1378
+
1379
+ class _MSUnicode(_UnicodeLiteral, sqltypes.Unicode):
1380
+ pass
1381
+
1382
+
1383
+ class _MSUnicodeText(_UnicodeLiteral, sqltypes.UnicodeText):
1384
+ pass
1385
+
1386
+
1387
+ class TIMESTAMP(sqltypes._Binary):
1388
+ """Implement the SQL Server TIMESTAMP type.
1389
+
1390
+ Note this is **completely different** than the SQL Standard
1391
+ TIMESTAMP type, which is not supported by SQL Server. It
1392
+ is a read-only datatype that does not support INSERT of values.
1393
+
1394
+ .. seealso::
1395
+
1396
+ :class:`_mssql.ROWVERSION`
1397
+
1398
+ """
1399
+
1400
+ __visit_name__ = "TIMESTAMP"
1401
+
1402
+ # expected by _Binary to be present
1403
+ length = None
1404
+
1405
+ def __init__(self, convert_int=False):
1406
+ """Construct a TIMESTAMP or ROWVERSION type.
1407
+
1408
+ :param convert_int: if True, binary integer values will
1409
+ be converted to integers on read.
1410
+
1411
+ """
1412
+ self.convert_int = convert_int
1413
+
1414
+ def result_processor(self, dialect, coltype):
1415
+ super_ = super().result_processor(dialect, coltype)
1416
+ if self.convert_int:
1417
+
1418
+ def process(value):
1419
+ if super_:
1420
+ value = super_(value)
1421
+ if value is not None:
1422
+ # https://stackoverflow.com/a/30403242/34549
1423
+ value = int(codecs.encode(value, "hex"), 16)
1424
+ return value
1425
+
1426
+ return process
1427
+ else:
1428
+ return super_
1429
+
1430
+
1431
+ class ROWVERSION(TIMESTAMP):
1432
+ """Implement the SQL Server ROWVERSION type.
1433
+
1434
+ The ROWVERSION datatype is a SQL Server synonym for the TIMESTAMP
1435
+ datatype, however current SQL Server documentation suggests using
1436
+ ROWVERSION for new datatypes going forward.
1437
+
1438
+ The ROWVERSION datatype does **not** reflect (e.g. introspect) from the
1439
+ database as itself; the returned datatype will be
1440
+ :class:`_mssql.TIMESTAMP`.
1441
+
1442
+ This is a read-only datatype that does not support INSERT of values.
1443
+
1444
+ .. seealso::
1445
+
1446
+ :class:`_mssql.TIMESTAMP`
1447
+
1448
+ """
1449
+
1450
+ __visit_name__ = "ROWVERSION"
1451
+
1452
+
1453
+ class NTEXT(sqltypes.UnicodeText):
1454
+ """MSSQL NTEXT type, for variable-length unicode text up to 2^30
1455
+ characters."""
1456
+
1457
+ __visit_name__ = "NTEXT"
1458
+
1459
+
1460
+ class VARBINARY(sqltypes.VARBINARY, sqltypes.LargeBinary):
1461
+ """The MSSQL VARBINARY type.
1462
+
1463
+ This type adds additional features to the core :class:`_types.VARBINARY`
1464
+ type, including "deprecate_large_types" mode where
1465
+ either ``VARBINARY(max)`` or IMAGE is rendered, as well as the SQL
1466
+ Server ``FILESTREAM`` option.
1467
+
1468
+ .. seealso::
1469
+
1470
+ :ref:`mssql_large_type_deprecation`
1471
+
1472
+ """
1473
+
1474
+ __visit_name__ = "VARBINARY"
1475
+
1476
+ def __init__(self, length=None, filestream=False):
1477
+ """
1478
+ Construct a VARBINARY type.
1479
+
1480
+ :param length: optional, a length for the column for use in
1481
+ DDL statements, for those binary types that accept a length,
1482
+ such as the MySQL BLOB type.
1483
+
1484
+ :param filestream=False: if True, renders the ``FILESTREAM`` keyword
1485
+ in the table definition. In this case ``length`` must be ``None``
1486
+ or ``'max'``.
1487
+
1488
+ .. versionadded:: 1.4.31
1489
+
1490
+ """
1491
+
1492
+ self.filestream = filestream
1493
+ if self.filestream and length not in (None, "max"):
1494
+ raise ValueError(
1495
+ "length must be None or 'max' when setting filestream"
1496
+ )
1497
+ super().__init__(length=length)
1498
+
1499
+
1500
+ class IMAGE(sqltypes.LargeBinary):
1501
+ __visit_name__ = "IMAGE"
1502
+
1503
+
1504
+ class XML(sqltypes.Text):
1505
+ """MSSQL XML type.
1506
+
1507
+ This is a placeholder type for reflection purposes that does not include
1508
+ any Python-side datatype support. It also does not currently support
1509
+ additional arguments, such as "CONTENT", "DOCUMENT",
1510
+ "xml_schema_collection".
1511
+
1512
+ """
1513
+
1514
+ __visit_name__ = "XML"
1515
+
1516
+
1517
+ class BIT(sqltypes.Boolean):
1518
+ """MSSQL BIT type.
1519
+
1520
+ Both pyodbc and pymssql return values from BIT columns as
1521
+ Python <class 'bool'> so just subclass Boolean.
1522
+
1523
+ """
1524
+
1525
+ __visit_name__ = "BIT"
1526
+
1527
+
1528
+ class MONEY(sqltypes.TypeEngine):
1529
+ __visit_name__ = "MONEY"
1530
+
1531
+
1532
+ class SMALLMONEY(sqltypes.TypeEngine):
1533
+ __visit_name__ = "SMALLMONEY"
1534
+
1535
+
1536
+ class MSUUid(sqltypes.Uuid):
1537
+ def bind_processor(self, dialect):
1538
+ if self.native_uuid:
1539
+ # this is currently assuming pyodbc; might not work for
1540
+ # some other mssql driver
1541
+ return None
1542
+ else:
1543
+ if self.as_uuid:
1544
+
1545
+ def process(value):
1546
+ if value is not None:
1547
+ value = value.hex
1548
+ return value
1549
+
1550
+ return process
1551
+ else:
1552
+
1553
+ def process(value):
1554
+ if value is not None:
1555
+ value = value.replace("-", "").replace("''", "'")
1556
+ return value
1557
+
1558
+ return process
1559
+
1560
+ def literal_processor(self, dialect):
1561
+ if self.native_uuid:
1562
+
1563
+ def process(value):
1564
+ return f"""'{str(value).replace("'", "''")}'"""
1565
+
1566
+ return process
1567
+ else:
1568
+ if self.as_uuid:
1569
+
1570
+ def process(value):
1571
+ return f"""'{value.hex}'"""
1572
+
1573
+ return process
1574
+ else:
1575
+
1576
+ def process(value):
1577
+ return f"""'{
1578
+ value.replace("-", "").replace("'", "''")
1579
+ }'"""
1580
+
1581
+ return process
1582
+
1583
+
1584
+ class UNIQUEIDENTIFIER(sqltypes.Uuid[sqltypes._UUID_RETURN]):
1585
+ __visit_name__ = "UNIQUEIDENTIFIER"
1586
+
1587
+ @overload
1588
+ def __init__(
1589
+ self: UNIQUEIDENTIFIER[_python_UUID], as_uuid: Literal[True] = ...
1590
+ ): ...
1591
+
1592
+ @overload
1593
+ def __init__(
1594
+ self: UNIQUEIDENTIFIER[str], as_uuid: Literal[False] = ...
1595
+ ): ...
1596
+
1597
+ def __init__(self, as_uuid: bool = True):
1598
+ """Construct a :class:`_mssql.UNIQUEIDENTIFIER` type.
1599
+
1600
+
1601
+ :param as_uuid=True: if True, values will be interpreted
1602
+ as Python uuid objects, converting to/from string via the
1603
+ DBAPI.
1604
+
1605
+ .. versionchanged:: 2.0 Added direct "uuid" support to the
1606
+ :class:`_mssql.UNIQUEIDENTIFIER` datatype; uuid interpretation
1607
+ defaults to ``True``.
1608
+
1609
+ """
1610
+ self.as_uuid = as_uuid
1611
+ self.native_uuid = True
1612
+
1613
+
1614
+ class SQL_VARIANT(sqltypes.TypeEngine):
1615
+ __visit_name__ = "SQL_VARIANT"
1616
+
1617
+
1618
+ # old names.
1619
+ MSDateTime = _MSDateTime
1620
+ MSDate = _MSDate
1621
+ MSReal = REAL
1622
+ MSTinyInteger = TINYINT
1623
+ MSTime = TIME
1624
+ MSSmallDateTime = SMALLDATETIME
1625
+ MSDateTime2 = DATETIME2
1626
+ MSDateTimeOffset = DATETIMEOFFSET
1627
+ MSText = TEXT
1628
+ MSNText = NTEXT
1629
+ MSString = VARCHAR
1630
+ MSNVarchar = NVARCHAR
1631
+ MSChar = CHAR
1632
+ MSNChar = NCHAR
1633
+ MSBinary = BINARY
1634
+ MSVarBinary = VARBINARY
1635
+ MSImage = IMAGE
1636
+ MSBit = BIT
1637
+ MSMoney = MONEY
1638
+ MSSmallMoney = SMALLMONEY
1639
+ MSUniqueIdentifier = UNIQUEIDENTIFIER
1640
+ MSVariant = SQL_VARIANT
1641
+
1642
+ ischema_names = {
1643
+ "int": INTEGER,
1644
+ "bigint": BIGINT,
1645
+ "smallint": SMALLINT,
1646
+ "tinyint": TINYINT,
1647
+ "varchar": VARCHAR,
1648
+ "nvarchar": NVARCHAR,
1649
+ "char": CHAR,
1650
+ "nchar": NCHAR,
1651
+ "text": TEXT,
1652
+ "ntext": NTEXT,
1653
+ "decimal": DECIMAL,
1654
+ "numeric": NUMERIC,
1655
+ "float": FLOAT,
1656
+ "datetime": DATETIME,
1657
+ "datetime2": DATETIME2,
1658
+ "datetimeoffset": DATETIMEOFFSET,
1659
+ "date": DATE,
1660
+ "time": TIME,
1661
+ "smalldatetime": SMALLDATETIME,
1662
+ "binary": BINARY,
1663
+ "varbinary": VARBINARY,
1664
+ "bit": BIT,
1665
+ "real": REAL,
1666
+ "double precision": DOUBLE_PRECISION,
1667
+ "image": IMAGE,
1668
+ "xml": XML,
1669
+ "timestamp": TIMESTAMP,
1670
+ "money": MONEY,
1671
+ "smallmoney": SMALLMONEY,
1672
+ "uniqueidentifier": UNIQUEIDENTIFIER,
1673
+ "sql_variant": SQL_VARIANT,
1674
+ }
1675
+
1676
+
1677
+ class MSTypeCompiler(compiler.GenericTypeCompiler):
1678
+ def _extend(self, spec, type_, length=None):
1679
+ """Extend a string-type declaration with standard SQL
1680
+ COLLATE annotations.
1681
+
1682
+ """
1683
+
1684
+ if getattr(type_, "collation", None):
1685
+ collation = "COLLATE %s" % type_.collation
1686
+ else:
1687
+ collation = None
1688
+
1689
+ if not length:
1690
+ length = type_.length
1691
+
1692
+ if length:
1693
+ spec = spec + "(%s)" % length
1694
+
1695
+ return " ".join([c for c in (spec, collation) if c is not None])
1696
+
1697
+ def visit_double(self, type_, **kw):
1698
+ return self.visit_DOUBLE_PRECISION(type_, **kw)
1699
+
1700
+ def visit_FLOAT(self, type_, **kw):
1701
+ precision = getattr(type_, "precision", None)
1702
+ if precision is None:
1703
+ return "FLOAT"
1704
+ else:
1705
+ return "FLOAT(%(precision)s)" % {"precision": precision}
1706
+
1707
+ def visit_TINYINT(self, type_, **kw):
1708
+ return "TINYINT"
1709
+
1710
+ def visit_TIME(self, type_, **kw):
1711
+ precision = getattr(type_, "precision", None)
1712
+ if precision is not None:
1713
+ return "TIME(%s)" % precision
1714
+ else:
1715
+ return "TIME"
1716
+
1717
+ def visit_TIMESTAMP(self, type_, **kw):
1718
+ return "TIMESTAMP"
1719
+
1720
+ def visit_ROWVERSION(self, type_, **kw):
1721
+ return "ROWVERSION"
1722
+
1723
+ def visit_datetime(self, type_, **kw):
1724
+ if type_.timezone:
1725
+ return self.visit_DATETIMEOFFSET(type_, **kw)
1726
+ else:
1727
+ return self.visit_DATETIME(type_, **kw)
1728
+
1729
+ def visit_DATETIMEOFFSET(self, type_, **kw):
1730
+ precision = getattr(type_, "precision", None)
1731
+ if precision is not None:
1732
+ return "DATETIMEOFFSET(%s)" % type_.precision
1733
+ else:
1734
+ return "DATETIMEOFFSET"
1735
+
1736
+ def visit_DATETIME2(self, type_, **kw):
1737
+ precision = getattr(type_, "precision", None)
1738
+ if precision is not None:
1739
+ return "DATETIME2(%s)" % precision
1740
+ else:
1741
+ return "DATETIME2"
1742
+
1743
+ def visit_SMALLDATETIME(self, type_, **kw):
1744
+ return "SMALLDATETIME"
1745
+
1746
+ def visit_unicode(self, type_, **kw):
1747
+ return self.visit_NVARCHAR(type_, **kw)
1748
+
1749
+ def visit_text(self, type_, **kw):
1750
+ if self.dialect.deprecate_large_types:
1751
+ return self.visit_VARCHAR(type_, **kw)
1752
+ else:
1753
+ return self.visit_TEXT(type_, **kw)
1754
+
1755
+ def visit_unicode_text(self, type_, **kw):
1756
+ if self.dialect.deprecate_large_types:
1757
+ return self.visit_NVARCHAR(type_, **kw)
1758
+ else:
1759
+ return self.visit_NTEXT(type_, **kw)
1760
+
1761
+ def visit_NTEXT(self, type_, **kw):
1762
+ return self._extend("NTEXT", type_)
1763
+
1764
+ def visit_TEXT(self, type_, **kw):
1765
+ return self._extend("TEXT", type_)
1766
+
1767
+ def visit_VARCHAR(self, type_, **kw):
1768
+ return self._extend("VARCHAR", type_, length=type_.length or "max")
1769
+
1770
+ def visit_CHAR(self, type_, **kw):
1771
+ return self._extend("CHAR", type_)
1772
+
1773
+ def visit_NCHAR(self, type_, **kw):
1774
+ return self._extend("NCHAR", type_)
1775
+
1776
+ def visit_NVARCHAR(self, type_, **kw):
1777
+ return self._extend("NVARCHAR", type_, length=type_.length or "max")
1778
+
1779
+ def visit_date(self, type_, **kw):
1780
+ if self.dialect.server_version_info < MS_2008_VERSION:
1781
+ return self.visit_DATETIME(type_, **kw)
1782
+ else:
1783
+ return self.visit_DATE(type_, **kw)
1784
+
1785
+ def visit__BASETIMEIMPL(self, type_, **kw):
1786
+ return self.visit_time(type_, **kw)
1787
+
1788
+ def visit_time(self, type_, **kw):
1789
+ if self.dialect.server_version_info < MS_2008_VERSION:
1790
+ return self.visit_DATETIME(type_, **kw)
1791
+ else:
1792
+ return self.visit_TIME(type_, **kw)
1793
+
1794
+ def visit_large_binary(self, type_, **kw):
1795
+ if self.dialect.deprecate_large_types:
1796
+ return self.visit_VARBINARY(type_, **kw)
1797
+ else:
1798
+ return self.visit_IMAGE(type_, **kw)
1799
+
1800
+ def visit_IMAGE(self, type_, **kw):
1801
+ return "IMAGE"
1802
+
1803
+ def visit_XML(self, type_, **kw):
1804
+ return "XML"
1805
+
1806
+ def visit_VARBINARY(self, type_, **kw):
1807
+ text = self._extend("VARBINARY", type_, length=type_.length or "max")
1808
+ if getattr(type_, "filestream", False):
1809
+ text += " FILESTREAM"
1810
+ return text
1811
+
1812
+ def visit_boolean(self, type_, **kw):
1813
+ return self.visit_BIT(type_)
1814
+
1815
+ def visit_BIT(self, type_, **kw):
1816
+ return "BIT"
1817
+
1818
+ def visit_JSON(self, type_, **kw):
1819
+ # this is a bit of a break with SQLAlchemy's convention of
1820
+ # "UPPERCASE name goes to UPPERCASE type name with no modification"
1821
+ return self._extend("NVARCHAR", type_, length="max")
1822
+
1823
+ def visit_MONEY(self, type_, **kw):
1824
+ return "MONEY"
1825
+
1826
+ def visit_SMALLMONEY(self, type_, **kw):
1827
+ return "SMALLMONEY"
1828
+
1829
+ def visit_uuid(self, type_, **kw):
1830
+ if type_.native_uuid:
1831
+ return self.visit_UNIQUEIDENTIFIER(type_, **kw)
1832
+ else:
1833
+ return super().visit_uuid(type_, **kw)
1834
+
1835
+ def visit_UNIQUEIDENTIFIER(self, type_, **kw):
1836
+ return "UNIQUEIDENTIFIER"
1837
+
1838
+ def visit_SQL_VARIANT(self, type_, **kw):
1839
+ return "SQL_VARIANT"
1840
+
1841
+
1842
+ class MSExecutionContext(default.DefaultExecutionContext):
1843
+ _embedded_scope_identity = False
1844
+ _enable_identity_insert = False
1845
+ _select_lastrowid = False
1846
+ _lastrowid = None
1847
+
1848
+ dialect: MSDialect
1849
+
1850
+ def _opt_encode(self, statement):
1851
+ if self.compiled and self.compiled.schema_translate_map:
1852
+ rst = self.compiled.preparer._render_schema_translates
1853
+ statement = rst(statement, self.compiled.schema_translate_map)
1854
+
1855
+ return statement
1856
+
1857
+ def pre_exec(self):
1858
+ """Activate IDENTITY_INSERT if needed."""
1859
+
1860
+ if self.isinsert:
1861
+ if TYPE_CHECKING:
1862
+ assert is_sql_compiler(self.compiled)
1863
+ assert isinstance(self.compiled.compile_state, DMLState)
1864
+ assert isinstance(
1865
+ self.compiled.compile_state.dml_table, TableClause
1866
+ )
1867
+
1868
+ tbl = self.compiled.compile_state.dml_table
1869
+ id_column = tbl._autoincrement_column
1870
+
1871
+ if id_column is not None and (
1872
+ not isinstance(id_column.default, Sequence)
1873
+ ):
1874
+ insert_has_identity = True
1875
+ compile_state = self.compiled.dml_compile_state
1876
+ self._enable_identity_insert = (
1877
+ id_column.key in self.compiled_parameters[0]
1878
+ ) or (
1879
+ compile_state._dict_parameters
1880
+ and (id_column.key in compile_state._insert_col_keys)
1881
+ )
1882
+
1883
+ else:
1884
+ insert_has_identity = False
1885
+ self._enable_identity_insert = False
1886
+
1887
+ self._select_lastrowid = (
1888
+ not self.compiled.inline
1889
+ and insert_has_identity
1890
+ and not self.compiled.effective_returning
1891
+ and not self._enable_identity_insert
1892
+ and not self.executemany
1893
+ )
1894
+
1895
+ if self._enable_identity_insert:
1896
+ self.root_connection._cursor_execute(
1897
+ self.cursor,
1898
+ self._opt_encode(
1899
+ "SET IDENTITY_INSERT %s ON"
1900
+ % self.identifier_preparer.format_table(tbl)
1901
+ ),
1902
+ (),
1903
+ self,
1904
+ )
1905
+
1906
+ # don't embed the scope_identity select into an
1907
+ # "INSERT .. DEFAULT VALUES"
1908
+ if (
1909
+ self._select_lastrowid
1910
+ and self.dialect.scope_identity_must_be_embedded
1911
+ and self.dialect.use_scope_identity
1912
+ and len(self.parameters[0])
1913
+ ):
1914
+ self._embedded_scope_identity = True
1915
+
1916
+ self.statement += "; select scope_identity()"
1917
+
1918
+ def post_exec(self):
1919
+
1920
+ conn = self.root_connection
1921
+
1922
+ if self.isinsert or self.isupdate or self.isdelete:
1923
+ self._rowcount = self.cursor.rowcount
1924
+
1925
+ # handle INSERT with embedded SELECT SCOPE_IDENTITY() call
1926
+ if self._embedded_scope_identity:
1927
+ # Fetch the last inserted id from the manipulated statement
1928
+ # We may have to skip over a number of result sets with
1929
+ # no data (due to triggers, etc.) so run up to three times
1930
+
1931
+ row = None
1932
+ for _ in range(3):
1933
+ if self.cursor.description:
1934
+ rows = self.cursor.fetchall()
1935
+ if rows:
1936
+ row = rows[0]
1937
+ break
1938
+ else:
1939
+ self.cursor.nextset()
1940
+
1941
+ self._lastrowid = int(row[0]) if row else None
1942
+
1943
+ self.cursor_fetch_strategy = _cursor._NO_CURSOR_DML
1944
+
1945
+ elif self._select_lastrowid:
1946
+ if self.dialect.use_scope_identity:
1947
+ conn._cursor_execute(
1948
+ self.cursor,
1949
+ "SELECT scope_identity() AS lastrowid",
1950
+ (),
1951
+ self,
1952
+ )
1953
+ else:
1954
+ conn._cursor_execute(
1955
+ self.cursor, "SELECT @@identity AS lastrowid", (), self
1956
+ )
1957
+ # fetchall() ensures the cursor is consumed without closing it
1958
+ row = self.cursor.fetchall()[0]
1959
+ self._lastrowid = int(row[0])
1960
+
1961
+ self.cursor_fetch_strategy = _cursor._NO_CURSOR_DML
1962
+ elif (
1963
+ self.compiled is not None
1964
+ and is_sql_compiler(self.compiled)
1965
+ and self.compiled.effective_returning
1966
+ ):
1967
+ self.cursor_fetch_strategy = (
1968
+ _cursor.FullyBufferedCursorFetchStrategy(
1969
+ self.cursor,
1970
+ self.cursor.description,
1971
+ self.cursor.fetchall(),
1972
+ )
1973
+ )
1974
+
1975
+ if self._enable_identity_insert:
1976
+ # Disable IDENTITY_INSERT if enabled.
1977
+ if TYPE_CHECKING:
1978
+ assert is_sql_compiler(self.compiled)
1979
+ assert isinstance(self.compiled.compile_state, DMLState)
1980
+ assert isinstance(
1981
+ self.compiled.compile_state.dml_table, TableClause
1982
+ )
1983
+ conn._cursor_execute(
1984
+ self.cursor,
1985
+ self._opt_encode(
1986
+ "SET IDENTITY_INSERT %s OFF"
1987
+ % self.identifier_preparer.format_table(
1988
+ self.compiled.compile_state.dml_table
1989
+ )
1990
+ ),
1991
+ (),
1992
+ self,
1993
+ )
1994
+
1995
+ def get_lastrowid(self):
1996
+ return self._lastrowid
1997
+
1998
+ def handle_dbapi_exception(self, e):
1999
+ if self._enable_identity_insert:
2000
+ try:
2001
+ self.cursor.execute(
2002
+ self._opt_encode(
2003
+ "SET IDENTITY_INSERT %s OFF"
2004
+ % self.identifier_preparer.format_table(
2005
+ self.compiled.compile_state.dml_table
2006
+ )
2007
+ )
2008
+ )
2009
+ except Exception:
2010
+ pass
2011
+
2012
+ def fire_sequence(self, seq, type_):
2013
+ return self._execute_scalar(
2014
+ (
2015
+ "SELECT NEXT VALUE FOR %s"
2016
+ % self.identifier_preparer.format_sequence(seq)
2017
+ ),
2018
+ type_,
2019
+ )
2020
+
2021
+ def get_insert_default(self, column):
2022
+ if (
2023
+ isinstance(column, sa_schema.Column)
2024
+ and column is column.table._autoincrement_column
2025
+ and isinstance(column.default, sa_schema.Sequence)
2026
+ and column.default.optional
2027
+ ):
2028
+ return None
2029
+ return super().get_insert_default(column)
2030
+
2031
+
2032
+ class MSSQLCompiler(compiler.SQLCompiler):
2033
+ returning_precedes_values = True
2034
+
2035
+ extract_map = util.update_copy(
2036
+ compiler.SQLCompiler.extract_map,
2037
+ {
2038
+ "doy": "dayofyear",
2039
+ "dow": "weekday",
2040
+ "milliseconds": "millisecond",
2041
+ "microseconds": "microsecond",
2042
+ },
2043
+ )
2044
+
2045
+ def __init__(self, *args, **kwargs):
2046
+ self.tablealiases = {}
2047
+ super().__init__(*args, **kwargs)
2048
+
2049
+ def visit_frame_clause(self, frameclause, **kw):
2050
+ kw["literal_execute"] = True
2051
+ return super().visit_frame_clause(frameclause, **kw)
2052
+
2053
+ def _with_legacy_schema_aliasing(fn):
2054
+ def decorate(self, *arg, **kw):
2055
+ if self.dialect.legacy_schema_aliasing:
2056
+ return fn(self, *arg, **kw)
2057
+ else:
2058
+ super_ = getattr(super(MSSQLCompiler, self), fn.__name__)
2059
+ return super_(*arg, **kw)
2060
+
2061
+ return decorate
2062
+
2063
+ def visit_now_func(self, fn, **kw):
2064
+ return "CURRENT_TIMESTAMP"
2065
+
2066
+ def visit_current_date_func(self, fn, **kw):
2067
+ return "GETDATE()"
2068
+
2069
+ def visit_length_func(self, fn, **kw):
2070
+ return "LEN%s" % self.function_argspec(fn, **kw)
2071
+
2072
+ def visit_char_length_func(self, fn, **kw):
2073
+ return "LEN%s" % self.function_argspec(fn, **kw)
2074
+
2075
+ def visit_aggregate_strings_func(self, fn, **kw):
2076
+ cl = list(fn.clauses)
2077
+ expr, delimiter = cl[0:2]
2078
+
2079
+ literal_exec = dict(kw)
2080
+ literal_exec["literal_execute"] = True
2081
+
2082
+ return (
2083
+ f"string_agg({expr._compiler_dispatch(self, **kw)}, "
2084
+ f"{delimiter._compiler_dispatch(self, **literal_exec)})"
2085
+ )
2086
+
2087
+ def visit_pow_func(self, fn, **kw):
2088
+ return f"POWER{self.function_argspec(fn)}"
2089
+
2090
+ def visit_concat_op_expression_clauselist(
2091
+ self, clauselist, operator, **kw
2092
+ ):
2093
+ return " + ".join(self.process(elem, **kw) for elem in clauselist)
2094
+
2095
+ def visit_concat_op_binary(self, binary, operator, **kw):
2096
+ return "%s + %s" % (
2097
+ self.process(binary.left, **kw),
2098
+ self.process(binary.right, **kw),
2099
+ )
2100
+
2101
+ def visit_true(self, expr, **kw):
2102
+ return "1"
2103
+
2104
+ def visit_false(self, expr, **kw):
2105
+ return "0"
2106
+
2107
+ def visit_match_op_binary(self, binary, operator, **kw):
2108
+ return "CONTAINS (%s, %s)" % (
2109
+ self.process(binary.left, **kw),
2110
+ self.process(binary.right, **kw),
2111
+ )
2112
+
2113
+ def get_select_precolumns(self, select, **kw):
2114
+ """MS-SQL puts TOP, it's version of LIMIT here"""
2115
+
2116
+ s = super().get_select_precolumns(select, **kw)
2117
+
2118
+ if select._has_row_limiting_clause and self._use_top(select):
2119
+ # ODBC drivers and possibly others
2120
+ # don't support bind params in the SELECT clause on SQL Server.
2121
+ # so have to use literal here.
2122
+ kw["literal_execute"] = True
2123
+ s += "TOP %s " % self.process(
2124
+ self._get_limit_or_fetch(select), **kw
2125
+ )
2126
+ if select._fetch_clause is not None:
2127
+ if select._fetch_clause_options["percent"]:
2128
+ s += "PERCENT "
2129
+ if select._fetch_clause_options["with_ties"]:
2130
+ s += "WITH TIES "
2131
+
2132
+ return s
2133
+
2134
+ def get_from_hint_text(self, table, text):
2135
+ return text
2136
+
2137
+ def get_crud_hint_text(self, table, text):
2138
+ return text
2139
+
2140
+ def _get_limit_or_fetch(self, select):
2141
+ if select._fetch_clause is None:
2142
+ return select._limit_clause
2143
+ else:
2144
+ return select._fetch_clause
2145
+
2146
+ def _use_top(self, select):
2147
+ return (select._offset_clause is None) and (
2148
+ select._simple_int_clause(select._limit_clause)
2149
+ or (
2150
+ # limit can use TOP with is by itself. fetch only uses TOP
2151
+ # when it needs to because of PERCENT and/or WITH TIES
2152
+ # TODO: Why? shouldn't we use TOP always ?
2153
+ select._simple_int_clause(select._fetch_clause)
2154
+ and (
2155
+ select._fetch_clause_options["percent"]
2156
+ or select._fetch_clause_options["with_ties"]
2157
+ )
2158
+ )
2159
+ )
2160
+
2161
+ def limit_clause(self, cs, **kwargs):
2162
+ return ""
2163
+
2164
+ def _check_can_use_fetch_limit(self, select):
2165
+ # to use ROW_NUMBER(), an ORDER BY is required.
2166
+ # OFFSET are FETCH are options of the ORDER BY clause
2167
+ if not select._order_by_clause.clauses:
2168
+ raise exc.CompileError(
2169
+ "MSSQL requires an order_by when "
2170
+ "using an OFFSET or a non-simple "
2171
+ "LIMIT clause"
2172
+ )
2173
+
2174
+ if select._fetch_clause_options is not None and (
2175
+ select._fetch_clause_options["percent"]
2176
+ or select._fetch_clause_options["with_ties"]
2177
+ ):
2178
+ raise exc.CompileError(
2179
+ "MSSQL needs TOP to use PERCENT and/or WITH TIES. "
2180
+ "Only simple fetch without offset can be used."
2181
+ )
2182
+
2183
+ def _row_limit_clause(self, select, **kw):
2184
+ """MSSQL 2012 supports OFFSET/FETCH operators
2185
+ Use it instead subquery with row_number
2186
+
2187
+ """
2188
+
2189
+ if self.dialect._supports_offset_fetch and not self._use_top(select):
2190
+ self._check_can_use_fetch_limit(select)
2191
+
2192
+ return self.fetch_clause(
2193
+ select,
2194
+ fetch_clause=self._get_limit_or_fetch(select),
2195
+ require_offset=True,
2196
+ **kw,
2197
+ )
2198
+
2199
+ else:
2200
+ return ""
2201
+
2202
+ def visit_try_cast(self, element, **kw):
2203
+ return "TRY_CAST (%s AS %s)" % (
2204
+ self.process(element.clause, **kw),
2205
+ self.process(element.typeclause, **kw),
2206
+ )
2207
+
2208
+ def translate_select_structure(self, select_stmt, **kwargs):
2209
+ """Look for ``LIMIT`` and OFFSET in a select statement, and if
2210
+ so tries to wrap it in a subquery with ``row_number()`` criterion.
2211
+ MSSQL 2012 and above are excluded
2212
+
2213
+ """
2214
+ select = select_stmt
2215
+
2216
+ if (
2217
+ select._has_row_limiting_clause
2218
+ and not self.dialect._supports_offset_fetch
2219
+ and not self._use_top(select)
2220
+ and not getattr(select, "_mssql_visit", None)
2221
+ ):
2222
+ self._check_can_use_fetch_limit(select)
2223
+
2224
+ _order_by_clauses = [
2225
+ sql_util.unwrap_label_reference(elem)
2226
+ for elem in select._order_by_clause.clauses
2227
+ ]
2228
+
2229
+ limit_clause = self._get_limit_or_fetch(select)
2230
+ offset_clause = select._offset_clause
2231
+
2232
+ select = select._generate()
2233
+ select._mssql_visit = True
2234
+ select = (
2235
+ select.add_columns(
2236
+ sql.func.ROW_NUMBER()
2237
+ .over(order_by=_order_by_clauses)
2238
+ .label("mssql_rn")
2239
+ )
2240
+ .order_by(None)
2241
+ .alias()
2242
+ )
2243
+
2244
+ mssql_rn = sql.column("mssql_rn")
2245
+ limitselect = sql.select(
2246
+ *[c for c in select.c if c.key != "mssql_rn"]
2247
+ )
2248
+ if offset_clause is not None:
2249
+ limitselect = limitselect.where(mssql_rn > offset_clause)
2250
+ if limit_clause is not None:
2251
+ limitselect = limitselect.where(
2252
+ mssql_rn <= (limit_clause + offset_clause)
2253
+ )
2254
+ else:
2255
+ limitselect = limitselect.where(mssql_rn <= (limit_clause))
2256
+ return limitselect
2257
+ else:
2258
+ return select
2259
+
2260
+ @_with_legacy_schema_aliasing
2261
+ def visit_table(self, table, mssql_aliased=False, iscrud=False, **kwargs):
2262
+ if mssql_aliased is table or iscrud:
2263
+ return super().visit_table(table, **kwargs)
2264
+
2265
+ # alias schema-qualified tables
2266
+ alias = self._schema_aliased_table(table)
2267
+ if alias is not None:
2268
+ return self.process(alias, mssql_aliased=table, **kwargs)
2269
+ else:
2270
+ return super().visit_table(table, **kwargs)
2271
+
2272
+ @_with_legacy_schema_aliasing
2273
+ def visit_alias(self, alias, **kw):
2274
+ # translate for schema-qualified table aliases
2275
+ kw["mssql_aliased"] = alias.element
2276
+ return super().visit_alias(alias, **kw)
2277
+
2278
+ @_with_legacy_schema_aliasing
2279
+ def visit_column(self, column, add_to_result_map=None, **kw):
2280
+ if (
2281
+ column.table is not None
2282
+ and (not self.isupdate and not self.isdelete)
2283
+ or self.is_subquery()
2284
+ ):
2285
+ # translate for schema-qualified table aliases
2286
+ t = self._schema_aliased_table(column.table)
2287
+ if t is not None:
2288
+ converted = elements._corresponding_column_or_error(t, column)
2289
+ if add_to_result_map is not None:
2290
+ add_to_result_map(
2291
+ column.name,
2292
+ column.name,
2293
+ (column, column.name, column.key),
2294
+ column.type,
2295
+ )
2296
+
2297
+ return super().visit_column(converted, **kw)
2298
+
2299
+ return super().visit_column(
2300
+ column, add_to_result_map=add_to_result_map, **kw
2301
+ )
2302
+
2303
+ def _schema_aliased_table(self, table):
2304
+ if getattr(table, "schema", None) is not None:
2305
+ if table not in self.tablealiases:
2306
+ self.tablealiases[table] = table.alias()
2307
+ return self.tablealiases[table]
2308
+ else:
2309
+ return None
2310
+
2311
+ def visit_extract(self, extract, **kw):
2312
+ field = self.extract_map.get(extract.field, extract.field)
2313
+ return "DATEPART(%s, %s)" % (field, self.process(extract.expr, **kw))
2314
+
2315
+ def visit_savepoint(self, savepoint_stmt, **kw):
2316
+ return "SAVE TRANSACTION %s" % self.preparer.format_savepoint(
2317
+ savepoint_stmt
2318
+ )
2319
+
2320
+ def visit_rollback_to_savepoint(self, savepoint_stmt, **kw):
2321
+ return "ROLLBACK TRANSACTION %s" % self.preparer.format_savepoint(
2322
+ savepoint_stmt
2323
+ )
2324
+
2325
+ def visit_binary(self, binary, **kwargs):
2326
+ """Move bind parameters to the right-hand side of an operator, where
2327
+ possible.
2328
+
2329
+ """
2330
+ if (
2331
+ isinstance(binary.left, expression.BindParameter)
2332
+ and binary.operator == operator.eq
2333
+ and not isinstance(binary.right, expression.BindParameter)
2334
+ ):
2335
+ return self.process(
2336
+ expression.BinaryExpression(
2337
+ binary.right, binary.left, binary.operator
2338
+ ),
2339
+ **kwargs,
2340
+ )
2341
+ return super().visit_binary(binary, **kwargs)
2342
+
2343
+ def returning_clause(
2344
+ self, stmt, returning_cols, *, populate_result_map, **kw
2345
+ ):
2346
+ # SQL server returning clause requires that the columns refer to
2347
+ # the virtual table names "inserted" or "deleted". Here, we make
2348
+ # a simple alias of our table with that name, and then adapt the
2349
+ # columns we have from the list of RETURNING columns to that new name
2350
+ # so that they render as "inserted.<colname>" / "deleted.<colname>".
2351
+
2352
+ if stmt.is_insert or stmt.is_update:
2353
+ target = stmt.table.alias("inserted")
2354
+ elif stmt.is_delete:
2355
+ target = stmt.table.alias("deleted")
2356
+ else:
2357
+ assert False, "expected Insert, Update or Delete statement"
2358
+
2359
+ adapter = sql_util.ClauseAdapter(target)
2360
+
2361
+ # adapter.traverse() takes a column from our target table and returns
2362
+ # the one that is linked to the "inserted" / "deleted" tables. So in
2363
+ # order to retrieve these values back from the result (e.g. like
2364
+ # row[column]), tell the compiler to also add the original unadapted
2365
+ # column to the result map. Before #4877, these were (unknowingly)
2366
+ # falling back using string name matching in the result set which
2367
+ # necessarily used an expensive KeyError in order to match.
2368
+
2369
+ columns = [
2370
+ self._label_returning_column(
2371
+ stmt,
2372
+ adapter.traverse(column),
2373
+ populate_result_map,
2374
+ {"result_map_targets": (column,)},
2375
+ fallback_label_name=fallback_label_name,
2376
+ column_is_repeated=repeated,
2377
+ name=name,
2378
+ proxy_name=proxy_name,
2379
+ **kw,
2380
+ )
2381
+ for (
2382
+ name,
2383
+ proxy_name,
2384
+ fallback_label_name,
2385
+ column,
2386
+ repeated,
2387
+ ) in stmt._generate_columns_plus_names(
2388
+ True, cols=expression._select_iterables(returning_cols)
2389
+ )
2390
+ ]
2391
+
2392
+ return "OUTPUT " + ", ".join(columns)
2393
+
2394
+ def get_cte_preamble(self, recursive):
2395
+ # SQL Server finds it too inconvenient to accept
2396
+ # an entirely optional, SQL standard specified,
2397
+ # "RECURSIVE" word with their "WITH",
2398
+ # so here we go
2399
+ return "WITH"
2400
+
2401
+ def label_select_column(self, select, column, asfrom):
2402
+ if isinstance(column, expression.Function):
2403
+ return column.label(None)
2404
+ else:
2405
+ return super().label_select_column(select, column, asfrom)
2406
+
2407
+ def for_update_clause(self, select, **kw):
2408
+ # "FOR UPDATE" is only allowed on "DECLARE CURSOR" which
2409
+ # SQLAlchemy doesn't use
2410
+ return ""
2411
+
2412
+ def order_by_clause(self, select, **kw):
2413
+ # MSSQL only allows ORDER BY in subqueries if there is a LIMIT:
2414
+ # "The ORDER BY clause is invalid in views, inline functions,
2415
+ # derived tables, subqueries, and common table expressions,
2416
+ # unless TOP, OFFSET or FOR XML is also specified."
2417
+ if (
2418
+ self.is_subquery()
2419
+ and not self._use_top(select)
2420
+ and (
2421
+ select._offset is None
2422
+ or not self.dialect._supports_offset_fetch
2423
+ )
2424
+ ):
2425
+ # avoid processing the order by clause if we won't end up
2426
+ # using it, because we don't want all the bind params tacked
2427
+ # onto the positional list if that is what the dbapi requires
2428
+ return ""
2429
+
2430
+ order_by = self.process(select._order_by_clause, **kw)
2431
+
2432
+ if order_by:
2433
+ return " ORDER BY " + order_by
2434
+ else:
2435
+ return ""
2436
+
2437
+ def update_from_clause(
2438
+ self, update_stmt, from_table, extra_froms, from_hints, **kw
2439
+ ):
2440
+ """Render the UPDATE..FROM clause specific to MSSQL.
2441
+
2442
+ In MSSQL, if the UPDATE statement involves an alias of the table to
2443
+ be updated, then the table itself must be added to the FROM list as
2444
+ well. Otherwise, it is optional. Here, we add it regardless.
2445
+
2446
+ """
2447
+ return "FROM " + ", ".join(
2448
+ t._compiler_dispatch(self, asfrom=True, fromhints=from_hints, **kw)
2449
+ for t in [from_table] + extra_froms
2450
+ )
2451
+
2452
+ def delete_table_clause(self, delete_stmt, from_table, extra_froms, **kw):
2453
+ """If we have extra froms make sure we render any alias as hint."""
2454
+ ashint = False
2455
+ if extra_froms:
2456
+ ashint = True
2457
+ return from_table._compiler_dispatch(
2458
+ self, asfrom=True, iscrud=True, ashint=ashint, **kw
2459
+ )
2460
+
2461
+ def delete_extra_from_clause(
2462
+ self, delete_stmt, from_table, extra_froms, from_hints, **kw
2463
+ ):
2464
+ """Render the DELETE .. FROM clause specific to MSSQL.
2465
+
2466
+ Yes, it has the FROM keyword twice.
2467
+
2468
+ """
2469
+ return "FROM " + ", ".join(
2470
+ t._compiler_dispatch(self, asfrom=True, fromhints=from_hints, **kw)
2471
+ for t in [from_table] + extra_froms
2472
+ )
2473
+
2474
+ def visit_empty_set_expr(self, type_, **kw):
2475
+ return "SELECT 1 WHERE 1!=1"
2476
+
2477
+ def visit_is_distinct_from_binary(self, binary, operator, **kw):
2478
+ return "NOT EXISTS (SELECT %s INTERSECT SELECT %s)" % (
2479
+ self.process(binary.left),
2480
+ self.process(binary.right),
2481
+ )
2482
+
2483
+ def visit_is_not_distinct_from_binary(self, binary, operator, **kw):
2484
+ return "EXISTS (SELECT %s INTERSECT SELECT %s)" % (
2485
+ self.process(binary.left),
2486
+ self.process(binary.right),
2487
+ )
2488
+
2489
+ def _render_json_extract_from_binary(self, binary, operator, **kw):
2490
+ # note we are intentionally calling upon the process() calls in the
2491
+ # order in which they appear in the SQL String as this is used
2492
+ # by positional parameter rendering
2493
+
2494
+ if binary.type._type_affinity is sqltypes.JSON:
2495
+ return "JSON_QUERY(%s, %s)" % (
2496
+ self.process(binary.left, **kw),
2497
+ self.process(binary.right, **kw),
2498
+ )
2499
+
2500
+ # as with other dialects, start with an explicit test for NULL
2501
+ case_expression = "CASE JSON_VALUE(%s, %s) WHEN NULL THEN NULL" % (
2502
+ self.process(binary.left, **kw),
2503
+ self.process(binary.right, **kw),
2504
+ )
2505
+
2506
+ if binary.type._type_affinity is sqltypes.Integer:
2507
+ type_expression = "ELSE CAST(JSON_VALUE(%s, %s) AS INTEGER)" % (
2508
+ self.process(binary.left, **kw),
2509
+ self.process(binary.right, **kw),
2510
+ )
2511
+ elif binary.type._type_affinity in (sqltypes.Numeric, sqltypes.Float):
2512
+ type_expression = "ELSE CAST(JSON_VALUE(%s, %s) AS %s)" % (
2513
+ self.process(binary.left, **kw),
2514
+ self.process(binary.right, **kw),
2515
+ (
2516
+ "FLOAT"
2517
+ if isinstance(binary.type, sqltypes.Float)
2518
+ else "NUMERIC(%s, %s)"
2519
+ % (binary.type.precision, binary.type.scale)
2520
+ ),
2521
+ )
2522
+ elif binary.type._type_affinity is sqltypes.Boolean:
2523
+ # the NULL handling is particularly weird with boolean, so
2524
+ # explicitly return numeric (BIT) constants
2525
+ type_expression = (
2526
+ "WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE "
2527
+ "CAST(JSON_VALUE(%s, %s) AS BIT)"
2528
+ % (
2529
+ self.process(binary.left, **kw),
2530
+ self.process(binary.right, **kw),
2531
+ )
2532
+ )
2533
+ elif binary.type._type_affinity is sqltypes.String:
2534
+ # TODO: does this comment (from mysql) apply to here, too?
2535
+ # this fails with a JSON value that's a four byte unicode
2536
+ # string. SQLite has the same problem at the moment
2537
+ type_expression = "ELSE JSON_VALUE(%s, %s)" % (
2538
+ self.process(binary.left, **kw),
2539
+ self.process(binary.right, **kw),
2540
+ )
2541
+ else:
2542
+ # other affinity....this is not expected right now
2543
+ type_expression = "ELSE JSON_QUERY(%s, %s)" % (
2544
+ self.process(binary.left, **kw),
2545
+ self.process(binary.right, **kw),
2546
+ )
2547
+
2548
+ return case_expression + " " + type_expression + " END"
2549
+
2550
+ def visit_json_getitem_op_binary(self, binary, operator, **kw):
2551
+ return self._render_json_extract_from_binary(binary, operator, **kw)
2552
+
2553
+ def visit_json_path_getitem_op_binary(self, binary, operator, **kw):
2554
+ return self._render_json_extract_from_binary(binary, operator, **kw)
2555
+
2556
+ def visit_sequence(self, seq, **kw):
2557
+ return "NEXT VALUE FOR %s" % self.preparer.format_sequence(seq)
2558
+
2559
+
2560
+ class MSSQLStrictCompiler(MSSQLCompiler):
2561
+ """A subclass of MSSQLCompiler which disables the usage of bind
2562
+ parameters where not allowed natively by MS-SQL.
2563
+
2564
+ A dialect may use this compiler on a platform where native
2565
+ binds are used.
2566
+
2567
+ """
2568
+
2569
+ ansi_bind_rules = True
2570
+
2571
+ def visit_in_op_binary(self, binary, operator, **kw):
2572
+ kw["literal_execute"] = True
2573
+ return "%s IN %s" % (
2574
+ self.process(binary.left, **kw),
2575
+ self.process(binary.right, **kw),
2576
+ )
2577
+
2578
+ def visit_not_in_op_binary(self, binary, operator, **kw):
2579
+ kw["literal_execute"] = True
2580
+ return "%s NOT IN %s" % (
2581
+ self.process(binary.left, **kw),
2582
+ self.process(binary.right, **kw),
2583
+ )
2584
+
2585
+ def render_literal_value(self, value, type_):
2586
+ """
2587
+ For date and datetime values, convert to a string
2588
+ format acceptable to MSSQL. That seems to be the
2589
+ so-called ODBC canonical date format which looks
2590
+ like this:
2591
+
2592
+ yyyy-mm-dd hh:mi:ss.mmm(24h)
2593
+
2594
+ For other data types, call the base class implementation.
2595
+ """
2596
+ # datetime and date are both subclasses of datetime.date
2597
+ if issubclass(type(value), datetime.date):
2598
+ # SQL Server wants single quotes around the date string.
2599
+ return "'" + str(value) + "'"
2600
+ else:
2601
+ return super().render_literal_value(value, type_)
2602
+
2603
+
2604
+ class MSDDLCompiler(compiler.DDLCompiler):
2605
+ def get_column_specification(self, column, **kwargs):
2606
+ colspec = self.preparer.format_column(column)
2607
+
2608
+ # type is not accepted in a computed column
2609
+ if column.computed is not None:
2610
+ colspec += " " + self.process(column.computed)
2611
+ else:
2612
+ colspec += " " + self.dialect.type_compiler_instance.process(
2613
+ column.type, type_expression=column
2614
+ )
2615
+
2616
+ if column.nullable is not None:
2617
+ if (
2618
+ not column.nullable
2619
+ or column.primary_key
2620
+ or isinstance(column.default, sa_schema.Sequence)
2621
+ or column.autoincrement is True
2622
+ or column.identity
2623
+ ):
2624
+ colspec += " NOT NULL"
2625
+ elif column.computed is None:
2626
+ # don't specify "NULL" for computed columns
2627
+ colspec += " NULL"
2628
+
2629
+ if column.table is None:
2630
+ raise exc.CompileError(
2631
+ "mssql requires Table-bound columns "
2632
+ "in order to generate DDL"
2633
+ )
2634
+
2635
+ d_opt = column.dialect_options["mssql"]
2636
+ start = d_opt["identity_start"]
2637
+ increment = d_opt["identity_increment"]
2638
+ if start is not None or increment is not None:
2639
+ if column.identity:
2640
+ raise exc.CompileError(
2641
+ "Cannot specify options 'mssql_identity_start' and/or "
2642
+ "'mssql_identity_increment' while also using the "
2643
+ "'Identity' construct."
2644
+ )
2645
+ util.warn_deprecated(
2646
+ "The dialect options 'mssql_identity_start' and "
2647
+ "'mssql_identity_increment' are deprecated. "
2648
+ "Use the 'Identity' object instead.",
2649
+ "1.4",
2650
+ )
2651
+
2652
+ if column.identity:
2653
+ colspec += self.process(column.identity, **kwargs)
2654
+ elif (
2655
+ column is column.table._autoincrement_column
2656
+ or column.autoincrement is True
2657
+ ) and (
2658
+ not isinstance(column.default, Sequence) or column.default.optional
2659
+ ):
2660
+ colspec += self.process(Identity(start=start, increment=increment))
2661
+ else:
2662
+ default = self.get_column_default_string(column)
2663
+ if default is not None:
2664
+ colspec += " DEFAULT " + default
2665
+
2666
+ return colspec
2667
+
2668
+ def visit_create_index(self, create, include_schema=False, **kw):
2669
+ index = create.element
2670
+ self._verify_index_table(index)
2671
+ preparer = self.preparer
2672
+ text = "CREATE "
2673
+ if index.unique:
2674
+ text += "UNIQUE "
2675
+
2676
+ # handle clustering option
2677
+ clustered = index.dialect_options["mssql"]["clustered"]
2678
+ if clustered is not None:
2679
+ if clustered:
2680
+ text += "CLUSTERED "
2681
+ else:
2682
+ text += "NONCLUSTERED "
2683
+
2684
+ # handle columnstore option (has no negative value)
2685
+ columnstore = index.dialect_options["mssql"]["columnstore"]
2686
+ if columnstore:
2687
+ text += "COLUMNSTORE "
2688
+
2689
+ text += "INDEX %s ON %s" % (
2690
+ self._prepared_index_name(index, include_schema=include_schema),
2691
+ preparer.format_table(index.table),
2692
+ )
2693
+
2694
+ # in some case mssql allows indexes with no columns defined
2695
+ if len(index.expressions) > 0:
2696
+ text += " (%s)" % ", ".join(
2697
+ self.sql_compiler.process(
2698
+ expr, include_table=False, literal_binds=True
2699
+ )
2700
+ for expr in index.expressions
2701
+ )
2702
+
2703
+ # handle other included columns
2704
+ if index.dialect_options["mssql"]["include"]:
2705
+ inclusions = [
2706
+ index.table.c[col] if isinstance(col, str) else col
2707
+ for col in index.dialect_options["mssql"]["include"]
2708
+ ]
2709
+
2710
+ text += " INCLUDE (%s)" % ", ".join(
2711
+ [preparer.quote(c.name) for c in inclusions]
2712
+ )
2713
+
2714
+ whereclause = index.dialect_options["mssql"]["where"]
2715
+
2716
+ if whereclause is not None:
2717
+ whereclause = coercions.expect(
2718
+ roles.DDLExpressionRole, whereclause
2719
+ )
2720
+
2721
+ where_compiled = self.sql_compiler.process(
2722
+ whereclause, include_table=False, literal_binds=True
2723
+ )
2724
+ text += " WHERE " + where_compiled
2725
+
2726
+ return text
2727
+
2728
+ def visit_drop_index(self, drop: DropIndex, **kw: Any) -> str:
2729
+ index_name = self._prepared_index_name(
2730
+ drop.element, include_schema=False
2731
+ )
2732
+ table_name = self.preparer.format_table(drop.element.table)
2733
+ if_exists = " IF EXISTS" if drop.if_exists else ""
2734
+ return f"\nDROP INDEX{if_exists} {index_name} ON {table_name}"
2735
+
2736
+ def visit_create_table_as(self, element, **kw):
2737
+ prep = self.preparer
2738
+
2739
+ # SQL Server doesn't support CREATE TABLE AS, use SELECT INTO instead
2740
+ # Format: SELECT columns INTO new_table FROM source WHERE ...
2741
+
2742
+ qualified = prep.format_table(element.table)
2743
+
2744
+ # Get the inner SELECT SQL
2745
+ inner_kw = dict(kw)
2746
+ inner_kw["literal_binds"] = True
2747
+ select_sql = self.sql_compiler.process(element.selectable, **inner_kw)
2748
+
2749
+ # Inject INTO clause before FROM keyword
2750
+ # Find FROM position (case-insensitive)
2751
+ select_upper = select_sql.upper()
2752
+ from_idx = select_upper.find(" FROM ")
2753
+ if from_idx == -1:
2754
+ from_idx = select_upper.find("\nFROM ")
2755
+
2756
+ if from_idx == -1:
2757
+ raise exc.CompileError(
2758
+ "Could not find FROM keyword in selectable for CREATE TABLE AS"
2759
+ )
2760
+
2761
+ # Insert INTO clause before FROM
2762
+ result = (
2763
+ select_sql[:from_idx]
2764
+ + f"INTO {qualified} "
2765
+ + select_sql[from_idx:]
2766
+ )
2767
+
2768
+ return result
2769
+
2770
+ def visit_create_view(self, create, **kw):
2771
+ # SQL Server uses CREATE OR ALTER instead of CREATE OR REPLACE
2772
+ result = super().visit_create_view(create, **kw)
2773
+ if create.or_replace:
2774
+ result = result.replace("CREATE OR REPLACE", "CREATE OR ALTER")
2775
+ return result
2776
+
2777
+ def visit_primary_key_constraint(self, constraint, **kw):
2778
+ if len(constraint) == 0:
2779
+ return ""
2780
+ text = ""
2781
+ if constraint.name is not None:
2782
+ text += "CONSTRAINT %s " % self.preparer.format_constraint(
2783
+ constraint
2784
+ )
2785
+ text += "PRIMARY KEY "
2786
+
2787
+ clustered = constraint.dialect_options["mssql"]["clustered"]
2788
+ if clustered is not None:
2789
+ if clustered:
2790
+ text += "CLUSTERED "
2791
+ else:
2792
+ text += "NONCLUSTERED "
2793
+
2794
+ text += "(%s)" % ", ".join(
2795
+ self.preparer.quote(c.name) for c in constraint
2796
+ )
2797
+ text += self.define_constraint_deferrability(constraint)
2798
+ return text
2799
+
2800
+ def visit_unique_constraint(self, constraint, **kw):
2801
+ if len(constraint) == 0:
2802
+ return ""
2803
+ text = ""
2804
+ if constraint.name is not None:
2805
+ formatted_name = self.preparer.format_constraint(constraint)
2806
+ if formatted_name is not None:
2807
+ text += "CONSTRAINT %s " % formatted_name
2808
+ text += "UNIQUE %s" % self.define_unique_constraint_distinct(
2809
+ constraint, **kw
2810
+ )
2811
+ clustered = constraint.dialect_options["mssql"]["clustered"]
2812
+ if clustered is not None:
2813
+ if clustered:
2814
+ text += "CLUSTERED "
2815
+ else:
2816
+ text += "NONCLUSTERED "
2817
+
2818
+ text += "(%s)" % ", ".join(
2819
+ self.preparer.quote(c.name) for c in constraint
2820
+ )
2821
+ text += self.define_constraint_deferrability(constraint)
2822
+ return text
2823
+
2824
+ def visit_computed_column(self, generated, **kw):
2825
+ text = "AS (%s)" % self.sql_compiler.process(
2826
+ generated.sqltext, include_table=False, literal_binds=True
2827
+ )
2828
+ # explicitly check for True|False since None means server default
2829
+ if generated.persisted is True:
2830
+ text += " PERSISTED"
2831
+ return text
2832
+
2833
+ def visit_set_table_comment(self, create, **kw):
2834
+ schema = self.preparer.schema_for_object(create.element)
2835
+ schema_name = schema if schema else self.dialect.default_schema_name
2836
+ return (
2837
+ "execute sp_addextendedproperty 'MS_Description', "
2838
+ "{}, 'schema', {}, 'table', {}".format(
2839
+ self.sql_compiler.render_literal_value(
2840
+ create.element.comment, sqltypes.NVARCHAR()
2841
+ ),
2842
+ self.preparer.quote_schema(schema_name),
2843
+ self.preparer.format_table(create.element, use_schema=False),
2844
+ )
2845
+ )
2846
+
2847
+ def visit_drop_table_comment(self, drop, **kw):
2848
+ schema = self.preparer.schema_for_object(drop.element)
2849
+ schema_name = schema if schema else self.dialect.default_schema_name
2850
+ return (
2851
+ "execute sp_dropextendedproperty 'MS_Description', 'schema', "
2852
+ "{}, 'table', {}".format(
2853
+ self.preparer.quote_schema(schema_name),
2854
+ self.preparer.format_table(drop.element, use_schema=False),
2855
+ )
2856
+ )
2857
+
2858
+ def visit_set_column_comment(self, create, **kw):
2859
+ schema = self.preparer.schema_for_object(create.element.table)
2860
+ schema_name = schema if schema else self.dialect.default_schema_name
2861
+ return (
2862
+ "execute sp_addextendedproperty 'MS_Description', "
2863
+ "{}, 'schema', {}, 'table', {}, 'column', {}".format(
2864
+ self.sql_compiler.render_literal_value(
2865
+ create.element.comment, sqltypes.NVARCHAR()
2866
+ ),
2867
+ self.preparer.quote_schema(schema_name),
2868
+ self.preparer.format_table(
2869
+ create.element.table, use_schema=False
2870
+ ),
2871
+ self.preparer.format_column(create.element),
2872
+ )
2873
+ )
2874
+
2875
+ def visit_drop_column_comment(self, drop, **kw):
2876
+ schema = self.preparer.schema_for_object(drop.element.table)
2877
+ schema_name = schema if schema else self.dialect.default_schema_name
2878
+ return (
2879
+ "execute sp_dropextendedproperty 'MS_Description', 'schema', "
2880
+ "{}, 'table', {}, 'column', {}".format(
2881
+ self.preparer.quote_schema(schema_name),
2882
+ self.preparer.format_table(
2883
+ drop.element.table, use_schema=False
2884
+ ),
2885
+ self.preparer.format_column(drop.element),
2886
+ )
2887
+ )
2888
+
2889
+ def visit_create_sequence(self, create, **kw):
2890
+ prefix = None
2891
+ if create.element.data_type is not None:
2892
+ data_type = create.element.data_type
2893
+ prefix = " AS %s" % self.type_compiler.process(data_type)
2894
+ return super().visit_create_sequence(create, prefix=prefix, **kw)
2895
+
2896
+ def visit_identity_column(self, identity, **kw):
2897
+ text = " IDENTITY"
2898
+ if identity.start is not None or identity.increment is not None:
2899
+ start = 1 if identity.start is None else identity.start
2900
+ increment = 1 if identity.increment is None else identity.increment
2901
+ text += "(%s,%s)" % (start, increment)
2902
+ return text
2903
+
2904
+
2905
+ class MSIdentifierPreparer(compiler.IdentifierPreparer):
2906
+ reserved_words = RESERVED_WORDS
2907
+
2908
+ def __init__(self, dialect):
2909
+ super().__init__(
2910
+ dialect,
2911
+ initial_quote="[",
2912
+ final_quote="]",
2913
+ quote_case_sensitive_collations=False,
2914
+ )
2915
+
2916
+ def _escape_identifier(self, value):
2917
+ return value.replace("]", "]]")
2918
+
2919
+ def _unescape_identifier(self, value):
2920
+ return value.replace("]]", "]")
2921
+
2922
+ def quote_schema(self, schema):
2923
+ """Prepare a quoted table and schema name."""
2924
+
2925
+ dbname, owner = _schema_elements(schema)
2926
+ if dbname:
2927
+ result = "%s.%s" % (self.quote(dbname), self.quote(owner))
2928
+ elif owner:
2929
+ result = self.quote(owner)
2930
+ else:
2931
+ result = ""
2932
+ return result
2933
+
2934
+
2935
+ def _db_plus_owner_listing(fn):
2936
+ def wrap(dialect, connection, schema=None, **kw):
2937
+ dbname, owner = _owner_plus_db(dialect, schema)
2938
+ return _switch_db(
2939
+ dbname,
2940
+ connection,
2941
+ fn,
2942
+ dialect,
2943
+ connection,
2944
+ dbname,
2945
+ owner,
2946
+ schema,
2947
+ **kw,
2948
+ )
2949
+
2950
+ return update_wrapper(wrap, fn)
2951
+
2952
+
2953
+ def _db_plus_owner(fn):
2954
+ def wrap(dialect, connection, tablename, schema=None, **kw):
2955
+ dbname, owner = _owner_plus_db(dialect, schema)
2956
+ return _switch_db(
2957
+ dbname,
2958
+ connection,
2959
+ fn,
2960
+ dialect,
2961
+ connection,
2962
+ tablename,
2963
+ dbname,
2964
+ owner,
2965
+ schema,
2966
+ **kw,
2967
+ )
2968
+
2969
+ return update_wrapper(wrap, fn)
2970
+
2971
+
2972
+ def _db_plus_owner_multi(fn):
2973
+ def wrap(dialect, connection, schema=None, **kw):
2974
+ dbname, owner = _owner_plus_db(dialect, schema)
2975
+ return _switch_db(
2976
+ dbname,
2977
+ connection,
2978
+ fn,
2979
+ dialect,
2980
+ connection,
2981
+ dbname,
2982
+ owner,
2983
+ schema,
2984
+ **kw,
2985
+ )
2986
+
2987
+ return update_wrapper(wrap, fn)
2988
+
2989
+
2990
+ def _switch_db(dbname, connection, fn, *arg, **kw):
2991
+ if dbname:
2992
+ current_db = connection.exec_driver_sql("select db_name()").scalar()
2993
+ if current_db != dbname:
2994
+ connection.exec_driver_sql(
2995
+ "use %s" % connection.dialect.identifier_preparer.quote(dbname)
2996
+ )
2997
+ try:
2998
+ return fn(*arg, **kw)
2999
+ finally:
3000
+ if dbname and current_db != dbname:
3001
+ connection.exec_driver_sql(
3002
+ "use %s"
3003
+ % connection.dialect.identifier_preparer.quote(current_db)
3004
+ )
3005
+
3006
+
3007
+ def _owner_plus_db(dialect, schema):
3008
+ if not schema:
3009
+ return None, dialect.default_schema_name
3010
+ else:
3011
+ return _schema_elements(schema)
3012
+
3013
+
3014
+ _memoized_schema = util.LRUCache()
3015
+
3016
+
3017
+ def _schema_elements(schema):
3018
+ if isinstance(schema, quoted_name) and schema.quote:
3019
+ return None, schema
3020
+
3021
+ if schema in _memoized_schema:
3022
+ return _memoized_schema[schema]
3023
+
3024
+ # tests for this function are in:
3025
+ # test/dialect/mssql/test_reflection.py ->
3026
+ # OwnerPlusDBTest.test_owner_database_pairs
3027
+ # test/dialect/mssql/test_compiler.py -> test_force_schema_*
3028
+ # test/dialect/mssql/test_compiler.py -> test_schema_many_tokens_*
3029
+ #
3030
+
3031
+ if schema.startswith("__[SCHEMA_"):
3032
+ return None, schema
3033
+
3034
+ push = []
3035
+ symbol = ""
3036
+ bracket = False
3037
+ has_brackets = False
3038
+ for token in re.split(r"(\[|\]|\.)", schema):
3039
+ if not token:
3040
+ continue
3041
+ if token == "[":
3042
+ bracket = True
3043
+ has_brackets = True
3044
+ elif token == "]":
3045
+ bracket = False
3046
+ elif not bracket and token == ".":
3047
+ if has_brackets:
3048
+ push.append("[%s]" % symbol)
3049
+ else:
3050
+ push.append(symbol)
3051
+ symbol = ""
3052
+ has_brackets = False
3053
+ else:
3054
+ symbol += token
3055
+ if symbol:
3056
+ push.append(symbol)
3057
+ if len(push) > 1:
3058
+ dbname, owner = ".".join(push[0:-1]), push[-1]
3059
+
3060
+ # test for internal brackets
3061
+ if re.match(r".*\].*\[.*", dbname[1:-1]):
3062
+ dbname = quoted_name(dbname, quote=False)
3063
+ else:
3064
+ dbname = dbname.lstrip("[").rstrip("]")
3065
+
3066
+ elif len(push):
3067
+ dbname, owner = None, push[0]
3068
+ else:
3069
+ dbname, owner = None, None
3070
+
3071
+ _memoized_schema[schema] = dbname, owner
3072
+ return dbname, owner
3073
+
3074
+
3075
+ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
3076
+ # will assume it's at least mssql2005
3077
+ name = "mssql"
3078
+ supports_statement_cache = True
3079
+ supports_default_values = True
3080
+ supports_empty_insert = False
3081
+ favor_returning_over_lastrowid = True
3082
+ scope_identity_must_be_embedded = False
3083
+
3084
+ returns_native_bytes = True
3085
+
3086
+ supports_comments = True
3087
+ supports_default_metavalue = False
3088
+ """dialect supports INSERT... VALUES (DEFAULT) syntax -
3089
+ SQL Server **does** support this, but **not** for the IDENTITY column,
3090
+ so we can't turn this on.
3091
+
3092
+ """
3093
+
3094
+ aggregate_order_by_style = AggregateOrderByStyle.WITHIN_GROUP
3095
+
3096
+ # supports_native_uuid is partial here, so we implement our
3097
+ # own impl type
3098
+
3099
+ execution_ctx_cls = MSExecutionContext
3100
+ use_scope_identity = True
3101
+ max_identifier_length = 128
3102
+ schema_name = "dbo"
3103
+
3104
+ insert_returning = True
3105
+ update_returning = True
3106
+ delete_returning = True
3107
+ update_returning_multifrom = True
3108
+ delete_returning_multifrom = True
3109
+
3110
+ colspecs = {
3111
+ sqltypes.DateTime: _MSDateTime,
3112
+ sqltypes.Date: _MSDate,
3113
+ sqltypes.JSON: JSON,
3114
+ sqltypes.JSON.JSONIndexType: JSONIndexType,
3115
+ sqltypes.JSON.JSONPathType: JSONPathType,
3116
+ sqltypes.Time: _BASETIMEIMPL,
3117
+ sqltypes.Unicode: _MSUnicode,
3118
+ sqltypes.UnicodeText: _MSUnicodeText,
3119
+ DATETIMEOFFSET: DATETIMEOFFSET,
3120
+ DATETIME2: DATETIME2,
3121
+ SMALLDATETIME: SMALLDATETIME,
3122
+ DATETIME: DATETIME,
3123
+ sqltypes.Uuid: MSUUid,
3124
+ }
3125
+
3126
+ engine_config_types = default.DefaultDialect.engine_config_types.union(
3127
+ {"legacy_schema_aliasing": util.asbool}
3128
+ )
3129
+
3130
+ ischema_names = ischema_names
3131
+
3132
+ supports_sequences = True
3133
+ sequences_optional = True
3134
+ # This is actually used for autoincrement, where itentity is used that
3135
+ # starts with 1.
3136
+ # for sequences T-SQL's actual default is -9223372036854775808
3137
+ default_sequence_base = 1
3138
+
3139
+ supports_native_boolean = False
3140
+ non_native_boolean_check_constraint = False
3141
+ supports_unicode_binds = True
3142
+ postfetch_lastrowid = True
3143
+
3144
+ # may be changed at server inspection time for older SQL server versions
3145
+ supports_multivalues_insert = True
3146
+
3147
+ use_insertmanyvalues = True
3148
+
3149
+ # note pyodbc will set this to False if fast_executemany is set,
3150
+ # as of SQLAlchemy 2.0.9
3151
+ use_insertmanyvalues_wo_returning = True
3152
+
3153
+ insertmanyvalues_implicit_sentinel = (
3154
+ InsertmanyvaluesSentinelOpts.AUTOINCREMENT
3155
+ | InsertmanyvaluesSentinelOpts.IDENTITY
3156
+ | InsertmanyvaluesSentinelOpts.USE_INSERT_FROM_SELECT
3157
+ )
3158
+
3159
+ # "The incoming request has too many parameters. The server supports a "
3160
+ # "maximum of 2100 parameters."
3161
+ # in fact you can have 2099 parameters.
3162
+ insertmanyvalues_max_parameters = 2099
3163
+
3164
+ _supports_offset_fetch = False
3165
+ _supports_nvarchar_max = False
3166
+
3167
+ legacy_schema_aliasing = False
3168
+
3169
+ server_version_info = ()
3170
+
3171
+ statement_compiler = MSSQLCompiler
3172
+ ddl_compiler = MSDDLCompiler
3173
+ type_compiler_cls = MSTypeCompiler
3174
+ preparer = MSIdentifierPreparer
3175
+
3176
+ construct_arguments = [
3177
+ (sa_schema.PrimaryKeyConstraint, {"clustered": None}),
3178
+ (sa_schema.UniqueConstraint, {"clustered": None}),
3179
+ (
3180
+ sa_schema.Index,
3181
+ {
3182
+ "clustered": None,
3183
+ "include": None,
3184
+ "where": None,
3185
+ "columnstore": None,
3186
+ },
3187
+ ),
3188
+ (
3189
+ sa_schema.Column,
3190
+ {"identity_start": None, "identity_increment": None},
3191
+ ),
3192
+ ]
3193
+
3194
+ def __init__(
3195
+ self,
3196
+ query_timeout=None,
3197
+ use_scope_identity=True,
3198
+ schema_name="dbo",
3199
+ deprecate_large_types=None,
3200
+ supports_comments=None,
3201
+ json_serializer=None,
3202
+ json_deserializer=None,
3203
+ legacy_schema_aliasing=None,
3204
+ ignore_no_transaction_on_rollback=False,
3205
+ **opts,
3206
+ ):
3207
+ self.query_timeout = int(query_timeout or 0)
3208
+ self.schema_name = schema_name
3209
+
3210
+ self.use_scope_identity = use_scope_identity
3211
+ self.deprecate_large_types = deprecate_large_types
3212
+ self.ignore_no_transaction_on_rollback = (
3213
+ ignore_no_transaction_on_rollback
3214
+ )
3215
+ self._user_defined_supports_comments = uds = supports_comments
3216
+ if uds is not None:
3217
+ self.supports_comments = uds
3218
+
3219
+ if legacy_schema_aliasing is not None:
3220
+ util.warn_deprecated(
3221
+ "The legacy_schema_aliasing parameter is "
3222
+ "deprecated and will be removed in a future release.",
3223
+ "1.4",
3224
+ )
3225
+ self.legacy_schema_aliasing = legacy_schema_aliasing
3226
+
3227
+ super().__init__(**opts)
3228
+
3229
+ self._json_serializer = json_serializer
3230
+ self._json_deserializer = json_deserializer
3231
+
3232
+ def do_savepoint(self, connection, name):
3233
+ # give the DBAPI a push
3234
+ connection.exec_driver_sql("IF @@TRANCOUNT = 0 BEGIN TRANSACTION")
3235
+ super().do_savepoint(connection, name)
3236
+
3237
+ def do_release_savepoint(self, connection, name):
3238
+ # SQL Server does not support RELEASE SAVEPOINT
3239
+ pass
3240
+
3241
+ def do_rollback(self, dbapi_connection):
3242
+ try:
3243
+ super().do_rollback(dbapi_connection)
3244
+ except self.dbapi.ProgrammingError as e:
3245
+ if self.ignore_no_transaction_on_rollback and re.match(
3246
+ r".*\b111214\b", str(e)
3247
+ ):
3248
+ util.warn(
3249
+ "ProgrammingError 111214 "
3250
+ "'No corresponding transaction found.' "
3251
+ "has been suppressed via "
3252
+ "ignore_no_transaction_on_rollback=True"
3253
+ )
3254
+ else:
3255
+ raise
3256
+
3257
+ _isolation_lookup = {
3258
+ "SERIALIZABLE",
3259
+ "READ UNCOMMITTED",
3260
+ "READ COMMITTED",
3261
+ "REPEATABLE READ",
3262
+ "SNAPSHOT",
3263
+ }
3264
+
3265
+ def get_isolation_level_values(self, dbapi_connection):
3266
+ return list(self._isolation_lookup)
3267
+
3268
+ def set_isolation_level(self, dbapi_connection, level):
3269
+ cursor = dbapi_connection.cursor()
3270
+ cursor.execute(f"SET TRANSACTION ISOLATION LEVEL {level}")
3271
+ cursor.close()
3272
+ if level == "SNAPSHOT":
3273
+ dbapi_connection.commit()
3274
+
3275
+ def get_isolation_level(self, dbapi_connection):
3276
+ cursor = dbapi_connection.cursor()
3277
+ view_name = "sys.system_views"
3278
+ try:
3279
+ cursor.execute(
3280
+ (
3281
+ "SELECT name FROM {} WHERE name IN "
3282
+ "('dm_exec_sessions', 'dm_pdw_nodes_exec_sessions')"
3283
+ ).format(view_name)
3284
+ )
3285
+ row = cursor.fetchone()
3286
+ if not row:
3287
+ raise NotImplementedError(
3288
+ "Can't fetch isolation level on this particular "
3289
+ "SQL Server version."
3290
+ )
3291
+
3292
+ view_name = f"sys.{row[0]}"
3293
+
3294
+ cursor.execute("""
3295
+ SELECT CASE transaction_isolation_level
3296
+ WHEN 0 THEN NULL
3297
+ WHEN 1 THEN 'READ UNCOMMITTED'
3298
+ WHEN 2 THEN 'READ COMMITTED'
3299
+ WHEN 3 THEN 'REPEATABLE READ'
3300
+ WHEN 4 THEN 'SERIALIZABLE'
3301
+ WHEN 5 THEN 'SNAPSHOT' END
3302
+ AS TRANSACTION_ISOLATION_LEVEL
3303
+ FROM {}
3304
+ where session_id = @@SPID
3305
+ """.format(view_name))
3306
+ except self.dbapi.Error as err:
3307
+ raise NotImplementedError(
3308
+ "Can't fetch isolation level; encountered error {} when "
3309
+ 'attempting to query the "{}" view.'.format(err, view_name)
3310
+ ) from err
3311
+ else:
3312
+
3313
+ row = cursor.fetchone()
3314
+ return row[0].upper()
3315
+ finally:
3316
+ cursor.close()
3317
+
3318
+ def initialize(self, connection):
3319
+ super().initialize(connection)
3320
+ self._setup_version_attributes()
3321
+ self._setup_supports_nvarchar_max(connection)
3322
+ self._setup_supports_comments(connection)
3323
+
3324
+ def _setup_version_attributes(self):
3325
+ if self.server_version_info >= MS_2008_VERSION:
3326
+ self.supports_multivalues_insert = True
3327
+ else:
3328
+ self.supports_multivalues_insert = False
3329
+
3330
+ if self.deprecate_large_types is None:
3331
+ self.deprecate_large_types = (
3332
+ self.server_version_info >= MS_2012_VERSION
3333
+ )
3334
+
3335
+ self._supports_offset_fetch = (
3336
+ self.server_version_info and self.server_version_info[0] >= 11
3337
+ )
3338
+
3339
+ def _setup_supports_nvarchar_max(self, connection):
3340
+ try:
3341
+ connection.scalar(
3342
+ sql.text("SELECT CAST('test max support' AS NVARCHAR(max))")
3343
+ )
3344
+ except exc.DBAPIError:
3345
+ self._supports_nvarchar_max = False
3346
+ else:
3347
+ self._supports_nvarchar_max = True
3348
+
3349
+ def _setup_supports_comments(self, connection):
3350
+ if self._user_defined_supports_comments is not None:
3351
+ return
3352
+
3353
+ try:
3354
+ connection.scalar(
3355
+ sql.text(
3356
+ "SELECT 1 FROM fn_listextendedproperty"
3357
+ "(default, default, default, default, "
3358
+ "default, default, default)"
3359
+ )
3360
+ )
3361
+ except exc.DBAPIError:
3362
+ self.supports_comments = False
3363
+ else:
3364
+ self.supports_comments = True
3365
+
3366
+ def _get_default_schema_name(self, connection):
3367
+ query = sql.text("SELECT schema_name()")
3368
+ default_schema_name = connection.scalar(query)
3369
+ if default_schema_name is not None:
3370
+ # guard against the case where the default_schema_name is being
3371
+ # fed back into a table reflection function.
3372
+ return quoted_name(default_schema_name, quote=True)
3373
+ else:
3374
+ return self.schema_name
3375
+
3376
+ @reflection.cache
3377
+ @_db_plus_owner
3378
+ def has_table(self, connection, tablename, dbname, owner, schema, **kw):
3379
+ self._ensure_has_table_connection(connection)
3380
+
3381
+ return self._internal_has_multi_table(
3382
+ connection, [tablename], owner, schema, **kw
3383
+ )[schema, tablename]
3384
+
3385
+ def has_multi_table(self, connection, table_names, schema=None, **kw):
3386
+ # this does not really matche the signature of _db_plus_owner_multi
3387
+ # so manually do the unpacking here
3388
+ dbname, owner = _owner_plus_db(self, schema)
3389
+ return _switch_db(
3390
+ dbname,
3391
+ connection,
3392
+ self._internal_has_multi_table,
3393
+ connection,
3394
+ table_names,
3395
+ owner,
3396
+ schema,
3397
+ **kw,
3398
+ ).items()
3399
+
3400
+ @reflection.cache
3401
+ @_db_plus_owner
3402
+ def has_sequence(
3403
+ self, connection, sequencename, dbname, owner, schema, **kw
3404
+ ):
3405
+ sequences = ischema.sequences
3406
+
3407
+ s = sql.select(sequences.c.sequence_name).where(
3408
+ sequences.c.sequence_name == sequencename
3409
+ )
3410
+
3411
+ if owner:
3412
+ s = s.where(sequences.c.sequence_schema == owner)
3413
+
3414
+ c = connection.execute(s)
3415
+
3416
+ return c.first() is not None
3417
+
3418
+ @reflection.cache
3419
+ @_db_plus_owner_listing
3420
+ def get_sequence_names(self, connection, dbname, owner, schema, **kw):
3421
+ sequences = ischema.sequences
3422
+
3423
+ s = sql.select(sequences.c.sequence_name)
3424
+ if owner:
3425
+ s = s.where(sequences.c.sequence_schema == owner)
3426
+
3427
+ c = connection.execute(s)
3428
+
3429
+ return [row[0] for row in c]
3430
+
3431
+ @reflection.cache
3432
+ def get_schema_names(self, connection, **kw):
3433
+ s = sql.select(ischema.schemata.c.schema_name).order_by(
3434
+ ischema.schemata.c.schema_name
3435
+ )
3436
+ schema_names = [r[0] for r in connection.execute(s)]
3437
+ return schema_names
3438
+
3439
+ @reflection.cache
3440
+ @_db_plus_owner_listing
3441
+ def get_table_names(self, connection, dbname, owner, schema, **kw):
3442
+ tables = ischema.tables
3443
+ s = (
3444
+ sql.select(tables.c.table_name)
3445
+ .where(
3446
+ sql.and_(
3447
+ tables.c.table_schema == owner,
3448
+ tables.c.table_type == "BASE TABLE",
3449
+ )
3450
+ )
3451
+ .order_by(tables.c.table_name)
3452
+ )
3453
+ table_names = [r[0] for r in connection.execute(s)]
3454
+ return table_names
3455
+
3456
+ @reflection.cache
3457
+ @_db_plus_owner_listing
3458
+ def get_view_names(self, connection, dbname, owner, schema, **kw):
3459
+ tables = ischema.tables
3460
+ s = (
3461
+ sql.select(tables.c.table_name)
3462
+ .where(
3463
+ sql.and_(
3464
+ tables.c.table_schema == owner,
3465
+ tables.c.table_type == "VIEW",
3466
+ )
3467
+ )
3468
+ .order_by(tables.c.table_name)
3469
+ )
3470
+ view_names = [r[0] for r in connection.execute(s)]
3471
+ return view_names
3472
+
3473
+ def _internal_has_multi_table(
3474
+ self, connection, table_names, owner, schema, **kw
3475
+ ):
3476
+ regular_names, multi_object_names, temp_names = (
3477
+ self._partition_filter_names(
3478
+ connection, owner, table_names, ObjectScope.ANY, ObjectKind.ANY
3479
+ )
3480
+ )
3481
+
3482
+ result = {}
3483
+ if multi_object_names:
3484
+ # in multi_object_names we already have the names that exist in
3485
+ # the database, no need to do another query here
3486
+ name_map = self._multi_name_map(regular_names)
3487
+ for server_name in multi_object_names:
3488
+ result[(schema, name_map(server_name))] = True
3489
+
3490
+ if temp_names:
3491
+ # mssql does not support temporary views
3492
+ # U filters on user tables only.
3493
+ query = text("SELECT object_id(:table_name, 'U')")
3494
+ for temp_name in temp_names:
3495
+ result[(schema, temp_name)] = bool(
3496
+ connection.scalar(
3497
+ query,
3498
+ {"table_name": f"tempdb..[{temp_name}]"},
3499
+ )
3500
+ )
3501
+
3502
+ for name in table_names:
3503
+ result.setdefault((schema, name), False)
3504
+ return result
3505
+
3506
+ @reflection.cache
3507
+ @_db_plus_owner
3508
+ def get_view_definition(
3509
+ self, connection, viewname, dbname, owner, schema, **kw
3510
+ ):
3511
+ view_def = connection.execute(
3512
+ sql.text(
3513
+ "select mod.definition "
3514
+ "from sys.sql_modules as mod "
3515
+ "join sys.views as views on mod.object_id = views.object_id "
3516
+ "join sys.schemas as sch on views.schema_id = sch.schema_id "
3517
+ "where views.name=:viewname and sch.name=:schname"
3518
+ ).bindparams(
3519
+ sql.bindparam("viewname", viewname, ischema.CoerceUnicode()),
3520
+ sql.bindparam("schname", owner, ischema.CoerceUnicode()),
3521
+ )
3522
+ ).scalar()
3523
+ if view_def:
3524
+ return view_def
3525
+ else:
3526
+ raise exc.NoSuchTableError(f"{owner}.{viewname}")
3527
+
3528
+ def _temp_table_name_like_pattern(self, tablename):
3529
+ # LIKE uses '%' to match zero or more characters and '_' to match any
3530
+ # single character. We want to match literal underscores, so T-SQL
3531
+ # requires that we enclose them in square brackets.
3532
+ return tablename + (
3533
+ ("[_][_][_]%") if not tablename.startswith("##") else ""
3534
+ )
3535
+
3536
+ def _get_internal_temp_table_name(self, connection, tablename):
3537
+ # it's likely that schema is always "dbo", but since we can
3538
+ # get it here, let's get it.
3539
+ # see https://stackoverflow.com/questions/8311959/
3540
+ # specifying-schema-for-temporary-tables
3541
+
3542
+ try:
3543
+ return connection.execute(
3544
+ sql.text(
3545
+ "select table_schema, table_name "
3546
+ "from tempdb.information_schema.tables "
3547
+ "where table_name like :p1"
3548
+ ),
3549
+ {"p1": self._temp_table_name_like_pattern(tablename)},
3550
+ ).one()
3551
+ except exc.MultipleResultsFound as me:
3552
+ raise exc.UnreflectableTableError(
3553
+ "Found more than one temporary table named '%s' in tempdb "
3554
+ "at this time. Cannot reliably resolve that name to its "
3555
+ "internal table name." % tablename
3556
+ ) from me
3557
+ except exc.NoResultFound as ne:
3558
+ raise exc.NoSuchTableError(
3559
+ "Unable to find a temporary table named '%s' in tempdb."
3560
+ % tablename
3561
+ ) from ne
3562
+
3563
+ def _parse_column_info(
3564
+ self,
3565
+ name,
3566
+ type_,
3567
+ nullable,
3568
+ maxlen,
3569
+ numericprec,
3570
+ numericscale,
3571
+ default,
3572
+ collation,
3573
+ definition,
3574
+ is_persisted,
3575
+ is_identity,
3576
+ identity_start,
3577
+ identity_increment,
3578
+ comment,
3579
+ base_type=None,
3580
+ ):
3581
+ # Try to resolve the user type first (e.g., "sysname"),
3582
+ # then fall back to the base type (e.g., "nvarchar").
3583
+ # base_type may be None for CLR types (geography, geometry,
3584
+ # hierarchyid) which have no corresponding base type.
3585
+ coltype = self.ischema_names.get(type_, None)
3586
+ if coltype is None and base_type is not None and base_type != type_:
3587
+ coltype = self.ischema_names.get(base_type, None)
3588
+ kwargs = {}
3589
+
3590
+ if coltype in (MSBinary, MSVarBinary, sqltypes.LargeBinary):
3591
+ kwargs["length"] = maxlen if maxlen != -1 else None
3592
+ elif coltype in (MSString, MSChar):
3593
+ kwargs["length"] = maxlen if maxlen != -1 else None
3594
+ if collation:
3595
+ kwargs["collation"] = collation
3596
+ elif coltype in (MSNVarchar, MSNChar):
3597
+ kwargs["length"] = maxlen // 2 if maxlen != -1 else None
3598
+ if collation:
3599
+ kwargs["collation"] = collation
3600
+ elif coltype in (MSText, MSNText):
3601
+ # TEXT / NTEXT are unlengthed LOB types. sys.columns.max_length
3602
+ # reports 16 for these, which is the size of the in-row LOB
3603
+ # pointer and not a character length, so no length is applied.
3604
+ if collation:
3605
+ kwargs["collation"] = collation
3606
+
3607
+ if coltype is None:
3608
+ if base_type is not None and base_type != type_:
3609
+ util.warn(
3610
+ "Did not recognize type '%s' (user type) or '%s' "
3611
+ "(base type) of column '%s'" % (type_, base_type, name)
3612
+ )
3613
+ else:
3614
+ util.warn(
3615
+ "Did not recognize type '%s' of column '%s'"
3616
+ % (type_, name)
3617
+ )
3618
+ coltype = sqltypes.NULLTYPE
3619
+ else:
3620
+ if issubclass(coltype, sqltypes.NumericCommon):
3621
+ kwargs["precision"] = numericprec
3622
+ if not issubclass(coltype, sqltypes.Float):
3623
+ kwargs["scale"] = numericscale
3624
+ coltype = coltype(**kwargs)
3625
+
3626
+ cdict = {
3627
+ "name": name,
3628
+ "type": coltype,
3629
+ "nullable": nullable,
3630
+ "default": default,
3631
+ "autoincrement": is_identity is not None,
3632
+ "comment": comment,
3633
+ }
3634
+
3635
+ if definition is not None and is_persisted is not None:
3636
+ cdict["computed"] = {
3637
+ "sqltext": definition,
3638
+ "persisted": is_persisted,
3639
+ }
3640
+
3641
+ if is_identity is not None:
3642
+ if identity_start is None or identity_increment is None:
3643
+ cdict["identity"] = {}
3644
+ else:
3645
+ if isinstance(
3646
+ coltype, (sqltypes.BigInteger, sqltypes.Integer)
3647
+ ):
3648
+ start = int(identity_start)
3649
+ increment = int(identity_increment)
3650
+ else:
3651
+ start = identity_start
3652
+ increment = identity_increment
3653
+ cdict["identity"] = {
3654
+ "start": start,
3655
+ "increment": increment,
3656
+ }
3657
+
3658
+ return cdict
3659
+
3660
+ @lru_cache()
3661
+ def _columns_select(self):
3662
+ """Build the unified Core sys.* select for column reflection.
3663
+
3664
+ Returns a ``select()`` that includes ``table_name`` and ``owner``
3665
+ in the result so callers can group rows by table.
3666
+
3667
+ Used by :meth:`.get_multi_columns` (and indirectly by
3668
+ :meth:`.get_columns` via the multi delegation).
3669
+ """
3670
+ sys_columns = ischema.sys_columns
3671
+ sys_types = ischema.sys_types
3672
+ sys_base_types = ischema.sys_types.alias("base_types")
3673
+ sys_default_constraints = ischema.sys_default_constraints
3674
+ computed_cols = ischema.computed_columns
3675
+ identity_cols = ischema.identity_columns
3676
+ extended_properties = ischema.extended_properties
3677
+ sys_objects = ischema.sys_objects
3678
+ sys_schemas = ischema.sys_schemas
3679
+
3680
+ if self._supports_nvarchar_max:
3681
+ computed_definition = computed_cols.c.definition
3682
+ else:
3683
+ # tds_version 4.2 does not support NVARCHAR(MAX)
3684
+ computed_definition = sql.cast(
3685
+ computed_cols.c.definition, NVARCHAR(4000)
3686
+ )
3687
+
3688
+ s = (
3689
+ sql.select(
3690
+ sys_objects.c.name.label("table_name"),
3691
+ sys_schemas.c.name.label("owner"),
3692
+ sys_columns.c.name.label("column_name"),
3693
+ sys_types.c.name.label("type_name"),
3694
+ sys_base_types.c.name.label("base_type"),
3695
+ sys_columns.c.is_nullable,
3696
+ sys_columns.c.max_length,
3697
+ sys_columns.c.precision,
3698
+ sys_columns.c.scale,
3699
+ sys_default_constraints.c.definition.label("default_value"),
3700
+ sys_columns.c.collation_name,
3701
+ computed_definition.label("computed_definition"),
3702
+ computed_cols.c.is_persisted,
3703
+ identity_cols.c.is_identity,
3704
+ identity_cols.c.seed_value,
3705
+ identity_cols.c.increment_value,
3706
+ extended_properties.c.value.label("comment"),
3707
+ sys_columns.c.column_id,
3708
+ )
3709
+ .select_from(sys_columns)
3710
+ .join(
3711
+ sys_objects,
3712
+ onclause=sys_columns.c.object_id == sys_objects.c.object_id,
3713
+ )
3714
+ .join(
3715
+ sys_schemas,
3716
+ onclause=sys_objects.c.schema_id == sys_schemas.c.schema_id,
3717
+ )
3718
+ .join(
3719
+ sys_types,
3720
+ onclause=sys_columns.c.user_type_id
3721
+ == sys_types.c.user_type_id,
3722
+ )
3723
+ .outerjoin(
3724
+ sys_base_types,
3725
+ onclause=sql.and_(
3726
+ sys_types.c.system_type_id
3727
+ == sys_base_types.c.system_type_id,
3728
+ sys_base_types.c.user_type_id
3729
+ == sys_base_types.c.system_type_id,
3730
+ ),
3731
+ )
3732
+ .outerjoin(
3733
+ sys_default_constraints,
3734
+ sql.and_(
3735
+ sys_default_constraints.c.object_id
3736
+ == sys_columns.c.default_object_id,
3737
+ sys_default_constraints.c.parent_column_id
3738
+ == sys_columns.c.column_id,
3739
+ ),
3740
+ )
3741
+ .outerjoin(
3742
+ computed_cols,
3743
+ onclause=sql.and_(
3744
+ computed_cols.c.object_id == sys_columns.c.object_id,
3745
+ computed_cols.c.column_id == sys_columns.c.column_id,
3746
+ ),
3747
+ )
3748
+ .outerjoin(
3749
+ identity_cols,
3750
+ onclause=sql.and_(
3751
+ identity_cols.c.object_id == sys_columns.c.object_id,
3752
+ identity_cols.c.column_id == sys_columns.c.column_id,
3753
+ ),
3754
+ )
3755
+ .outerjoin(
3756
+ extended_properties,
3757
+ onclause=sql.and_(
3758
+ extended_properties.c["class"] == 1,
3759
+ extended_properties.c.name == "MS_Description",
3760
+ sys_columns.c.object_id == extended_properties.c.major_id,
3761
+ sys_columns.c.column_id == extended_properties.c.minor_id,
3762
+ ),
3763
+ )
3764
+ .where(
3765
+ ischema.sys_schemas.c.name == sql.bindparam("owner"),
3766
+ ischema.sys_objects.c.name.in_(
3767
+ sql.bindparam("filter_names", expanding=True)
3768
+ ),
3769
+ )
3770
+ .order_by(sys_objects.c.name, sys_columns.c.column_id)
3771
+ )
3772
+ return s
3773
+
3774
+ @staticmethod
3775
+ def _fk_query_sql(fk_info_where, extra_cols=""):
3776
+ return """\
3777
+ WITH fk_info AS (
3778
+ SELECT
3779
+ ischema_ref_con.constraint_schema,
3780
+ ischema_ref_con.constraint_name,
3781
+ ischema_key_col.ordinal_position,
3782
+ ischema_key_col.table_schema,
3783
+ ischema_key_col.table_name,
3784
+ ischema_ref_con.unique_constraint_schema,
3785
+ ischema_ref_con.unique_constraint_name,
3786
+ ischema_ref_con.match_option,
3787
+ ischema_ref_con.update_rule,
3788
+ ischema_ref_con.delete_rule,
3789
+ ischema_key_col.column_name AS constrained_column
3790
+ FROM
3791
+ INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ischema_ref_con
3792
+ INNER JOIN
3793
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE ischema_key_col ON
3794
+ ischema_key_col.table_schema =
3795
+ ischema_ref_con.constraint_schema
3796
+ AND ischema_key_col.constraint_name =
3797
+ ischema_ref_con.constraint_name
3798
+ WHERE %(fk_info_where)s
3799
+ ),
3800
+ constraint_info AS (
3801
+ SELECT
3802
+ ischema_key_col.constraint_schema,
3803
+ ischema_key_col.constraint_name,
3804
+ ischema_key_col.ordinal_position,
3805
+ ischema_key_col.table_schema,
3806
+ ischema_key_col.table_name,
3807
+ ischema_key_col.column_name
3808
+ FROM
3809
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE ischema_key_col
3810
+ ),
3811
+ index_info AS (
3812
+ SELECT
3813
+ sys.schemas.name AS index_schema,
3814
+ sys.indexes.name AS index_name,
3815
+ sys.index_columns.key_ordinal AS ordinal_position,
3816
+ sys.schemas.name AS table_schema,
3817
+ sys.objects.name AS table_name,
3818
+ sys.columns.name AS column_name
3819
+ FROM
3820
+ sys.indexes
3821
+ INNER JOIN sys.objects
3822
+ ON sys.objects.object_id = sys.indexes.object_id
3823
+ INNER JOIN sys.schemas
3824
+ ON sys.schemas.schema_id = sys.objects.schema_id
3825
+ INNER JOIN sys.index_columns
3826
+ ON sys.index_columns.object_id = sys.objects.object_id
3827
+ AND sys.index_columns.index_id = sys.indexes.index_id
3828
+ INNER JOIN sys.columns
3829
+ ON sys.columns.object_id = sys.indexes.object_id
3830
+ AND sys.columns.column_id = sys.index_columns.column_id
3831
+ )
3832
+ SELECT
3833
+ %(extra_cols)s
3834
+ fk_info.constraint_schema,
3835
+ fk_info.constraint_name,
3836
+ fk_info.ordinal_position,
3837
+ fk_info.constrained_column,
3838
+ constraint_info.table_schema AS referred_table_schema,
3839
+ constraint_info.table_name AS referred_table_name,
3840
+ constraint_info.column_name AS referred_column,
3841
+ fk_info.match_option,
3842
+ fk_info.update_rule,
3843
+ fk_info.delete_rule
3844
+ FROM
3845
+ fk_info INNER JOIN constraint_info ON
3846
+ constraint_info.constraint_schema =
3847
+ fk_info.unique_constraint_schema
3848
+ AND constraint_info.constraint_name =
3849
+ fk_info.unique_constraint_name
3850
+ AND constraint_info.ordinal_position = fk_info.ordinal_position
3851
+ UNION
3852
+ SELECT
3853
+ %(extra_cols)s
3854
+ fk_info.constraint_schema,
3855
+ fk_info.constraint_name,
3856
+ fk_info.ordinal_position,
3857
+ fk_info.constrained_column,
3858
+ index_info.table_schema AS referred_table_schema,
3859
+ index_info.table_name AS referred_table_name,
3860
+ index_info.column_name AS referred_column,
3861
+ fk_info.match_option,
3862
+ fk_info.update_rule,
3863
+ fk_info.delete_rule
3864
+ FROM
3865
+ fk_info INNER JOIN index_info ON
3866
+ index_info.index_schema = fk_info.unique_constraint_schema
3867
+ AND index_info.index_name = fk_info.unique_constraint_name
3868
+ AND index_info.ordinal_position = fk_info.ordinal_position
3869
+ AND NOT (index_info.table_schema = fk_info.table_schema
3870
+ AND index_info.table_name = fk_info.table_name)
3871
+
3872
+ ORDER BY fk_info.constraint_schema, fk_info.constraint_name,
3873
+ fk_info.ordinal_position
3874
+ """ % {
3875
+ "fk_info_where": fk_info_where,
3876
+ "extra_cols": extra_cols,
3877
+ }
3878
+
3879
+ # --- multi-reflection API ---
3880
+
3881
+ @lru_cache
3882
+ def _partition_filter_names_query(self, add_filter_names: bool):
3883
+ sys_objects = ischema.sys_objects
3884
+ sys_schemas = ischema.sys_schemas
3885
+
3886
+ s = (
3887
+ sql.select(sys_objects.c.name)
3888
+ .select_from(sys_objects)
3889
+ .join(
3890
+ sys_schemas,
3891
+ onclause=sys_objects.c.schema_id == sys_schemas.c.schema_id,
3892
+ )
3893
+ .where(
3894
+ sys_schemas.c.name == sql.bindparam("schema"),
3895
+ sys_objects.c.type.in_(
3896
+ sql.bindparam("type_filter", expanding=True)
3897
+ ),
3898
+ )
3899
+ )
3900
+ if add_filter_names:
3901
+ s = s.where(
3902
+ sys_objects.c.name.in_(
3903
+ sql.bindparam("filter_names", expanding=True)
3904
+ )
3905
+ )
3906
+
3907
+ return s
3908
+
3909
+ def _partition_filter_names(
3910
+ self, connection, owner, filter_names, scope, kind
3911
+ ):
3912
+ """Split caller-supplied ``filter_names`` between the bulk
3913
+ ``sys.*`` query path (regular objects in the user database) and
3914
+ the temp object path (objects living in ``tempdb``), and resolve
3915
+ the existing-in-catalog names for the bulk path in one shot.
3916
+
3917
+ Returns the tuple
3918
+ ``(regular_names, multi_object_names, temp_names)``:
3919
+
3920
+ * ``regular_names`` -- list of caller-supplied names (with the
3921
+ caller's casing) for the bulk path, used to build the
3922
+ case-insensitive name map. Empty list when no regular names
3923
+ were requested.
3924
+ * ``multi_object_names`` -- list of names (with server-side
3925
+ casing) that actually exist in the catalog and match the
3926
+ requested ``kind``. This is what the bulk SQL's IN clause
3927
+ gets filtered with. Empty list when no matches, disabling
3928
+ the bulk query.
3929
+ * ``temp_names`` -- list of ``#``-prefixed temp object names
3930
+ that the caller asked for and that the requested
3931
+ ``scope``/``kind`` allows.
3932
+
3933
+ ``scope=ObjectScope.DEFAULT`` excludes temp objects entirely.
3934
+ ``scope=ObjectScope.TEMPORARY`` excludes regular objects.
3935
+ ``ObjectKind.VIEW`` (without TABLE) excludes temp because SQL
3936
+ Server does not have temp views in the same form. SQL Server
3937
+ also has no ``get_temp_table_names`` implementation, so temp
3938
+ objects can only be reflected when the caller supplies their
3939
+ names explicitly.
3940
+ """
3941
+ include_default = scope is not ObjectScope.TEMPORARY
3942
+ include_temp = (
3943
+ scope is not ObjectScope.DEFAULT and ObjectKind.TABLE in kind
3944
+ )
3945
+
3946
+ if filter_names:
3947
+ if include_temp:
3948
+ temp_names = [n for n in filter_names if n.startswith("#")]
3949
+ else:
3950
+ temp_names = []
3951
+
3952
+ if include_default:
3953
+ regular_names = [
3954
+ n for n in filter_names if not n.startswith("#")
3955
+ ]
3956
+ run_bulk = bool(regular_names)
3957
+ else:
3958
+ regular_names = []
3959
+ run_bulk = False
3960
+ else:
3961
+ temp_names = []
3962
+ regular_names = []
3963
+ run_bulk = include_default
3964
+
3965
+ if not run_bulk:
3966
+ return (regular_names, [], temp_names)
3967
+
3968
+ type_filter = []
3969
+ if ObjectKind.TABLE in kind:
3970
+ type_filter.append("U")
3971
+ if ObjectKind.VIEW in kind:
3972
+ type_filter.append("V")
3973
+ # SQL Server does not support materialized views, so ignore them
3974
+ if not type_filter:
3975
+ return (regular_names, [], temp_names)
3976
+
3977
+ has_regular_names = bool(regular_names)
3978
+ query = self._partition_filter_names_query(has_regular_names)
3979
+ params = {"schema": owner, "type_filter": type_filter}
3980
+ if has_regular_names:
3981
+ params["filter_names"] = regular_names
3982
+
3983
+ multi_object_names = connection.scalars(query, params).all()
3984
+
3985
+ return (regular_names, multi_object_names, temp_names)
3986
+
3987
+ @staticmethod
3988
+ def _multi_name_map(filter_names):
3989
+ """Build a case-insensitive ``server_name -> user_name`` mapper.
3990
+
3991
+ MSSQL object names are case-insensitive under the default
3992
+ collation, so a user may pass ``"sOmEtAbLe"`` for a table
3993
+ physically stored as ``"SomeTable"``. The bulk SQL returns the
3994
+ server-side casing, but result keys must use the user-supplied
3995
+ casing so that downstream code (e.g.
3996
+ :meth:`.Inspector.reflect_table`) can find the entry.
3997
+
3998
+ Returns a callable ``(server_name) -> user_name``. When
3999
+ ``filter_names`` is empty/None, returns identity.
4000
+ """
4001
+ if not filter_names:
4002
+ return lambda n: n
4003
+ lookup = {n.lower(): n for n in filter_names}
4004
+ return lambda n: lookup.get(n.lower(), n)
4005
+
4006
+ @_db_plus_owner_multi
4007
+ def get_multi_columns(
4008
+ self,
4009
+ connection,
4010
+ dbname,
4011
+ owner,
4012
+ schema,
4013
+ filter_names,
4014
+ scope,
4015
+ kind,
4016
+ **kw,
4017
+ ):
4018
+ regular_names, multi_object_names, temp_names = (
4019
+ self._partition_filter_names(
4020
+ connection, owner, filter_names, scope, kind
4021
+ )
4022
+ )
4023
+
4024
+ result = {}
4025
+
4026
+ if multi_object_names:
4027
+ name_map = self._multi_name_map(regular_names)
4028
+ self._fetch_multi_columns(
4029
+ connection,
4030
+ owner=owner,
4031
+ names=multi_object_names,
4032
+ schema=schema,
4033
+ name_map=name_map,
4034
+ result=result,
4035
+ exec_opts={"schema_translate_map": {}},
4036
+ )
4037
+
4038
+ if temp_names:
4039
+ self._fetch_multi_columns_temp(
4040
+ connection,
4041
+ temp_names=temp_names,
4042
+ schema=schema,
4043
+ result=result,
4044
+ )
4045
+
4046
+ return result.items()
4047
+
4048
+ def _fetch_multi_columns(
4049
+ self, connection, owner, names, schema, name_map, result, exec_opts
4050
+ ):
4051
+ """Execute the unified columns select for a single catalog pass.
4052
+
4053
+ Used by :meth:`.get_multi_columns` for both the main-database pass
4054
+ (no ``schema_translate_map``) and the tempdb pass (with
4055
+ ``schema_translate_map={"sys": "tempdb.sys"}`` applied).
4056
+ """
4057
+ s = self._columns_select()
4058
+
4059
+ rp = connection.execute(
4060
+ s,
4061
+ {"owner": owner, "filter_names": names},
4062
+ execution_options=exec_opts,
4063
+ )
4064
+
4065
+ for row in rp.mappings():
4066
+ table_name = name_map(row["table_name"])
4067
+ cdict = self._parse_column_info(
4068
+ name=row["column_name"],
4069
+ type_=row["type_name"],
4070
+ base_type=row["base_type"],
4071
+ nullable=row["is_nullable"] == 1,
4072
+ maxlen=row["max_length"],
4073
+ numericprec=row["precision"],
4074
+ numericscale=row["scale"],
4075
+ default=row["default_value"],
4076
+ collation=row["collation_name"],
4077
+ definition=row["computed_definition"],
4078
+ is_persisted=row["is_persisted"],
4079
+ is_identity=row["is_identity"],
4080
+ identity_start=row["seed_value"],
4081
+ identity_increment=row["increment_value"],
4082
+ comment=row["comment"],
4083
+ )
4084
+ result.setdefault((schema, table_name), []).append(cdict)
4085
+
4086
+ for n in names:
4087
+ key = (schema, name_map(n))
4088
+ if key not in result:
4089
+ result[key] = ReflectionDefaults.columns()
4090
+
4091
+ def _fetch_multi_columns_temp(
4092
+ self, connection, temp_names, schema, result
4093
+ ):
4094
+ """Run the unified columns select against tempdb for temp names.
4095
+
4096
+ Resolves each ``#name`` to its mangled tempdb name (one round
4097
+ trip per temp via :meth:`._get_internal_temp_table_name`), then
4098
+ runs a single bulk select against ``tempdb.sys.*`` via
4099
+ ``schema_translate_map``. The result rows (keyed by mangled
4100
+ name) are mapped back to the original ``#name`` for the caller.
4101
+ """
4102
+ resolved_by_owner, temp_name_map = self._resolve_temp_names(
4103
+ connection, temp_names
4104
+ )
4105
+ for temp_owner, mangled_names in resolved_by_owner.items():
4106
+ self._fetch_multi_columns(
4107
+ connection,
4108
+ owner=temp_owner,
4109
+ names=mangled_names,
4110
+ schema=schema,
4111
+ name_map=temp_name_map,
4112
+ result=result,
4113
+ exec_opts={"schema_translate_map": {"sys": "tempdb.sys"}},
4114
+ )
4115
+
4116
+ def _resolve_temp_names(self, connection, temp_names):
4117
+ """Resolve user-facing temp names (``#foo``) to their mangled
4118
+ tempdb names, grouped by owner.
4119
+
4120
+ Returns ``(resolved_by_owner, temp_name_map)``:
4121
+
4122
+ * ``resolved_by_owner`` -- ``{owner: [mangled_name, ...]}``,
4123
+ one bulk query per owner (typically just ``'dbo'``).
4124
+ * ``temp_name_map`` -- a function that maps mangled names back
4125
+ to the user-supplied name.
4126
+
4127
+ Names that don't resolve (no matching temp table in tempdb) are
4128
+ silently dropped. The single-table reflection wrappers handle
4129
+ the resulting absence by raising :exc:`.NoSuchTableError` via
4130
+ :meth:`._value_or_raise`.
4131
+ """
4132
+ resolved_by_owner = {}
4133
+ original_by_mangled = {}
4134
+ for name in temp_names:
4135
+ try:
4136
+ temp_owner, mangled = self._get_internal_temp_table_name(
4137
+ connection, name
4138
+ )
4139
+ except exc.NoSuchTableError:
4140
+ continue
4141
+ resolved_by_owner.setdefault(temp_owner, []).append(mangled)
4142
+ original_by_mangled[mangled] = name
4143
+
4144
+ def temp_name_map(server_name):
4145
+ return original_by_mangled.get(server_name, server_name)
4146
+
4147
+ return resolved_by_owner, temp_name_map
4148
+
4149
+ @_db_plus_owner_multi
4150
+ def get_multi_pk_constraint(
4151
+ self,
4152
+ connection,
4153
+ dbname,
4154
+ owner,
4155
+ schema,
4156
+ filter_names,
4157
+ scope,
4158
+ kind,
4159
+ **kw,
4160
+ ):
4161
+ regular_names, multi_object_names, temp_names = (
4162
+ self._partition_filter_names(
4163
+ connection, owner, filter_names, scope, kind
4164
+ )
4165
+ )
4166
+
4167
+ result = {}
4168
+
4169
+ if multi_object_names:
4170
+ name_map = self._multi_name_map(regular_names)
4171
+ self._fetch_multi_pk_constraint(
4172
+ connection,
4173
+ owner=owner,
4174
+ names=multi_object_names,
4175
+ schema=schema,
4176
+ name_map=name_map,
4177
+ result=result,
4178
+ exec_opts={"schema_translate_map": {}},
4179
+ )
4180
+
4181
+ if temp_names:
4182
+ self._fetch_multi_pk_constraint_temp(
4183
+ connection,
4184
+ temp_names=temp_names,
4185
+ schema=schema,
4186
+ result=result,
4187
+ )
4188
+
4189
+ return result.items()
4190
+
4191
+ @lru_cache()
4192
+ def _pk_constraint_select(self):
4193
+ """Build the unified Core sys.* select for PK constraint reflection.
4194
+
4195
+ Returns a ``select()`` that includes ``table_name`` and ``owner``
4196
+ in the result so callers can group rows by table. The clustered
4197
+ flag comes directly from ``sys.indexes.type`` (1 = clustered).
4198
+ """
4199
+ sys_key_constraints = ischema.sys_key_constraints
4200
+ sys_indexes = ischema.sys_indexes
4201
+ sys_index_columns = ischema.sys_index_columns
4202
+ sys_columns = ischema.sys_columns
4203
+ sys_objects = ischema.sys_objects
4204
+ sys_schemas = ischema.sys_schemas
4205
+
4206
+ s = (
4207
+ sql.select(
4208
+ sys_objects.c.name.label("table_name"),
4209
+ sys_schemas.c.name.label("owner"),
4210
+ sys_key_constraints.c.name.label("constraint_name"),
4211
+ sys_columns.c.name.label("column_name"),
4212
+ sys_index_columns.c.key_ordinal,
4213
+ sys_indexes.c.type.label("index_type"),
4214
+ )
4215
+ .select_from(sys_key_constraints)
4216
+ .join(
4217
+ sys_objects,
4218
+ onclause=sys_key_constraints.c.parent_object_id
4219
+ == sys_objects.c.object_id,
4220
+ )
4221
+ .join(
4222
+ sys_schemas,
4223
+ onclause=sys_objects.c.schema_id == sys_schemas.c.schema_id,
4224
+ )
4225
+ .join(
4226
+ sys_index_columns,
4227
+ onclause=sql.and_(
4228
+ sys_index_columns.c.object_id
4229
+ == sys_key_constraints.c.parent_object_id,
4230
+ sys_index_columns.c.index_id
4231
+ == sys_key_constraints.c.unique_index_id,
4232
+ ),
4233
+ )
4234
+ .join(
4235
+ sys_columns,
4236
+ onclause=sql.and_(
4237
+ sys_columns.c.object_id == sys_index_columns.c.object_id,
4238
+ sys_columns.c.column_id == sys_index_columns.c.column_id,
4239
+ ),
4240
+ )
4241
+ .join(
4242
+ sys_indexes,
4243
+ onclause=sql.and_(
4244
+ sys_indexes.c.object_id
4245
+ == sys_key_constraints.c.parent_object_id,
4246
+ sys_indexes.c.index_id
4247
+ == sys_key_constraints.c.unique_index_id,
4248
+ ),
4249
+ )
4250
+ .where(
4251
+ sys_key_constraints.c.type == "PK",
4252
+ ischema.sys_schemas.c.name == sql.bindparam("owner"),
4253
+ ischema.sys_objects.c.name.in_(
4254
+ sql.bindparam("filter_names", expanding=True)
4255
+ ),
4256
+ )
4257
+ .order_by(sys_objects.c.name, sys_index_columns.c.key_ordinal)
4258
+ )
4259
+ return s
4260
+
4261
+ def _fetch_multi_pk_constraint(
4262
+ self, connection, owner, names, schema, name_map, result, exec_opts
4263
+ ):
4264
+ """Execute the unified pk_constraint select for one catalog pass.
4265
+
4266
+ Used by :meth:`.get_multi_pk_constraint` for both the main DB
4267
+ pass and the tempdb pass (with ``schema_translate_map`` set).
4268
+ """
4269
+ s = self._pk_constraint_select()
4270
+
4271
+ rp = connection.execute(
4272
+ s,
4273
+ {"owner": owner, "filter_names": names},
4274
+ execution_options=exec_opts,
4275
+ )
4276
+
4277
+ for row in rp.mappings():
4278
+ table_name = name_map(row["table_name"])
4279
+ key = (schema, table_name)
4280
+ if key not in result:
4281
+ result[key] = {
4282
+ "constrained_columns": [],
4283
+ "name": row["constraint_name"],
4284
+ "dialect_options": {
4285
+ "mssql_clustered": row["index_type"] == 1
4286
+ },
4287
+ }
4288
+ result[key]["constrained_columns"].append(row["column_name"])
4289
+
4290
+ for n in names:
4291
+ key = (schema, name_map(n))
4292
+ if key not in result:
4293
+ result[key] = ReflectionDefaults.pk_constraint()
4294
+
4295
+ def _fetch_multi_pk_constraint_temp(
4296
+ self, connection, temp_names, schema, result
4297
+ ):
4298
+ """Run the unified pk_constraint select against tempdb."""
4299
+ resolved_by_owner, temp_name_map = self._resolve_temp_names(
4300
+ connection, temp_names
4301
+ )
4302
+
4303
+ for temp_owner, mangled_names in resolved_by_owner.items():
4304
+ self._fetch_multi_pk_constraint(
4305
+ connection,
4306
+ owner=temp_owner,
4307
+ names=mangled_names,
4308
+ schema=schema,
4309
+ name_map=temp_name_map,
4310
+ result=result,
4311
+ exec_opts={"schema_translate_map": {"sys": "tempdb.sys"}},
4312
+ )
4313
+
4314
+ @_db_plus_owner_multi
4315
+ def get_multi_foreign_keys(
4316
+ self,
4317
+ connection,
4318
+ dbname,
4319
+ owner,
4320
+ schema,
4321
+ filter_names,
4322
+ scope,
4323
+ kind,
4324
+ **kw,
4325
+ ):
4326
+ regular_names, multi_object_names, temp_names = (
4327
+ self._partition_filter_names(
4328
+ connection, owner, filter_names, scope, kind
4329
+ )
4330
+ )
4331
+
4332
+ final = {}
4333
+
4334
+ if multi_object_names:
4335
+ name_map = self._multi_name_map(regular_names)
4336
+ self._fetch_multi_foreign_keys(
4337
+ connection,
4338
+ dbname=dbname,
4339
+ owner=owner,
4340
+ names=multi_object_names,
4341
+ schema=schema,
4342
+ name_map=name_map,
4343
+ final=final,
4344
+ )
4345
+
4346
+ # FK reflection for temp tables: the INFORMATION_SCHEMA query in
4347
+ # _fk_query_sql does not see tempdb objects (it queries the main
4348
+ # DB's catalog). The pre-existing single-table behavior was to
4349
+ # return empty foreign_keys for temp tables that exist. We
4350
+ # preserve that here by checking existence and returning the
4351
+ # default (empty) reflection for each temp that is reachable.
4352
+ has_temp_tables = self._internal_has_multi_table(
4353
+ connection, temp_names, owner="dbo", schema=schema, **kw
4354
+ )
4355
+ for name in temp_names:
4356
+ key = (schema, name)
4357
+ if has_temp_tables.get(key):
4358
+ final[key] = ReflectionDefaults.foreign_keys()
4359
+
4360
+ return final.items()
4361
+
4362
+ def _fetch_multi_foreign_keys(
4363
+ self, connection, dbname, owner, names, schema, name_map, final
4364
+ ):
4365
+ """Execute the unified FK query for the main-DB pass.
4366
+
4367
+ Uses the _fk_query_sql sql text, restricted to the provided owner and
4368
+ IN-list of names. Groups result rows by table and constraint
4369
+ name into the final dict.
4370
+ """
4371
+ rp = connection.execute(
4372
+ sql.text(
4373
+ self._fk_query_sql(
4374
+ fk_info_where=(
4375
+ "ischema_key_col.table_schema = :owner"
4376
+ "\n AND ischema_key_col.table_name"
4377
+ " IN :filter_names"
4378
+ ),
4379
+ extra_cols="fk_info.table_name,",
4380
+ )
4381
+ )
4382
+ .bindparams(
4383
+ sql.bindparam("owner", owner, ischema.CoerceUnicode()),
4384
+ sql.bindparam("filter_names", names, expanding=True),
4385
+ )
4386
+ .columns(
4387
+ constraint_schema=sqltypes.Unicode(),
4388
+ constraint_name=sqltypes.Unicode(),
4389
+ table_name=sqltypes.Unicode(),
4390
+ constrained_column=sqltypes.Unicode(),
4391
+ referred_table_schema=sqltypes.Unicode(),
4392
+ referred_table_name=sqltypes.Unicode(),
4393
+ referred_column=sqltypes.Unicode(),
4394
+ )
4395
+ )
4396
+
4397
+ grouped = {}
4398
+ for r in rp.all():
4399
+ (
4400
+ table_name,
4401
+ _, # constraint_schema
4402
+ rfknm,
4403
+ _, # ordinal
4404
+ scol,
4405
+ rschema,
4406
+ rtbl,
4407
+ rcol,
4408
+ _, # match
4409
+ fkuprule,
4410
+ fkdelrule,
4411
+ ) = r
4412
+
4413
+ key = (schema, name_map(table_name))
4414
+ if key not in grouped:
4415
+ grouped[key] = util.defaultdict(
4416
+ lambda: {
4417
+ "name": None,
4418
+ "constrained_columns": [],
4419
+ "referred_schema": None,
4420
+ "referred_table": None,
4421
+ "referred_columns": [],
4422
+ "options": {},
4423
+ }
4424
+ )
4425
+
4426
+ rec = grouped[key][rfknm]
4427
+ rec["name"] = rfknm
4428
+
4429
+ if fkuprule != "NO ACTION":
4430
+ rec["options"]["onupdate"] = fkuprule
4431
+ if fkdelrule != "NO ACTION":
4432
+ rec["options"]["ondelete"] = fkdelrule
4433
+
4434
+ if not rec["referred_table"]:
4435
+ rec["referred_table"] = rtbl
4436
+ if schema is not None or owner != rschema:
4437
+ if dbname:
4438
+ rschema = dbname + "." + rschema
4439
+ rec["referred_schema"] = rschema
4440
+
4441
+ rec["constrained_columns"].append(scol)
4442
+ rec["referred_columns"].append(rcol)
4443
+
4444
+ for key, fk_dict in grouped.items():
4445
+ final[key] = list(fk_dict.values())
4446
+
4447
+ for n in names:
4448
+ key = (schema, name_map(n))
4449
+ if key not in final:
4450
+ final[key] = ReflectionDefaults.foreign_keys()
4451
+
4452
+ @_db_plus_owner_multi
4453
+ def get_multi_indexes(
4454
+ self,
4455
+ connection,
4456
+ dbname,
4457
+ owner,
4458
+ schema,
4459
+ filter_names,
4460
+ scope,
4461
+ kind,
4462
+ **kw,
4463
+ ):
4464
+ regular_names, multi_object_names, temp_names = (
4465
+ self._partition_filter_names(
4466
+ connection, owner, filter_names, scope, kind
4467
+ )
4468
+ )
4469
+
4470
+ result = {}
4471
+
4472
+ if multi_object_names:
4473
+ name_map = self._multi_name_map(regular_names)
4474
+ self._fetch_multi_indexes(
4475
+ connection,
4476
+ owner=owner,
4477
+ names=multi_object_names,
4478
+ schema=schema,
4479
+ name_map=name_map,
4480
+ result=result,
4481
+ exec_opts={"schema_translate_map": {}},
4482
+ )
4483
+
4484
+ if temp_names:
4485
+ self._fetch_multi_indexes_temp(
4486
+ connection,
4487
+ temp_names=temp_names,
4488
+ schema=schema,
4489
+ result=result,
4490
+ )
4491
+
4492
+ return result.items()
4493
+
4494
+ @lru_cache()
4495
+ def _indexes_metadata_select(self):
4496
+ """Build the Core sys.* select for index metadata (one row per
4497
+ index per table).
4498
+
4499
+ Used by :meth:`._fetch_multi_indexes` for both the main-DB pass
4500
+ and the tempdb pass (via ``schema_translate_map``). Replaces a
4501
+ previous ``sql.text()`` body that did not honor
4502
+ ``schema_translate_map`` (which only rewrites Core
4503
+ schema-bearing objects, not literal SQL text).
4504
+ """
4505
+ sys_indexes = ischema.sys_indexes
4506
+ sys_objects = ischema.sys_objects
4507
+ sys_schemas = ischema.sys_schemas
4508
+
4509
+ if self.server_version_info >= MS_2008_VERSION:
4510
+ filter_definition = sys_indexes.c.filter_definition
4511
+ else:
4512
+ filter_definition = sql.null()
4513
+
4514
+ s = (
4515
+ sql.select(
4516
+ sys_objects.c.name.label("table_name"),
4517
+ sys_indexes.c.index_id,
4518
+ sys_indexes.c.is_unique,
4519
+ sys_indexes.c.name,
4520
+ sys_indexes.c.type,
4521
+ filter_definition.label("filter_definition"),
4522
+ )
4523
+ .select_from(sys_indexes)
4524
+ .join(
4525
+ sys_objects,
4526
+ onclause=sys_indexes.c.object_id == sys_objects.c.object_id,
4527
+ )
4528
+ .join(
4529
+ sys_schemas,
4530
+ onclause=sys_schemas.c.schema_id == sys_objects.c.schema_id,
4531
+ )
4532
+ .where(
4533
+ sys_indexes.c.is_primary_key == 0,
4534
+ sys_indexes.c.type != 0,
4535
+ ischema.sys_schemas.c.name == sql.bindparam("owner"),
4536
+ ischema.sys_objects.c.name.in_(
4537
+ sql.bindparam("filter_names", expanding=True)
4538
+ ),
4539
+ )
4540
+ .order_by(sys_objects.c.name, sys_indexes.c.name)
4541
+ )
4542
+ return s
4543
+
4544
+ @lru_cache()
4545
+ def _indexes_columns_select(self):
4546
+ """Build the Core sys.* select for index columns (one row per
4547
+ index column per index per table)."""
4548
+ sys_index_columns = ischema.sys_index_columns
4549
+ sys_columns = ischema.sys_columns
4550
+ sys_objects = ischema.sys_objects
4551
+ sys_schemas = ischema.sys_schemas
4552
+
4553
+ s = (
4554
+ sql.select(
4555
+ sys_objects.c.name.label("table_name"),
4556
+ sys_index_columns.c.index_id,
4557
+ sys_columns.c.name,
4558
+ sys_index_columns.c.is_included_column,
4559
+ )
4560
+ .select_from(sys_index_columns)
4561
+ .join(
4562
+ sys_columns,
4563
+ onclause=sql.and_(
4564
+ sys_columns.c.object_id == sys_index_columns.c.object_id,
4565
+ sys_columns.c.column_id == sys_index_columns.c.column_id,
4566
+ ),
4567
+ )
4568
+ .join(
4569
+ sys_objects,
4570
+ onclause=sys_objects.c.object_id
4571
+ == sys_index_columns.c.object_id,
4572
+ )
4573
+ .join(
4574
+ sys_schemas,
4575
+ onclause=sys_schemas.c.schema_id == sys_objects.c.schema_id,
4576
+ )
4577
+ .where(
4578
+ ischema.sys_schemas.c.name == sql.bindparam("owner"),
4579
+ ischema.sys_objects.c.name.in_(
4580
+ sql.bindparam("filter_names", expanding=True)
4581
+ ),
4582
+ )
4583
+ .order_by(
4584
+ sys_objects.c.name,
4585
+ sys_index_columns.c.index_id,
4586
+ sys_index_columns.c.key_ordinal,
4587
+ )
4588
+ )
4589
+ return s
4590
+
4591
+ def _fetch_multi_indexes(
4592
+ self, connection, owner, names, schema, name_map, result, exec_opts
4593
+ ):
4594
+ """Execute the unified indexes queries for one catalog pass.
4595
+
4596
+ Runs two Core sys.* selects (index metadata + index columns),
4597
+ grouped by table. Used for both the main-DB pass and the
4598
+ tempdb pass (with ``schema_translate_map={"sys": "tempdb.sys"}``
4599
+ applied).
4600
+ """
4601
+ meta_q = self._indexes_metadata_select()
4602
+
4603
+ rp = connection.execute(
4604
+ meta_q,
4605
+ {"owner": owner, "filter_names": names},
4606
+ execution_options=exec_opts,
4607
+ )
4608
+
4609
+ # {table_name: {index_id: index_dict}}
4610
+ indexes_by_table = {}
4611
+ for row in rp.mappings():
4612
+ tname = name_map(row["table_name"])
4613
+ if tname not in indexes_by_table:
4614
+ indexes_by_table[tname] = {}
4615
+
4616
+ current = {
4617
+ "name": row["name"],
4618
+ "unique": row["is_unique"] == 1,
4619
+ "column_names": [],
4620
+ "dialect_options": {"mssql_include": []},
4621
+ }
4622
+ do = current["dialect_options"]
4623
+ index_type = row["type"]
4624
+ if index_type in {1, 2}:
4625
+ do["mssql_clustered"] = index_type == 1
4626
+ if index_type in {5, 6}:
4627
+ do["mssql_clustered"] = index_type == 5
4628
+ do["mssql_columnstore"] = True
4629
+ if row["filter_definition"] is not None:
4630
+ do["mssql_where"] = row["filter_definition"]
4631
+
4632
+ indexes_by_table[tname][row["index_id"]] = current
4633
+
4634
+ cols_q = self._indexes_columns_select()
4635
+
4636
+ rp2 = connection.execute(
4637
+ cols_q,
4638
+ {"owner": owner, "filter_names": names},
4639
+ execution_options=exec_opts,
4640
+ )
4641
+
4642
+ for row in rp2.mappings():
4643
+ tname = name_map(row["table_name"])
4644
+ idx_id = row["index_id"]
4645
+ if tname not in indexes_by_table:
4646
+ continue
4647
+ if idx_id not in indexes_by_table[tname]:
4648
+ continue
4649
+ index_def = indexes_by_table[tname][idx_id]
4650
+ do = index_def["dialect_options"]
4651
+ is_colstore = do.get("mssql_columnstore")
4652
+ is_clustered = do.get("mssql_clustered")
4653
+ if not (is_colstore and is_clustered):
4654
+ # a clustered columnstore index includes all columns but does
4655
+ # not want them in the index definition
4656
+ if row["is_included_column"] and not is_colstore:
4657
+ # a noncludsted columnstore index reports that includes
4658
+ # columns but requires that are listed as normal columns
4659
+ do["mssql_include"].append(row["name"])
4660
+ else:
4661
+ index_def["column_names"].append(row["name"])
4662
+
4663
+ for tname, idx_dict in indexes_by_table.items():
4664
+ result[(schema, tname)] = list(idx_dict.values())
4665
+
4666
+ for n in names:
4667
+ key = (schema, name_map(n))
4668
+ if key not in result:
4669
+ result[key] = ReflectionDefaults.indexes()
4670
+
4671
+ def _fetch_multi_indexes_temp(
4672
+ self, connection, temp_names, schema, result
4673
+ ):
4674
+ """Run the unified indexes queries against tempdb."""
4675
+ resolved_by_owner, temp_name_map = self._resolve_temp_names(
4676
+ connection, temp_names
4677
+ )
4678
+
4679
+ for temp_owner, mangled_names in resolved_by_owner.items():
4680
+ self._fetch_multi_indexes(
4681
+ connection,
4682
+ owner=temp_owner,
4683
+ names=mangled_names,
4684
+ schema=schema,
4685
+ name_map=temp_name_map,
4686
+ result=result,
4687
+ exec_opts={"schema_translate_map": {"sys": "tempdb.sys"}},
4688
+ )
4689
+
4690
+ @_db_plus_owner_multi
4691
+ def get_multi_table_comment(
4692
+ self,
4693
+ connection,
4694
+ dbname,
4695
+ owner,
4696
+ schema,
4697
+ filter_names,
4698
+ scope,
4699
+ kind,
4700
+ **kw,
4701
+ ):
4702
+ if not self.supports_comments:
4703
+ raise NotImplementedError(
4704
+ "Can't get table comments on current SQL Server "
4705
+ "version in use"
4706
+ )
4707
+
4708
+ regular_names, multi_object_names, temp_names = (
4709
+ self._partition_filter_names(
4710
+ connection, owner, filter_names, scope, kind
4711
+ )
4712
+ )
4713
+
4714
+ result = {}
4715
+
4716
+ if multi_object_names:
4717
+ name_map = self._multi_name_map(regular_names)
4718
+ self._fetch_multi_table_comment(
4719
+ connection,
4720
+ owner=owner,
4721
+ names=multi_object_names,
4722
+ schema=schema,
4723
+ name_map=name_map,
4724
+ result=result,
4725
+ exec_opts={"schema_translate_map": {}},
4726
+ )
4727
+
4728
+ if temp_names:
4729
+ self._fetch_multi_table_comment_temp(
4730
+ connection,
4731
+ temp_names=temp_names,
4732
+ schema=schema,
4733
+ result=result,
4734
+ )
4735
+
4736
+ return result.items()
4737
+
4738
+ @lru_cache()
4739
+ def _table_comment_select(self):
4740
+ """Build the Core sys.* select for table comment reflection."""
4741
+ sys_objects = ischema.sys_objects
4742
+ sys_schemas = ischema.sys_schemas
4743
+ extended_properties = ischema.extended_properties
4744
+
4745
+ s = (
4746
+ sql.select(
4747
+ sys_objects.c.name.label("table_name"),
4748
+ extended_properties.c.value.label("comment"),
4749
+ )
4750
+ .select_from(sys_objects)
4751
+ .join(
4752
+ sys_schemas,
4753
+ onclause=sys_objects.c.schema_id == sys_schemas.c.schema_id,
4754
+ )
4755
+ .outerjoin(
4756
+ extended_properties,
4757
+ onclause=sql.and_(
4758
+ extended_properties.c["class"] == 1,
4759
+ extended_properties.c.name == "MS_Description",
4760
+ extended_properties.c.major_id == sys_objects.c.object_id,
4761
+ extended_properties.c.minor_id == 0,
4762
+ ),
4763
+ )
4764
+ .where(
4765
+ sys_objects.c.type.in_(("U", "V")),
4766
+ ischema.sys_schemas.c.name == sql.bindparam("owner"),
4767
+ ischema.sys_objects.c.name.in_(
4768
+ sql.bindparam("filter_names", expanding=True)
4769
+ ),
4770
+ )
4771
+ )
4772
+ return s
4773
+
4774
+ def _fetch_multi_table_comment(
4775
+ self, connection, owner, names, schema, name_map, result, exec_opts
4776
+ ):
4777
+ """Execute the unified table comment query for one catalog pass.
4778
+
4779
+ Core sys.* select so ``schema_translate_map`` actually rewrites
4780
+ the catalog reference for the tempdb pass.
4781
+ """
4782
+ q = self._table_comment_select()
4783
+
4784
+ rp = connection.execute(
4785
+ q,
4786
+ {"owner": owner, "filter_names": names},
4787
+ execution_options=exec_opts,
4788
+ )
4789
+
4790
+ for row in rp.mappings():
4791
+ table_name = name_map(row["table_name"])
4792
+ comment = row["comment"]
4793
+ result[(schema, table_name)] = (
4794
+ {"text": comment} if comment else {"text": None}
4795
+ )
4796
+
4797
+ for n in names:
4798
+ key = (schema, name_map(n))
4799
+ if key not in result:
4800
+ result[key] = ReflectionDefaults.table_comment()
4801
+
4802
+ def _fetch_multi_table_comment_temp(
4803
+ self, connection, temp_names, schema, result
4804
+ ):
4805
+ """Run the unified table comment query against tempdb."""
4806
+ resolved_by_owner, temp_name_map = self._resolve_temp_names(
4807
+ connection, temp_names
4808
+ )
4809
+ for temp_owner, mangled_names in resolved_by_owner.items():
4810
+ self._fetch_multi_table_comment(
4811
+ connection,
4812
+ owner=temp_owner,
4813
+ names=mangled_names,
4814
+ schema=schema,
4815
+ name_map=temp_name_map,
4816
+ result=result,
4817
+ exec_opts={"schema_translate_map": {"sys": "tempdb.sys"}},
4818
+ )
4819
+
4820
+ # override mixin methods for which this dialect has no native
4821
+ # get_multi_* implementation, preventing infinite recursion
4822
+ # through DefaultDialect._default_multi_reflect
4823
+
4824
+ def get_unique_constraints(
4825
+ self, connection, table_name, schema=None, **kw
4826
+ ):
4827
+ raise NotImplementedError()
4828
+
4829
+ def get_check_constraints(self, connection, table_name, schema=None, **kw):
4830
+ raise NotImplementedError()
4831
+
4832
+ def get_table_options(self, connection, table_name, schema=None, **kw):
4833
+ raise NotImplementedError()