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