SQLAlchemy 2.0.36__cp313-cp313-win32.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-win32.pyd +0 -0
  13. sqlalchemy/cyextension/collections.pyx +409 -0
  14. sqlalchemy/cyextension/immutabledict.cp313-win32.pyd +0 -0
  15. sqlalchemy/cyextension/immutabledict.pxd +8 -0
  16. sqlalchemy/cyextension/immutabledict.pyx +133 -0
  17. sqlalchemy/cyextension/processors.cp313-win32.pyd +0 -0
  18. sqlalchemy/cyextension/processors.pyx +68 -0
  19. sqlalchemy/cyextension/resultproxy.cp313-win32.pyd +0 -0
  20. sqlalchemy/cyextension/resultproxy.pyx +102 -0
  21. sqlalchemy/cyextension/util.cp313-win32.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,3494 @@
1
+ # dialects/mysql/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
+ r"""
11
+
12
+ .. dialect:: mysql
13
+ :name: MySQL / MariaDB
14
+ :normal_support: 5.6+ / 10+
15
+ :best_effort: 5.0.2+ / 5.0.2+
16
+
17
+ Supported Versions and Features
18
+ -------------------------------
19
+
20
+ SQLAlchemy supports MySQL starting with version 5.0.2 through modern releases,
21
+ as well as all modern versions of MariaDB. See the official MySQL
22
+ documentation for detailed information about features supported in any given
23
+ server release.
24
+
25
+ .. versionchanged:: 1.4 minimum MySQL version supported is now 5.0.2.
26
+
27
+ MariaDB Support
28
+ ~~~~~~~~~~~~~~~
29
+
30
+ The MariaDB variant of MySQL retains fundamental compatibility with MySQL's
31
+ protocols however the development of these two products continues to diverge.
32
+ Within the realm of SQLAlchemy, the two databases have a small number of
33
+ syntactical and behavioral differences that SQLAlchemy accommodates automatically.
34
+ To connect to a MariaDB database, no changes to the database URL are required::
35
+
36
+
37
+ engine = create_engine("mysql+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4")
38
+
39
+ Upon first connect, the SQLAlchemy dialect employs a
40
+ server version detection scheme that determines if the
41
+ backing database reports as MariaDB. Based on this flag, the dialect
42
+ can make different choices in those of areas where its behavior
43
+ must be different.
44
+
45
+ .. _mysql_mariadb_only_mode:
46
+
47
+ MariaDB-Only Mode
48
+ ~~~~~~~~~~~~~~~~~
49
+
50
+ The dialect also supports an **optional** "MariaDB-only" mode of connection, which may be
51
+ useful for the case where an application makes use of MariaDB-specific features
52
+ and is not compatible with a MySQL database. To use this mode of operation,
53
+ replace the "mysql" token in the above URL with "mariadb"::
54
+
55
+ engine = create_engine("mariadb+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4")
56
+
57
+ The above engine, upon first connect, will raise an error if the server version
58
+ detection detects that the backing database is not MariaDB.
59
+
60
+ When using an engine with ``"mariadb"`` as the dialect name, **all mysql-specific options
61
+ that include the name "mysql" in them are now named with "mariadb"**. This means
62
+ options like ``mysql_engine`` should be named ``mariadb_engine``, etc. Both
63
+ "mysql" and "mariadb" options can be used simultaneously for applications that
64
+ use URLs with both "mysql" and "mariadb" dialects::
65
+
66
+ my_table = Table(
67
+ "mytable",
68
+ metadata,
69
+ Column("id", Integer, primary_key=True),
70
+ Column("textdata", String(50)),
71
+ mariadb_engine="InnoDB",
72
+ mysql_engine="InnoDB",
73
+ )
74
+
75
+ Index(
76
+ "textdata_ix",
77
+ my_table.c.textdata,
78
+ mysql_prefix="FULLTEXT",
79
+ mariadb_prefix="FULLTEXT",
80
+ )
81
+
82
+ Similar behavior will occur when the above structures are reflected, i.e. the
83
+ "mariadb" prefix will be present in the option names when the database URL
84
+ is based on the "mariadb" name.
85
+
86
+ .. versionadded:: 1.4 Added "mariadb" dialect name supporting "MariaDB-only mode"
87
+ for the MySQL dialect.
88
+
89
+ .. _mysql_connection_timeouts:
90
+
91
+ Connection Timeouts and Disconnects
92
+ -----------------------------------
93
+
94
+ MySQL / MariaDB feature an automatic connection close behavior, for connections that
95
+ have been idle for a fixed period of time, defaulting to eight hours.
96
+ To circumvent having this issue, use
97
+ the :paramref:`_sa.create_engine.pool_recycle` option which ensures that
98
+ a connection will be discarded and replaced with a new one if it has been
99
+ present in the pool for a fixed number of seconds::
100
+
101
+ engine = create_engine('mysql+mysqldb://...', pool_recycle=3600)
102
+
103
+ For more comprehensive disconnect detection of pooled connections, including
104
+ accommodation of server restarts and network issues, a pre-ping approach may
105
+ be employed. See :ref:`pool_disconnects` for current approaches.
106
+
107
+ .. seealso::
108
+
109
+ :ref:`pool_disconnects` - Background on several techniques for dealing
110
+ with timed out connections as well as database restarts.
111
+
112
+ .. _mysql_storage_engines:
113
+
114
+ CREATE TABLE arguments including Storage Engines
115
+ ------------------------------------------------
116
+
117
+ Both MySQL's and MariaDB's CREATE TABLE syntax includes a wide array of special options,
118
+ including ``ENGINE``, ``CHARSET``, ``MAX_ROWS``, ``ROW_FORMAT``,
119
+ ``INSERT_METHOD``, and many more.
120
+ To accommodate the rendering of these arguments, specify the form
121
+ ``mysql_argument_name="value"``. For example, to specify a table with
122
+ ``ENGINE`` of ``InnoDB``, ``CHARSET`` of ``utf8mb4``, and ``KEY_BLOCK_SIZE``
123
+ of ``1024``::
124
+
125
+ Table('mytable', metadata,
126
+ Column('data', String(32)),
127
+ mysql_engine='InnoDB',
128
+ mysql_charset='utf8mb4',
129
+ mysql_key_block_size="1024"
130
+ )
131
+
132
+ When supporting :ref:`mysql_mariadb_only_mode` mode, similar keys against
133
+ the "mariadb" prefix must be included as well. The values can of course
134
+ vary independently so that different settings on MySQL vs. MariaDB may
135
+ be maintained::
136
+
137
+ # support both "mysql" and "mariadb-only" engine URLs
138
+
139
+ Table('mytable', metadata,
140
+ Column('data', String(32)),
141
+
142
+ mysql_engine='InnoDB',
143
+ mariadb_engine='InnoDB',
144
+
145
+ mysql_charset='utf8mb4',
146
+ mariadb_charset='utf8',
147
+
148
+ mysql_key_block_size="1024"
149
+ mariadb_key_block_size="1024"
150
+
151
+ )
152
+
153
+ The MySQL / MariaDB dialects will normally transfer any keyword specified as
154
+ ``mysql_keyword_name`` to be rendered as ``KEYWORD_NAME`` in the
155
+ ``CREATE TABLE`` statement. A handful of these names will render with a space
156
+ instead of an underscore; to support this, the MySQL dialect has awareness of
157
+ these particular names, which include ``DATA DIRECTORY``
158
+ (e.g. ``mysql_data_directory``), ``CHARACTER SET`` (e.g.
159
+ ``mysql_character_set``) and ``INDEX DIRECTORY`` (e.g.
160
+ ``mysql_index_directory``).
161
+
162
+ The most common argument is ``mysql_engine``, which refers to the storage
163
+ engine for the table. Historically, MySQL server installations would default
164
+ to ``MyISAM`` for this value, although newer versions may be defaulting
165
+ to ``InnoDB``. The ``InnoDB`` engine is typically preferred for its support
166
+ of transactions and foreign keys.
167
+
168
+ A :class:`_schema.Table`
169
+ that is created in a MySQL / MariaDB database with a storage engine
170
+ of ``MyISAM`` will be essentially non-transactional, meaning any
171
+ INSERT/UPDATE/DELETE statement referring to this table will be invoked as
172
+ autocommit. It also will have no support for foreign key constraints; while
173
+ the ``CREATE TABLE`` statement accepts foreign key options, when using the
174
+ ``MyISAM`` storage engine these arguments are discarded. Reflecting such a
175
+ table will also produce no foreign key constraint information.
176
+
177
+ For fully atomic transactions as well as support for foreign key
178
+ constraints, all participating ``CREATE TABLE`` statements must specify a
179
+ transactional engine, which in the vast majority of cases is ``InnoDB``.
180
+
181
+
182
+ Case Sensitivity and Table Reflection
183
+ -------------------------------------
184
+
185
+ Both MySQL and MariaDB have inconsistent support for case-sensitive identifier
186
+ names, basing support on specific details of the underlying
187
+ operating system. However, it has been observed that no matter
188
+ what case sensitivity behavior is present, the names of tables in
189
+ foreign key declarations are *always* received from the database
190
+ as all-lower case, making it impossible to accurately reflect a
191
+ schema where inter-related tables use mixed-case identifier names.
192
+
193
+ Therefore it is strongly advised that table names be declared as
194
+ all lower case both within SQLAlchemy as well as on the MySQL / MariaDB
195
+ database itself, especially if database reflection features are
196
+ to be used.
197
+
198
+ .. _mysql_isolation_level:
199
+
200
+ Transaction Isolation Level
201
+ ---------------------------
202
+
203
+ All MySQL / MariaDB dialects support setting of transaction isolation level both via a
204
+ dialect-specific parameter :paramref:`_sa.create_engine.isolation_level`
205
+ accepted
206
+ by :func:`_sa.create_engine`, as well as the
207
+ :paramref:`.Connection.execution_options.isolation_level` argument as passed to
208
+ :meth:`_engine.Connection.execution_options`.
209
+ This feature works by issuing the
210
+ command ``SET SESSION TRANSACTION ISOLATION LEVEL <level>`` for each new
211
+ connection. For the special AUTOCOMMIT isolation level, DBAPI-specific
212
+ techniques are used.
213
+
214
+ To set isolation level using :func:`_sa.create_engine`::
215
+
216
+ engine = create_engine(
217
+ "mysql+mysqldb://scott:tiger@localhost/test",
218
+ isolation_level="READ UNCOMMITTED"
219
+ )
220
+
221
+ To set using per-connection execution options::
222
+
223
+ connection = engine.connect()
224
+ connection = connection.execution_options(
225
+ isolation_level="READ COMMITTED"
226
+ )
227
+
228
+ Valid values for ``isolation_level`` include:
229
+
230
+ * ``READ COMMITTED``
231
+ * ``READ UNCOMMITTED``
232
+ * ``REPEATABLE READ``
233
+ * ``SERIALIZABLE``
234
+ * ``AUTOCOMMIT``
235
+
236
+ The special ``AUTOCOMMIT`` value makes use of the various "autocommit"
237
+ attributes provided by specific DBAPIs, and is currently supported by
238
+ MySQLdb, MySQL-Client, MySQL-Connector Python, and PyMySQL. Using it,
239
+ the database connection will return true for the value of
240
+ ``SELECT @@autocommit;``.
241
+
242
+ There are also more options for isolation level configurations, such as
243
+ "sub-engine" objects linked to a main :class:`_engine.Engine` which each apply
244
+ different isolation level settings. See the discussion at
245
+ :ref:`dbapi_autocommit` for background.
246
+
247
+ .. seealso::
248
+
249
+ :ref:`dbapi_autocommit`
250
+
251
+ AUTO_INCREMENT Behavior
252
+ -----------------------
253
+
254
+ When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT`` on
255
+ the first :class:`.Integer` primary key column which is not marked as a
256
+ foreign key::
257
+
258
+ >>> t = Table('mytable', metadata,
259
+ ... Column('mytable_id', Integer, primary_key=True)
260
+ ... )
261
+ >>> t.create()
262
+ CREATE TABLE mytable (
263
+ id INTEGER NOT NULL AUTO_INCREMENT,
264
+ PRIMARY KEY (id)
265
+ )
266
+
267
+ You can disable this behavior by passing ``False`` to the
268
+ :paramref:`_schema.Column.autoincrement` argument of :class:`_schema.Column`.
269
+ This flag
270
+ can also be used to enable auto-increment on a secondary column in a
271
+ multi-column key for some storage engines::
272
+
273
+ Table('mytable', metadata,
274
+ Column('gid', Integer, primary_key=True, autoincrement=False),
275
+ Column('id', Integer, primary_key=True)
276
+ )
277
+
278
+ .. _mysql_ss_cursors:
279
+
280
+ Server Side Cursors
281
+ -------------------
282
+
283
+ Server-side cursor support is available for the mysqlclient, PyMySQL,
284
+ mariadbconnector dialects and may also be available in others. This makes use
285
+ of either the "buffered=True/False" flag if available or by using a class such
286
+ as ``MySQLdb.cursors.SSCursor`` or ``pymysql.cursors.SSCursor`` internally.
287
+
288
+
289
+ Server side cursors are enabled on a per-statement basis by using the
290
+ :paramref:`.Connection.execution_options.stream_results` connection execution
291
+ option::
292
+
293
+ with engine.connect() as conn:
294
+ result = conn.execution_options(stream_results=True).execute(text("select * from table"))
295
+
296
+ Note that some kinds of SQL statements may not be supported with
297
+ server side cursors; generally, only SQL statements that return rows should be
298
+ used with this option.
299
+
300
+ .. deprecated:: 1.4 The dialect-level server_side_cursors flag is deprecated
301
+ and will be removed in a future release. Please use the
302
+ :paramref:`_engine.Connection.stream_results` execution option for
303
+ unbuffered cursor support.
304
+
305
+ .. seealso::
306
+
307
+ :ref:`engine_stream_results`
308
+
309
+ .. _mysql_unicode:
310
+
311
+ Unicode
312
+ -------
313
+
314
+ Charset Selection
315
+ ~~~~~~~~~~~~~~~~~
316
+
317
+ Most MySQL / MariaDB DBAPIs offer the option to set the client character set for
318
+ a connection. This is typically delivered using the ``charset`` parameter
319
+ in the URL, such as::
320
+
321
+ e = create_engine(
322
+ "mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4")
323
+
324
+ This charset is the **client character set** for the connection. Some
325
+ MySQL DBAPIs will default this to a value such as ``latin1``, and some
326
+ will make use of the ``default-character-set`` setting in the ``my.cnf``
327
+ file as well. Documentation for the DBAPI in use should be consulted
328
+ for specific behavior.
329
+
330
+ The encoding used for Unicode has traditionally been ``'utf8'``. However, for
331
+ MySQL versions 5.5.3 and MariaDB 5.5 on forward, a new MySQL-specific encoding
332
+ ``'utf8mb4'`` has been introduced, and as of MySQL 8.0 a warning is emitted by
333
+ the server if plain ``utf8`` is specified within any server-side directives,
334
+ replaced with ``utf8mb3``. The rationale for this new encoding is due to the
335
+ fact that MySQL's legacy utf-8 encoding only supports codepoints up to three
336
+ bytes instead of four. Therefore, when communicating with a MySQL or MariaDB
337
+ database that includes codepoints more than three bytes in size, this new
338
+ charset is preferred, if supported by both the database as well as the client
339
+ DBAPI, as in::
340
+
341
+ e = create_engine(
342
+ "mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4")
343
+
344
+ All modern DBAPIs should support the ``utf8mb4`` charset.
345
+
346
+ In order to use ``utf8mb4`` encoding for a schema that was created with legacy
347
+ ``utf8``, changes to the MySQL/MariaDB schema and/or server configuration may be
348
+ required.
349
+
350
+ .. seealso::
351
+
352
+ `The utf8mb4 Character Set \
353
+ <https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html>`_ - \
354
+ in the MySQL documentation
355
+
356
+ .. _mysql_binary_introducer:
357
+
358
+ Dealing with Binary Data Warnings and Unicode
359
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
360
+
361
+ MySQL versions 5.6, 5.7 and later (not MariaDB at the time of this writing) now
362
+ emit a warning when attempting to pass binary data to the database, while a
363
+ character set encoding is also in place, when the binary data itself is not
364
+ valid for that encoding::
365
+
366
+ default.py:509: Warning: (1300, "Invalid utf8mb4 character string:
367
+ 'F9876A'")
368
+ cursor.execute(statement, parameters)
369
+
370
+ This warning is due to the fact that the MySQL client library is attempting to
371
+ interpret the binary string as a unicode object even if a datatype such
372
+ as :class:`.LargeBinary` is in use. To resolve this, the SQL statement requires
373
+ a binary "character set introducer" be present before any non-NULL value
374
+ that renders like this::
375
+
376
+ INSERT INTO table (data) VALUES (_binary %s)
377
+
378
+ These character set introducers are provided by the DBAPI driver, assuming the
379
+ use of mysqlclient or PyMySQL (both of which are recommended). Add the query
380
+ string parameter ``binary_prefix=true`` to the URL to repair this warning::
381
+
382
+ # mysqlclient
383
+ engine = create_engine(
384
+ "mysql+mysqldb://scott:tiger@localhost/test?charset=utf8mb4&binary_prefix=true")
385
+
386
+ # PyMySQL
387
+ engine = create_engine(
388
+ "mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4&binary_prefix=true")
389
+
390
+
391
+ The ``binary_prefix`` flag may or may not be supported by other MySQL drivers.
392
+
393
+ SQLAlchemy itself cannot render this ``_binary`` prefix reliably, as it does
394
+ not work with the NULL value, which is valid to be sent as a bound parameter.
395
+ As the MySQL driver renders parameters directly into the SQL string, it's the
396
+ most efficient place for this additional keyword to be passed.
397
+
398
+ .. seealso::
399
+
400
+ `Character set introducers <https://dev.mysql.com/doc/refman/5.7/en/charset-introducer.html>`_ - on the MySQL website
401
+
402
+
403
+ ANSI Quoting Style
404
+ ------------------
405
+
406
+ MySQL / MariaDB feature two varieties of identifier "quoting style", one using
407
+ backticks and the other using quotes, e.g. ```some_identifier``` vs.
408
+ ``"some_identifier"``. All MySQL dialects detect which version
409
+ is in use by checking the value of :ref:`sql_mode<mysql_sql_mode>` when a connection is first
410
+ established with a particular :class:`_engine.Engine`.
411
+ This quoting style comes
412
+ into play when rendering table and column names as well as when reflecting
413
+ existing database structures. The detection is entirely automatic and
414
+ no special configuration is needed to use either quoting style.
415
+
416
+
417
+ .. _mysql_sql_mode:
418
+
419
+ Changing the sql_mode
420
+ ---------------------
421
+
422
+ MySQL supports operating in multiple
423
+ `Server SQL Modes <https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html>`_ for
424
+ both Servers and Clients. To change the ``sql_mode`` for a given application, a
425
+ developer can leverage SQLAlchemy's Events system.
426
+
427
+ In the following example, the event system is used to set the ``sql_mode`` on
428
+ the ``first_connect`` and ``connect`` events::
429
+
430
+ from sqlalchemy import create_engine, event
431
+
432
+ eng = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo='debug')
433
+
434
+ # `insert=True` will ensure this is the very first listener to run
435
+ @event.listens_for(eng, "connect", insert=True)
436
+ def connect(dbapi_connection, connection_record):
437
+ cursor = dbapi_connection.cursor()
438
+ cursor.execute("SET sql_mode = 'STRICT_ALL_TABLES'")
439
+
440
+ conn = eng.connect()
441
+
442
+ In the example illustrated above, the "connect" event will invoke the "SET"
443
+ statement on the connection at the moment a particular DBAPI connection is
444
+ first created for a given Pool, before the connection is made available to the
445
+ connection pool. Additionally, because the function was registered with
446
+ ``insert=True``, it will be prepended to the internal list of registered
447
+ functions.
448
+
449
+
450
+ MySQL / MariaDB SQL Extensions
451
+ ------------------------------
452
+
453
+ Many of the MySQL / MariaDB SQL extensions are handled through SQLAlchemy's generic
454
+ function and operator support::
455
+
456
+ table.select(table.c.password==func.md5('plaintext'))
457
+ table.select(table.c.username.op('regexp')('^[a-d]'))
458
+
459
+ And of course any valid SQL statement can be executed as a string as well.
460
+
461
+ Some limited direct support for MySQL / MariaDB extensions to SQL is currently
462
+ available.
463
+
464
+ * INSERT..ON DUPLICATE KEY UPDATE: See
465
+ :ref:`mysql_insert_on_duplicate_key_update`
466
+
467
+ * SELECT pragma, use :meth:`_expression.Select.prefix_with` and
468
+ :meth:`_query.Query.prefix_with`::
469
+
470
+ select(...).prefix_with(['HIGH_PRIORITY', 'SQL_SMALL_RESULT'])
471
+
472
+ * UPDATE with LIMIT::
473
+
474
+ update(..., mysql_limit=10, mariadb_limit=10)
475
+
476
+ * optimizer hints, use :meth:`_expression.Select.prefix_with` and
477
+ :meth:`_query.Query.prefix_with`::
478
+
479
+ select(...).prefix_with("/*+ NO_RANGE_OPTIMIZATION(t4 PRIMARY) */")
480
+
481
+ * index hints, use :meth:`_expression.Select.with_hint` and
482
+ :meth:`_query.Query.with_hint`::
483
+
484
+ select(...).with_hint(some_table, "USE INDEX xyz")
485
+
486
+ * MATCH operator support::
487
+
488
+ from sqlalchemy.dialects.mysql import match
489
+ select(...).where(match(col1, col2, against="some expr").in_boolean_mode())
490
+
491
+ .. seealso::
492
+
493
+ :class:`_mysql.match`
494
+
495
+ INSERT/DELETE...RETURNING
496
+ -------------------------
497
+
498
+ The MariaDB dialect supports 10.5+'s ``INSERT..RETURNING`` and
499
+ ``DELETE..RETURNING`` (10.0+) syntaxes. ``INSERT..RETURNING`` may be used
500
+ automatically in some cases in order to fetch newly generated identifiers in
501
+ place of the traditional approach of using ``cursor.lastrowid``, however
502
+ ``cursor.lastrowid`` is currently still preferred for simple single-statement
503
+ cases for its better performance.
504
+
505
+ To specify an explicit ``RETURNING`` clause, use the
506
+ :meth:`._UpdateBase.returning` method on a per-statement basis::
507
+
508
+ # INSERT..RETURNING
509
+ result = connection.execute(
510
+ table.insert().
511
+ values(name='foo').
512
+ returning(table.c.col1, table.c.col2)
513
+ )
514
+ print(result.all())
515
+
516
+ # DELETE..RETURNING
517
+ result = connection.execute(
518
+ table.delete().
519
+ where(table.c.name=='foo').
520
+ returning(table.c.col1, table.c.col2)
521
+ )
522
+ print(result.all())
523
+
524
+ .. versionadded:: 2.0 Added support for MariaDB RETURNING
525
+
526
+ .. _mysql_insert_on_duplicate_key_update:
527
+
528
+ INSERT...ON DUPLICATE KEY UPDATE (Upsert)
529
+ ------------------------------------------
530
+
531
+ MySQL / MariaDB allow "upserts" (update or insert)
532
+ of rows into a table via the ``ON DUPLICATE KEY UPDATE`` clause of the
533
+ ``INSERT`` statement. A candidate row will only be inserted if that row does
534
+ not match an existing primary or unique key in the table; otherwise, an UPDATE
535
+ will be performed. The statement allows for separate specification of the
536
+ values to INSERT versus the values for UPDATE.
537
+
538
+ SQLAlchemy provides ``ON DUPLICATE KEY UPDATE`` support via the MySQL-specific
539
+ :func:`.mysql.insert()` function, which provides
540
+ the generative method :meth:`~.mysql.Insert.on_duplicate_key_update`:
541
+
542
+ .. sourcecode:: pycon+sql
543
+
544
+ >>> from sqlalchemy.dialects.mysql import insert
545
+
546
+ >>> insert_stmt = insert(my_table).values(
547
+ ... id='some_existing_id',
548
+ ... data='inserted value')
549
+
550
+ >>> on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
551
+ ... data=insert_stmt.inserted.data,
552
+ ... status='U'
553
+ ... )
554
+ >>> print(on_duplicate_key_stmt)
555
+ {printsql}INSERT INTO my_table (id, data) VALUES (%s, %s)
556
+ ON DUPLICATE KEY UPDATE data = VALUES(data), status = %s
557
+
558
+
559
+ Unlike PostgreSQL's "ON CONFLICT" phrase, the "ON DUPLICATE KEY UPDATE"
560
+ phrase will always match on any primary key or unique key, and will always
561
+ perform an UPDATE if there's a match; there are no options for it to raise
562
+ an error or to skip performing an UPDATE.
563
+
564
+ ``ON DUPLICATE KEY UPDATE`` is used to perform an update of the already
565
+ existing row, using any combination of new values as well as values
566
+ from the proposed insertion. These values are normally specified using
567
+ keyword arguments passed to the
568
+ :meth:`_mysql.Insert.on_duplicate_key_update`
569
+ given column key values (usually the name of the column, unless it
570
+ specifies :paramref:`_schema.Column.key`
571
+ ) as keys and literal or SQL expressions
572
+ as values:
573
+
574
+ .. sourcecode:: pycon+sql
575
+
576
+ >>> insert_stmt = insert(my_table).values(
577
+ ... id='some_existing_id',
578
+ ... data='inserted value')
579
+
580
+ >>> on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
581
+ ... data="some data",
582
+ ... updated_at=func.current_timestamp(),
583
+ ... )
584
+
585
+ >>> print(on_duplicate_key_stmt)
586
+ {printsql}INSERT INTO my_table (id, data) VALUES (%s, %s)
587
+ ON DUPLICATE KEY UPDATE data = %s, updated_at = CURRENT_TIMESTAMP
588
+
589
+ In a manner similar to that of :meth:`.UpdateBase.values`, other parameter
590
+ forms are accepted, including a single dictionary:
591
+
592
+ .. sourcecode:: pycon+sql
593
+
594
+ >>> on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
595
+ ... {"data": "some data", "updated_at": func.current_timestamp()},
596
+ ... )
597
+
598
+ as well as a list of 2-tuples, which will automatically provide
599
+ a parameter-ordered UPDATE statement in a manner similar to that described
600
+ at :ref:`tutorial_parameter_ordered_updates`. Unlike the :class:`_expression.Update`
601
+ object,
602
+ no special flag is needed to specify the intent since the argument form is
603
+ this context is unambiguous:
604
+
605
+ .. sourcecode:: pycon+sql
606
+
607
+ >>> on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
608
+ ... [
609
+ ... ("data", "some data"),
610
+ ... ("updated_at", func.current_timestamp()),
611
+ ... ]
612
+ ... )
613
+
614
+ >>> print(on_duplicate_key_stmt)
615
+ {printsql}INSERT INTO my_table (id, data) VALUES (%s, %s)
616
+ ON DUPLICATE KEY UPDATE data = %s, updated_at = CURRENT_TIMESTAMP
617
+
618
+ .. versionchanged:: 1.3 support for parameter-ordered UPDATE clause within
619
+ MySQL ON DUPLICATE KEY UPDATE
620
+
621
+ .. warning::
622
+
623
+ The :meth:`_mysql.Insert.on_duplicate_key_update`
624
+ method does **not** take into
625
+ account Python-side default UPDATE values or generation functions, e.g.
626
+ e.g. those specified using :paramref:`_schema.Column.onupdate`.
627
+ These values will not be exercised for an ON DUPLICATE KEY style of UPDATE,
628
+ unless they are manually specified explicitly in the parameters.
629
+
630
+
631
+
632
+ In order to refer to the proposed insertion row, the special alias
633
+ :attr:`_mysql.Insert.inserted` is available as an attribute on
634
+ the :class:`_mysql.Insert` object; this object is a
635
+ :class:`_expression.ColumnCollection` which contains all columns of the target
636
+ table:
637
+
638
+ .. sourcecode:: pycon+sql
639
+
640
+ >>> stmt = insert(my_table).values(
641
+ ... id='some_id',
642
+ ... data='inserted value',
643
+ ... author='jlh')
644
+
645
+ >>> do_update_stmt = stmt.on_duplicate_key_update(
646
+ ... data="updated value",
647
+ ... author=stmt.inserted.author
648
+ ... )
649
+
650
+ >>> print(do_update_stmt)
651
+ {printsql}INSERT INTO my_table (id, data, author) VALUES (%s, %s, %s)
652
+ ON DUPLICATE KEY UPDATE data = %s, author = VALUES(author)
653
+
654
+ When rendered, the "inserted" namespace will produce the expression
655
+ ``VALUES(<columnname>)``.
656
+
657
+ .. versionadded:: 1.2 Added support for MySQL ON DUPLICATE KEY UPDATE clause
658
+
659
+
660
+
661
+ rowcount Support
662
+ ----------------
663
+
664
+ SQLAlchemy standardizes the DBAPI ``cursor.rowcount`` attribute to be the
665
+ usual definition of "number of rows matched by an UPDATE or DELETE" statement.
666
+ This is in contradiction to the default setting on most MySQL DBAPI drivers,
667
+ which is "number of rows actually modified/deleted". For this reason, the
668
+ SQLAlchemy MySQL dialects always add the ``constants.CLIENT.FOUND_ROWS``
669
+ flag, or whatever is equivalent for the target dialect, upon connection.
670
+ This setting is currently hardcoded.
671
+
672
+ .. seealso::
673
+
674
+ :attr:`_engine.CursorResult.rowcount`
675
+
676
+
677
+ .. _mysql_indexes:
678
+
679
+ MySQL / MariaDB- Specific Index Options
680
+ -----------------------------------------
681
+
682
+ MySQL and MariaDB-specific extensions to the :class:`.Index` construct are available.
683
+
684
+ Index Length
685
+ ~~~~~~~~~~~~~
686
+
687
+ MySQL and MariaDB both provide an option to create index entries with a certain length, where
688
+ "length" refers to the number of characters or bytes in each value which will
689
+ become part of the index. SQLAlchemy provides this feature via the
690
+ ``mysql_length`` and/or ``mariadb_length`` parameters::
691
+
692
+ Index('my_index', my_table.c.data, mysql_length=10, mariadb_length=10)
693
+
694
+ Index('a_b_idx', my_table.c.a, my_table.c.b, mysql_length={'a': 4,
695
+ 'b': 9})
696
+
697
+ Index('a_b_idx', my_table.c.a, my_table.c.b, mariadb_length={'a': 4,
698
+ 'b': 9})
699
+
700
+ Prefix lengths are given in characters for nonbinary string types and in bytes
701
+ for binary string types. The value passed to the keyword argument *must* be
702
+ either an integer (and, thus, specify the same prefix length value for all
703
+ columns of the index) or a dict in which keys are column names and values are
704
+ prefix length values for corresponding columns. MySQL and MariaDB only allow a
705
+ length for a column of an index if it is for a CHAR, VARCHAR, TEXT, BINARY,
706
+ VARBINARY and BLOB.
707
+
708
+ Index Prefixes
709
+ ~~~~~~~~~~~~~~
710
+
711
+ MySQL storage engines permit you to specify an index prefix when creating
712
+ an index. SQLAlchemy provides this feature via the
713
+ ``mysql_prefix`` parameter on :class:`.Index`::
714
+
715
+ Index('my_index', my_table.c.data, mysql_prefix='FULLTEXT')
716
+
717
+ The value passed to the keyword argument will be simply passed through to the
718
+ underlying CREATE INDEX, so it *must* be a valid index prefix for your MySQL
719
+ storage engine.
720
+
721
+ .. seealso::
722
+
723
+ `CREATE INDEX <https://dev.mysql.com/doc/refman/5.0/en/create-index.html>`_ - MySQL documentation
724
+
725
+ Index Types
726
+ ~~~~~~~~~~~~~
727
+
728
+ Some MySQL storage engines permit you to specify an index type when creating
729
+ an index or primary key constraint. SQLAlchemy provides this feature via the
730
+ ``mysql_using`` parameter on :class:`.Index`::
731
+
732
+ Index('my_index', my_table.c.data, mysql_using='hash', mariadb_using='hash')
733
+
734
+ As well as the ``mysql_using`` parameter on :class:`.PrimaryKeyConstraint`::
735
+
736
+ PrimaryKeyConstraint("data", mysql_using='hash', mariadb_using='hash')
737
+
738
+ The value passed to the keyword argument will be simply passed through to the
739
+ underlying CREATE INDEX or PRIMARY KEY clause, so it *must* be a valid index
740
+ type for your MySQL storage engine.
741
+
742
+ More information can be found at:
743
+
744
+ https://dev.mysql.com/doc/refman/5.0/en/create-index.html
745
+
746
+ https://dev.mysql.com/doc/refman/5.0/en/create-table.html
747
+
748
+ Index Parsers
749
+ ~~~~~~~~~~~~~
750
+
751
+ CREATE FULLTEXT INDEX in MySQL also supports a "WITH PARSER" option. This
752
+ is available using the keyword argument ``mysql_with_parser``::
753
+
754
+ Index(
755
+ 'my_index', my_table.c.data,
756
+ mysql_prefix='FULLTEXT', mysql_with_parser="ngram",
757
+ mariadb_prefix='FULLTEXT', mariadb_with_parser="ngram",
758
+ )
759
+
760
+ .. versionadded:: 1.3
761
+
762
+
763
+ .. _mysql_foreign_keys:
764
+
765
+ MySQL / MariaDB Foreign Keys
766
+ -----------------------------
767
+
768
+ MySQL and MariaDB's behavior regarding foreign keys has some important caveats.
769
+
770
+ Foreign Key Arguments to Avoid
771
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
772
+
773
+ Neither MySQL nor MariaDB support the foreign key arguments "DEFERRABLE", "INITIALLY",
774
+ or "MATCH". Using the ``deferrable`` or ``initially`` keyword argument with
775
+ :class:`_schema.ForeignKeyConstraint` or :class:`_schema.ForeignKey`
776
+ will have the effect of
777
+ these keywords being rendered in a DDL expression, which will then raise an
778
+ error on MySQL or MariaDB. In order to use these keywords on a foreign key while having
779
+ them ignored on a MySQL / MariaDB backend, use a custom compile rule::
780
+
781
+ from sqlalchemy.ext.compiler import compiles
782
+ from sqlalchemy.schema import ForeignKeyConstraint
783
+
784
+ @compiles(ForeignKeyConstraint, "mysql", "mariadb")
785
+ def process(element, compiler, **kw):
786
+ element.deferrable = element.initially = None
787
+ return compiler.visit_foreign_key_constraint(element, **kw)
788
+
789
+ The "MATCH" keyword is in fact more insidious, and is explicitly disallowed
790
+ by SQLAlchemy in conjunction with the MySQL or MariaDB backends. This argument is
791
+ silently ignored by MySQL / MariaDB, but in addition has the effect of ON UPDATE and ON
792
+ DELETE options also being ignored by the backend. Therefore MATCH should
793
+ never be used with the MySQL / MariaDB backends; as is the case with DEFERRABLE and
794
+ INITIALLY, custom compilation rules can be used to correct a
795
+ ForeignKeyConstraint at DDL definition time.
796
+
797
+ Reflection of Foreign Key Constraints
798
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
799
+
800
+ Not all MySQL / MariaDB storage engines support foreign keys. When using the
801
+ very common ``MyISAM`` MySQL storage engine, the information loaded by table
802
+ reflection will not include foreign keys. For these tables, you may supply a
803
+ :class:`~sqlalchemy.ForeignKeyConstraint` at reflection time::
804
+
805
+ Table('mytable', metadata,
806
+ ForeignKeyConstraint(['other_id'], ['othertable.other_id']),
807
+ autoload_with=engine
808
+ )
809
+
810
+ .. seealso::
811
+
812
+ :ref:`mysql_storage_engines`
813
+
814
+ .. _mysql_unique_constraints:
815
+
816
+ MySQL / MariaDB Unique Constraints and Reflection
817
+ ----------------------------------------------------
818
+
819
+ SQLAlchemy supports both the :class:`.Index` construct with the
820
+ flag ``unique=True``, indicating a UNIQUE index, as well as the
821
+ :class:`.UniqueConstraint` construct, representing a UNIQUE constraint.
822
+ Both objects/syntaxes are supported by MySQL / MariaDB when emitting DDL to create
823
+ these constraints. However, MySQL / MariaDB does not have a unique constraint
824
+ construct that is separate from a unique index; that is, the "UNIQUE"
825
+ constraint on MySQL / MariaDB is equivalent to creating a "UNIQUE INDEX".
826
+
827
+ When reflecting these constructs, the
828
+ :meth:`_reflection.Inspector.get_indexes`
829
+ and the :meth:`_reflection.Inspector.get_unique_constraints`
830
+ methods will **both**
831
+ return an entry for a UNIQUE index in MySQL / MariaDB. However, when performing
832
+ full table reflection using ``Table(..., autoload_with=engine)``,
833
+ the :class:`.UniqueConstraint` construct is
834
+ **not** part of the fully reflected :class:`_schema.Table` construct under any
835
+ circumstances; this construct is always represented by a :class:`.Index`
836
+ with the ``unique=True`` setting present in the :attr:`_schema.Table.indexes`
837
+ collection.
838
+
839
+
840
+ TIMESTAMP / DATETIME issues
841
+ ---------------------------
842
+
843
+ .. _mysql_timestamp_onupdate:
844
+
845
+ Rendering ON UPDATE CURRENT TIMESTAMP for MySQL / MariaDB's explicit_defaults_for_timestamp
846
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
847
+
848
+ MySQL / MariaDB have historically expanded the DDL for the :class:`_types.TIMESTAMP`
849
+ datatype into the phrase "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE
850
+ CURRENT_TIMESTAMP", which includes non-standard SQL that automatically updates
851
+ the column with the current timestamp when an UPDATE occurs, eliminating the
852
+ usual need to use a trigger in such a case where server-side update changes are
853
+ desired.
854
+
855
+ MySQL 5.6 introduced a new flag `explicit_defaults_for_timestamp
856
+ <https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html
857
+ #sysvar_explicit_defaults_for_timestamp>`_ which disables the above behavior,
858
+ and in MySQL 8 this flag defaults to true, meaning in order to get a MySQL
859
+ "on update timestamp" without changing this flag, the above DDL must be
860
+ rendered explicitly. Additionally, the same DDL is valid for use of the
861
+ ``DATETIME`` datatype as well.
862
+
863
+ SQLAlchemy's MySQL dialect does not yet have an option to generate
864
+ MySQL's "ON UPDATE CURRENT_TIMESTAMP" clause, noting that this is not a general
865
+ purpose "ON UPDATE" as there is no such syntax in standard SQL. SQLAlchemy's
866
+ :paramref:`_schema.Column.server_onupdate` parameter is currently not related
867
+ to this special MySQL behavior.
868
+
869
+ To generate this DDL, make use of the :paramref:`_schema.Column.server_default`
870
+ parameter and pass a textual clause that also includes the ON UPDATE clause::
871
+
872
+ from sqlalchemy import Table, MetaData, Column, Integer, String, TIMESTAMP
873
+ from sqlalchemy import text
874
+
875
+ metadata = MetaData()
876
+
877
+ mytable = Table(
878
+ "mytable",
879
+ metadata,
880
+ Column('id', Integer, primary_key=True),
881
+ Column('data', String(50)),
882
+ Column(
883
+ 'last_updated',
884
+ TIMESTAMP,
885
+ server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
886
+ )
887
+ )
888
+
889
+ The same instructions apply to use of the :class:`_types.DateTime` and
890
+ :class:`_types.DATETIME` datatypes::
891
+
892
+ from sqlalchemy import DateTime
893
+
894
+ mytable = Table(
895
+ "mytable",
896
+ metadata,
897
+ Column('id', Integer, primary_key=True),
898
+ Column('data', String(50)),
899
+ Column(
900
+ 'last_updated',
901
+ DateTime,
902
+ server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
903
+ )
904
+ )
905
+
906
+
907
+ Even though the :paramref:`_schema.Column.server_onupdate` feature does not
908
+ generate this DDL, it still may be desirable to signal to the ORM that this
909
+ updated value should be fetched. This syntax looks like the following::
910
+
911
+ from sqlalchemy.schema import FetchedValue
912
+
913
+ class MyClass(Base):
914
+ __tablename__ = 'mytable'
915
+
916
+ id = Column(Integer, primary_key=True)
917
+ data = Column(String(50))
918
+ last_updated = Column(
919
+ TIMESTAMP,
920
+ server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
921
+ server_onupdate=FetchedValue()
922
+ )
923
+
924
+
925
+ .. _mysql_timestamp_null:
926
+
927
+ TIMESTAMP Columns and NULL
928
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
929
+
930
+ MySQL historically enforces that a column which specifies the
931
+ TIMESTAMP datatype implicitly includes a default value of
932
+ CURRENT_TIMESTAMP, even though this is not stated, and additionally
933
+ sets the column as NOT NULL, the opposite behavior vs. that of all
934
+ other datatypes::
935
+
936
+ mysql> CREATE TABLE ts_test (
937
+ -> a INTEGER,
938
+ -> b INTEGER NOT NULL,
939
+ -> c TIMESTAMP,
940
+ -> d TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
941
+ -> e TIMESTAMP NULL);
942
+ Query OK, 0 rows affected (0.03 sec)
943
+
944
+ mysql> SHOW CREATE TABLE ts_test;
945
+ +---------+-----------------------------------------------------
946
+ | Table | Create Table
947
+ +---------+-----------------------------------------------------
948
+ | ts_test | CREATE TABLE `ts_test` (
949
+ `a` int(11) DEFAULT NULL,
950
+ `b` int(11) NOT NULL,
951
+ `c` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
952
+ `d` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
953
+ `e` timestamp NULL DEFAULT NULL
954
+ ) ENGINE=MyISAM DEFAULT CHARSET=latin1
955
+
956
+ Above, we see that an INTEGER column defaults to NULL, unless it is specified
957
+ with NOT NULL. But when the column is of type TIMESTAMP, an implicit
958
+ default of CURRENT_TIMESTAMP is generated which also coerces the column
959
+ to be a NOT NULL, even though we did not specify it as such.
960
+
961
+ This behavior of MySQL can be changed on the MySQL side using the
962
+ `explicit_defaults_for_timestamp
963
+ <https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html
964
+ #sysvar_explicit_defaults_for_timestamp>`_ configuration flag introduced in
965
+ MySQL 5.6. With this server setting enabled, TIMESTAMP columns behave like
966
+ any other datatype on the MySQL side with regards to defaults and nullability.
967
+
968
+ However, to accommodate the vast majority of MySQL databases that do not
969
+ specify this new flag, SQLAlchemy emits the "NULL" specifier explicitly with
970
+ any TIMESTAMP column that does not specify ``nullable=False``. In order to
971
+ accommodate newer databases that specify ``explicit_defaults_for_timestamp``,
972
+ SQLAlchemy also emits NOT NULL for TIMESTAMP columns that do specify
973
+ ``nullable=False``. The following example illustrates::
974
+
975
+ from sqlalchemy import MetaData, Integer, Table, Column, text
976
+ from sqlalchemy.dialects.mysql import TIMESTAMP
977
+
978
+ m = MetaData()
979
+ t = Table('ts_test', m,
980
+ Column('a', Integer),
981
+ Column('b', Integer, nullable=False),
982
+ Column('c', TIMESTAMP),
983
+ Column('d', TIMESTAMP, nullable=False)
984
+ )
985
+
986
+
987
+ from sqlalchemy import create_engine
988
+ e = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True)
989
+ m.create_all(e)
990
+
991
+ output::
992
+
993
+ CREATE TABLE ts_test (
994
+ a INTEGER,
995
+ b INTEGER NOT NULL,
996
+ c TIMESTAMP NULL,
997
+ d TIMESTAMP NOT NULL
998
+ )
999
+
1000
+ """ # noqa
1001
+ from __future__ import annotations
1002
+
1003
+ from array import array as _array
1004
+ from collections import defaultdict
1005
+ from itertools import compress
1006
+ import re
1007
+ from typing import cast
1008
+
1009
+ from . import reflection as _reflection
1010
+ from .enumerated import ENUM
1011
+ from .enumerated import SET
1012
+ from .json import JSON
1013
+ from .json import JSONIndexType
1014
+ from .json import JSONPathType
1015
+ from .reserved_words import RESERVED_WORDS_MARIADB
1016
+ from .reserved_words import RESERVED_WORDS_MYSQL
1017
+ from .types import _FloatType
1018
+ from .types import _IntegerType
1019
+ from .types import _MatchType
1020
+ from .types import _NumericType
1021
+ from .types import _StringType
1022
+ from .types import BIGINT
1023
+ from .types import BIT
1024
+ from .types import CHAR
1025
+ from .types import DATETIME
1026
+ from .types import DECIMAL
1027
+ from .types import DOUBLE
1028
+ from .types import FLOAT
1029
+ from .types import INTEGER
1030
+ from .types import LONGBLOB
1031
+ from .types import LONGTEXT
1032
+ from .types import MEDIUMBLOB
1033
+ from .types import MEDIUMINT
1034
+ from .types import MEDIUMTEXT
1035
+ from .types import NCHAR
1036
+ from .types import NUMERIC
1037
+ from .types import NVARCHAR
1038
+ from .types import REAL
1039
+ from .types import SMALLINT
1040
+ from .types import TEXT
1041
+ from .types import TIME
1042
+ from .types import TIMESTAMP
1043
+ from .types import TINYBLOB
1044
+ from .types import TINYINT
1045
+ from .types import TINYTEXT
1046
+ from .types import VARCHAR
1047
+ from .types import YEAR
1048
+ from ... import exc
1049
+ from ... import literal_column
1050
+ from ... import log
1051
+ from ... import schema as sa_schema
1052
+ from ... import sql
1053
+ from ... import util
1054
+ from ...engine import cursor as _cursor
1055
+ from ...engine import default
1056
+ from ...engine import reflection
1057
+ from ...engine.reflection import ReflectionDefaults
1058
+ from ...sql import coercions
1059
+ from ...sql import compiler
1060
+ from ...sql import elements
1061
+ from ...sql import functions
1062
+ from ...sql import operators
1063
+ from ...sql import roles
1064
+ from ...sql import sqltypes
1065
+ from ...sql import util as sql_util
1066
+ from ...sql import visitors
1067
+ from ...sql.compiler import InsertmanyvaluesSentinelOpts
1068
+ from ...sql.compiler import SQLCompiler
1069
+ from ...sql.schema import SchemaConst
1070
+ from ...types import BINARY
1071
+ from ...types import BLOB
1072
+ from ...types import BOOLEAN
1073
+ from ...types import DATE
1074
+ from ...types import UUID
1075
+ from ...types import VARBINARY
1076
+ from ...util import topological
1077
+
1078
+
1079
+ SET_RE = re.compile(
1080
+ r"\s*SET\s+(?:(?:GLOBAL|SESSION)\s+)?\w", re.I | re.UNICODE
1081
+ )
1082
+
1083
+ # old names
1084
+ MSTime = TIME
1085
+ MSSet = SET
1086
+ MSEnum = ENUM
1087
+ MSLongBlob = LONGBLOB
1088
+ MSMediumBlob = MEDIUMBLOB
1089
+ MSTinyBlob = TINYBLOB
1090
+ MSBlob = BLOB
1091
+ MSBinary = BINARY
1092
+ MSVarBinary = VARBINARY
1093
+ MSNChar = NCHAR
1094
+ MSNVarChar = NVARCHAR
1095
+ MSChar = CHAR
1096
+ MSString = VARCHAR
1097
+ MSLongText = LONGTEXT
1098
+ MSMediumText = MEDIUMTEXT
1099
+ MSTinyText = TINYTEXT
1100
+ MSText = TEXT
1101
+ MSYear = YEAR
1102
+ MSTimeStamp = TIMESTAMP
1103
+ MSBit = BIT
1104
+ MSSmallInteger = SMALLINT
1105
+ MSTinyInteger = TINYINT
1106
+ MSMediumInteger = MEDIUMINT
1107
+ MSBigInteger = BIGINT
1108
+ MSNumeric = NUMERIC
1109
+ MSDecimal = DECIMAL
1110
+ MSDouble = DOUBLE
1111
+ MSReal = REAL
1112
+ MSFloat = FLOAT
1113
+ MSInteger = INTEGER
1114
+
1115
+ colspecs = {
1116
+ _IntegerType: _IntegerType,
1117
+ _NumericType: _NumericType,
1118
+ _FloatType: _FloatType,
1119
+ sqltypes.Numeric: NUMERIC,
1120
+ sqltypes.Float: FLOAT,
1121
+ sqltypes.Double: DOUBLE,
1122
+ sqltypes.Time: TIME,
1123
+ sqltypes.Enum: ENUM,
1124
+ sqltypes.MatchType: _MatchType,
1125
+ sqltypes.JSON: JSON,
1126
+ sqltypes.JSON.JSONIndexType: JSONIndexType,
1127
+ sqltypes.JSON.JSONPathType: JSONPathType,
1128
+ }
1129
+
1130
+ # Everything 3.23 through 5.1 excepting OpenGIS types.
1131
+ ischema_names = {
1132
+ "bigint": BIGINT,
1133
+ "binary": BINARY,
1134
+ "bit": BIT,
1135
+ "blob": BLOB,
1136
+ "boolean": BOOLEAN,
1137
+ "char": CHAR,
1138
+ "date": DATE,
1139
+ "datetime": DATETIME,
1140
+ "decimal": DECIMAL,
1141
+ "double": DOUBLE,
1142
+ "enum": ENUM,
1143
+ "fixed": DECIMAL,
1144
+ "float": FLOAT,
1145
+ "int": INTEGER,
1146
+ "integer": INTEGER,
1147
+ "json": JSON,
1148
+ "longblob": LONGBLOB,
1149
+ "longtext": LONGTEXT,
1150
+ "mediumblob": MEDIUMBLOB,
1151
+ "mediumint": MEDIUMINT,
1152
+ "mediumtext": MEDIUMTEXT,
1153
+ "nchar": NCHAR,
1154
+ "nvarchar": NVARCHAR,
1155
+ "numeric": NUMERIC,
1156
+ "set": SET,
1157
+ "smallint": SMALLINT,
1158
+ "text": TEXT,
1159
+ "time": TIME,
1160
+ "timestamp": TIMESTAMP,
1161
+ "tinyblob": TINYBLOB,
1162
+ "tinyint": TINYINT,
1163
+ "tinytext": TINYTEXT,
1164
+ "uuid": UUID,
1165
+ "varbinary": VARBINARY,
1166
+ "varchar": VARCHAR,
1167
+ "year": YEAR,
1168
+ }
1169
+
1170
+
1171
+ class MySQLExecutionContext(default.DefaultExecutionContext):
1172
+ def post_exec(self):
1173
+ if (
1174
+ self.isdelete
1175
+ and cast(SQLCompiler, self.compiled).effective_returning
1176
+ and not self.cursor.description
1177
+ ):
1178
+ # All MySQL/mariadb drivers appear to not include
1179
+ # cursor.description for DELETE..RETURNING with no rows if the
1180
+ # WHERE criteria is a straight "false" condition such as our EMPTY
1181
+ # IN condition. manufacture an empty result in this case (issue
1182
+ # #10505)
1183
+ #
1184
+ # taken from cx_Oracle implementation
1185
+ self.cursor_fetch_strategy = (
1186
+ _cursor.FullyBufferedCursorFetchStrategy(
1187
+ self.cursor,
1188
+ [
1189
+ (entry.keyname, None)
1190
+ for entry in cast(
1191
+ SQLCompiler, self.compiled
1192
+ )._result_columns
1193
+ ],
1194
+ [],
1195
+ )
1196
+ )
1197
+
1198
+ def create_server_side_cursor(self):
1199
+ if self.dialect.supports_server_side_cursors:
1200
+ return self._dbapi_connection.cursor(self.dialect._sscursor)
1201
+ else:
1202
+ raise NotImplementedError()
1203
+
1204
+ def fire_sequence(self, seq, type_):
1205
+ return self._execute_scalar(
1206
+ (
1207
+ "select nextval(%s)"
1208
+ % self.identifier_preparer.format_sequence(seq)
1209
+ ),
1210
+ type_,
1211
+ )
1212
+
1213
+
1214
+ class MySQLCompiler(compiler.SQLCompiler):
1215
+ render_table_with_column_in_update_from = True
1216
+ """Overridden from base SQLCompiler value"""
1217
+
1218
+ extract_map = compiler.SQLCompiler.extract_map.copy()
1219
+ extract_map.update({"milliseconds": "millisecond"})
1220
+
1221
+ def default_from(self):
1222
+ """Called when a ``SELECT`` statement has no froms,
1223
+ and no ``FROM`` clause is to be appended.
1224
+
1225
+ """
1226
+ if self.stack:
1227
+ stmt = self.stack[-1]["selectable"]
1228
+ if stmt._where_criteria:
1229
+ return " FROM DUAL"
1230
+
1231
+ return ""
1232
+
1233
+ def visit_random_func(self, fn, **kw):
1234
+ return "rand%s" % self.function_argspec(fn)
1235
+
1236
+ def visit_rollup_func(self, fn, **kw):
1237
+ clause = ", ".join(
1238
+ elem._compiler_dispatch(self, **kw) for elem in fn.clauses
1239
+ )
1240
+ return f"{clause} WITH ROLLUP"
1241
+
1242
+ def visit_aggregate_strings_func(self, fn, **kw):
1243
+ expr, delimeter = (
1244
+ elem._compiler_dispatch(self, **kw) for elem in fn.clauses
1245
+ )
1246
+ return f"group_concat({expr} SEPARATOR {delimeter})"
1247
+
1248
+ def visit_sequence(self, seq, **kw):
1249
+ return "nextval(%s)" % self.preparer.format_sequence(seq)
1250
+
1251
+ def visit_sysdate_func(self, fn, **kw):
1252
+ return "SYSDATE()"
1253
+
1254
+ def _render_json_extract_from_binary(self, binary, operator, **kw):
1255
+ # note we are intentionally calling upon the process() calls in the
1256
+ # order in which they appear in the SQL String as this is used
1257
+ # by positional parameter rendering
1258
+
1259
+ if binary.type._type_affinity is sqltypes.JSON:
1260
+ return "JSON_EXTRACT(%s, %s)" % (
1261
+ self.process(binary.left, **kw),
1262
+ self.process(binary.right, **kw),
1263
+ )
1264
+
1265
+ # for non-JSON, MySQL doesn't handle JSON null at all so it has to
1266
+ # be explicit
1267
+ case_expression = "CASE JSON_EXTRACT(%s, %s) WHEN 'null' THEN NULL" % (
1268
+ self.process(binary.left, **kw),
1269
+ self.process(binary.right, **kw),
1270
+ )
1271
+
1272
+ if binary.type._type_affinity is sqltypes.Integer:
1273
+ type_expression = (
1274
+ "ELSE CAST(JSON_EXTRACT(%s, %s) AS SIGNED INTEGER)"
1275
+ % (
1276
+ self.process(binary.left, **kw),
1277
+ self.process(binary.right, **kw),
1278
+ )
1279
+ )
1280
+ elif binary.type._type_affinity is sqltypes.Numeric:
1281
+ if (
1282
+ binary.type.scale is not None
1283
+ and binary.type.precision is not None
1284
+ ):
1285
+ # using DECIMAL here because MySQL does not recognize NUMERIC
1286
+ type_expression = (
1287
+ "ELSE CAST(JSON_EXTRACT(%s, %s) AS DECIMAL(%s, %s))"
1288
+ % (
1289
+ self.process(binary.left, **kw),
1290
+ self.process(binary.right, **kw),
1291
+ binary.type.precision,
1292
+ binary.type.scale,
1293
+ )
1294
+ )
1295
+ else:
1296
+ # FLOAT / REAL not added in MySQL til 8.0.17
1297
+ type_expression = (
1298
+ "ELSE JSON_EXTRACT(%s, %s)+0.0000000000000000000000"
1299
+ % (
1300
+ self.process(binary.left, **kw),
1301
+ self.process(binary.right, **kw),
1302
+ )
1303
+ )
1304
+ elif binary.type._type_affinity is sqltypes.Boolean:
1305
+ # the NULL handling is particularly weird with boolean, so
1306
+ # explicitly return true/false constants
1307
+ type_expression = "WHEN true THEN true ELSE false"
1308
+ elif binary.type._type_affinity is sqltypes.String:
1309
+ # (gord): this fails with a JSON value that's a four byte unicode
1310
+ # string. SQLite has the same problem at the moment
1311
+ # (zzzeek): I'm not really sure. let's take a look at a test case
1312
+ # that hits each backend and maybe make a requires rule for it?
1313
+ type_expression = "ELSE JSON_UNQUOTE(JSON_EXTRACT(%s, %s))" % (
1314
+ self.process(binary.left, **kw),
1315
+ self.process(binary.right, **kw),
1316
+ )
1317
+ else:
1318
+ # other affinity....this is not expected right now
1319
+ type_expression = "ELSE JSON_EXTRACT(%s, %s)" % (
1320
+ self.process(binary.left, **kw),
1321
+ self.process(binary.right, **kw),
1322
+ )
1323
+
1324
+ return case_expression + " " + type_expression + " END"
1325
+
1326
+ def visit_json_getitem_op_binary(self, binary, operator, **kw):
1327
+ return self._render_json_extract_from_binary(binary, operator, **kw)
1328
+
1329
+ def visit_json_path_getitem_op_binary(self, binary, operator, **kw):
1330
+ return self._render_json_extract_from_binary(binary, operator, **kw)
1331
+
1332
+ def visit_on_duplicate_key_update(self, on_duplicate, **kw):
1333
+ statement = self.current_executable
1334
+
1335
+ if on_duplicate._parameter_ordering:
1336
+ parameter_ordering = [
1337
+ coercions.expect(roles.DMLColumnRole, key)
1338
+ for key in on_duplicate._parameter_ordering
1339
+ ]
1340
+ ordered_keys = set(parameter_ordering)
1341
+ cols = [
1342
+ statement.table.c[key]
1343
+ for key in parameter_ordering
1344
+ if key in statement.table.c
1345
+ ] + [c for c in statement.table.c if c.key not in ordered_keys]
1346
+ else:
1347
+ cols = statement.table.c
1348
+
1349
+ clauses = []
1350
+
1351
+ requires_mysql8_alias = statement.select is None and (
1352
+ self.dialect._requires_alias_for_on_duplicate_key
1353
+ )
1354
+
1355
+ if requires_mysql8_alias:
1356
+ if statement.table.name.lower() == "new":
1357
+ _on_dup_alias_name = "new_1"
1358
+ else:
1359
+ _on_dup_alias_name = "new"
1360
+
1361
+ # traverses through all table columns to preserve table column order
1362
+ for column in (col for col in cols if col.key in on_duplicate.update):
1363
+ val = on_duplicate.update[column.key]
1364
+
1365
+ if coercions._is_literal(val):
1366
+ val = elements.BindParameter(None, val, type_=column.type)
1367
+ value_text = self.process(val.self_group(), use_schema=False)
1368
+ else:
1369
+
1370
+ def replace(obj):
1371
+ if (
1372
+ isinstance(obj, elements.BindParameter)
1373
+ and obj.type._isnull
1374
+ ):
1375
+ obj = obj._clone()
1376
+ obj.type = column.type
1377
+ return obj
1378
+ elif (
1379
+ isinstance(obj, elements.ColumnClause)
1380
+ and obj.table is on_duplicate.inserted_alias
1381
+ ):
1382
+ if requires_mysql8_alias:
1383
+ column_literal_clause = (
1384
+ f"{_on_dup_alias_name}."
1385
+ f"{self.preparer.quote(obj.name)}"
1386
+ )
1387
+ else:
1388
+ column_literal_clause = (
1389
+ f"VALUES({self.preparer.quote(obj.name)})"
1390
+ )
1391
+ return literal_column(column_literal_clause)
1392
+ else:
1393
+ # element is not replaced
1394
+ return None
1395
+
1396
+ val = visitors.replacement_traverse(val, {}, replace)
1397
+ value_text = self.process(val.self_group(), use_schema=False)
1398
+
1399
+ name_text = self.preparer.quote(column.name)
1400
+ clauses.append("%s = %s" % (name_text, value_text))
1401
+
1402
+ non_matching = set(on_duplicate.update) - {c.key for c in cols}
1403
+ if non_matching:
1404
+ util.warn(
1405
+ "Additional column names not matching "
1406
+ "any column keys in table '%s': %s"
1407
+ % (
1408
+ self.statement.table.name,
1409
+ (", ".join("'%s'" % c for c in non_matching)),
1410
+ )
1411
+ )
1412
+
1413
+ if requires_mysql8_alias:
1414
+ return (
1415
+ f"AS {_on_dup_alias_name} "
1416
+ f"ON DUPLICATE KEY UPDATE {', '.join(clauses)}"
1417
+ )
1418
+ else:
1419
+ return f"ON DUPLICATE KEY UPDATE {', '.join(clauses)}"
1420
+
1421
+ def visit_concat_op_expression_clauselist(
1422
+ self, clauselist, operator, **kw
1423
+ ):
1424
+ return "concat(%s)" % (
1425
+ ", ".join(self.process(elem, **kw) for elem in clauselist.clauses)
1426
+ )
1427
+
1428
+ def visit_concat_op_binary(self, binary, operator, **kw):
1429
+ return "concat(%s, %s)" % (
1430
+ self.process(binary.left, **kw),
1431
+ self.process(binary.right, **kw),
1432
+ )
1433
+
1434
+ _match_valid_flag_combinations = frozenset(
1435
+ (
1436
+ # (boolean_mode, natural_language, query_expansion)
1437
+ (False, False, False),
1438
+ (True, False, False),
1439
+ (False, True, False),
1440
+ (False, False, True),
1441
+ (False, True, True),
1442
+ )
1443
+ )
1444
+
1445
+ _match_flag_expressions = (
1446
+ "IN BOOLEAN MODE",
1447
+ "IN NATURAL LANGUAGE MODE",
1448
+ "WITH QUERY EXPANSION",
1449
+ )
1450
+
1451
+ def visit_mysql_match(self, element, **kw):
1452
+ return self.visit_match_op_binary(element, element.operator, **kw)
1453
+
1454
+ def visit_match_op_binary(self, binary, operator, **kw):
1455
+ """
1456
+ Note that `mysql_boolean_mode` is enabled by default because of
1457
+ backward compatibility
1458
+ """
1459
+
1460
+ modifiers = binary.modifiers
1461
+
1462
+ boolean_mode = modifiers.get("mysql_boolean_mode", True)
1463
+ natural_language = modifiers.get("mysql_natural_language", False)
1464
+ query_expansion = modifiers.get("mysql_query_expansion", False)
1465
+
1466
+ flag_combination = (boolean_mode, natural_language, query_expansion)
1467
+
1468
+ if flag_combination not in self._match_valid_flag_combinations:
1469
+ flags = (
1470
+ "in_boolean_mode=%s" % boolean_mode,
1471
+ "in_natural_language_mode=%s" % natural_language,
1472
+ "with_query_expansion=%s" % query_expansion,
1473
+ )
1474
+
1475
+ flags = ", ".join(flags)
1476
+
1477
+ raise exc.CompileError("Invalid MySQL match flags: %s" % flags)
1478
+
1479
+ match_clause = binary.left
1480
+ match_clause = self.process(match_clause, **kw)
1481
+ against_clause = self.process(binary.right, **kw)
1482
+
1483
+ if any(flag_combination):
1484
+ flag_expressions = compress(
1485
+ self._match_flag_expressions,
1486
+ flag_combination,
1487
+ )
1488
+
1489
+ against_clause = [against_clause]
1490
+ against_clause.extend(flag_expressions)
1491
+
1492
+ against_clause = " ".join(against_clause)
1493
+
1494
+ return "MATCH (%s) AGAINST (%s)" % (match_clause, against_clause)
1495
+
1496
+ def get_from_hint_text(self, table, text):
1497
+ return text
1498
+
1499
+ def visit_typeclause(self, typeclause, type_=None, **kw):
1500
+ if type_ is None:
1501
+ type_ = typeclause.type.dialect_impl(self.dialect)
1502
+ if isinstance(type_, sqltypes.TypeDecorator):
1503
+ return self.visit_typeclause(typeclause, type_.impl, **kw)
1504
+ elif isinstance(type_, sqltypes.Integer):
1505
+ if getattr(type_, "unsigned", False):
1506
+ return "UNSIGNED INTEGER"
1507
+ else:
1508
+ return "SIGNED INTEGER"
1509
+ elif isinstance(type_, sqltypes.TIMESTAMP):
1510
+ return "DATETIME"
1511
+ elif isinstance(
1512
+ type_,
1513
+ (
1514
+ sqltypes.DECIMAL,
1515
+ sqltypes.DateTime,
1516
+ sqltypes.Date,
1517
+ sqltypes.Time,
1518
+ ),
1519
+ ):
1520
+ return self.dialect.type_compiler_instance.process(type_)
1521
+ elif isinstance(type_, sqltypes.String) and not isinstance(
1522
+ type_, (ENUM, SET)
1523
+ ):
1524
+ adapted = CHAR._adapt_string_for_cast(type_)
1525
+ return self.dialect.type_compiler_instance.process(adapted)
1526
+ elif isinstance(type_, sqltypes._Binary):
1527
+ return "BINARY"
1528
+ elif isinstance(type_, sqltypes.JSON):
1529
+ return "JSON"
1530
+ elif isinstance(type_, sqltypes.NUMERIC):
1531
+ return self.dialect.type_compiler_instance.process(type_).replace(
1532
+ "NUMERIC", "DECIMAL"
1533
+ )
1534
+ elif (
1535
+ isinstance(type_, sqltypes.Float)
1536
+ and self.dialect._support_float_cast
1537
+ ):
1538
+ return self.dialect.type_compiler_instance.process(type_)
1539
+ else:
1540
+ return None
1541
+
1542
+ def visit_cast(self, cast, **kw):
1543
+ type_ = self.process(cast.typeclause)
1544
+ if type_ is None:
1545
+ util.warn(
1546
+ "Datatype %s does not support CAST on MySQL/MariaDb; "
1547
+ "the CAST will be skipped."
1548
+ % self.dialect.type_compiler_instance.process(
1549
+ cast.typeclause.type
1550
+ )
1551
+ )
1552
+ return self.process(cast.clause.self_group(), **kw)
1553
+
1554
+ return "CAST(%s AS %s)" % (self.process(cast.clause, **kw), type_)
1555
+
1556
+ def render_literal_value(self, value, type_):
1557
+ value = super().render_literal_value(value, type_)
1558
+ if self.dialect._backslash_escapes:
1559
+ value = value.replace("\\", "\\\\")
1560
+ return value
1561
+
1562
+ # override native_boolean=False behavior here, as
1563
+ # MySQL still supports native boolean
1564
+ def visit_true(self, element, **kw):
1565
+ return "true"
1566
+
1567
+ def visit_false(self, element, **kw):
1568
+ return "false"
1569
+
1570
+ def get_select_precolumns(self, select, **kw):
1571
+ """Add special MySQL keywords in place of DISTINCT.
1572
+
1573
+ .. deprecated:: 1.4 This usage is deprecated.
1574
+ :meth:`_expression.Select.prefix_with` should be used for special
1575
+ keywords at the start of a SELECT.
1576
+
1577
+ """
1578
+ if isinstance(select._distinct, str):
1579
+ util.warn_deprecated(
1580
+ "Sending string values for 'distinct' is deprecated in the "
1581
+ "MySQL dialect and will be removed in a future release. "
1582
+ "Please use :meth:`.Select.prefix_with` for special keywords "
1583
+ "at the start of a SELECT statement",
1584
+ version="1.4",
1585
+ )
1586
+ return select._distinct.upper() + " "
1587
+
1588
+ return super().get_select_precolumns(select, **kw)
1589
+
1590
+ def visit_join(self, join, asfrom=False, from_linter=None, **kwargs):
1591
+ if from_linter:
1592
+ from_linter.edges.add((join.left, join.right))
1593
+
1594
+ if join.full:
1595
+ join_type = " FULL OUTER JOIN "
1596
+ elif join.isouter:
1597
+ join_type = " LEFT OUTER JOIN "
1598
+ else:
1599
+ join_type = " INNER JOIN "
1600
+
1601
+ return "".join(
1602
+ (
1603
+ self.process(
1604
+ join.left, asfrom=True, from_linter=from_linter, **kwargs
1605
+ ),
1606
+ join_type,
1607
+ self.process(
1608
+ join.right, asfrom=True, from_linter=from_linter, **kwargs
1609
+ ),
1610
+ " ON ",
1611
+ self.process(join.onclause, from_linter=from_linter, **kwargs),
1612
+ )
1613
+ )
1614
+
1615
+ def for_update_clause(self, select, **kw):
1616
+ if select._for_update_arg.read:
1617
+ tmp = " LOCK IN SHARE MODE"
1618
+ else:
1619
+ tmp = " FOR UPDATE"
1620
+
1621
+ if select._for_update_arg.of and self.dialect.supports_for_update_of:
1622
+ tables = util.OrderedSet()
1623
+ for c in select._for_update_arg.of:
1624
+ tables.update(sql_util.surface_selectables_only(c))
1625
+
1626
+ tmp += " OF " + ", ".join(
1627
+ self.process(table, ashint=True, use_schema=False, **kw)
1628
+ for table in tables
1629
+ )
1630
+
1631
+ if select._for_update_arg.nowait:
1632
+ tmp += " NOWAIT"
1633
+
1634
+ if select._for_update_arg.skip_locked:
1635
+ tmp += " SKIP LOCKED"
1636
+
1637
+ return tmp
1638
+
1639
+ def limit_clause(self, select, **kw):
1640
+ # MySQL supports:
1641
+ # LIMIT <limit>
1642
+ # LIMIT <offset>, <limit>
1643
+ # and in server versions > 3.3:
1644
+ # LIMIT <limit> OFFSET <offset>
1645
+ # The latter is more readable for offsets but we're stuck with the
1646
+ # former until we can refine dialects by server revision.
1647
+
1648
+ limit_clause, offset_clause = (
1649
+ select._limit_clause,
1650
+ select._offset_clause,
1651
+ )
1652
+
1653
+ if limit_clause is None and offset_clause is None:
1654
+ return ""
1655
+ elif offset_clause is not None:
1656
+ # As suggested by the MySQL docs, need to apply an
1657
+ # artificial limit if one wasn't provided
1658
+ # https://dev.mysql.com/doc/refman/5.0/en/select.html
1659
+ if limit_clause is None:
1660
+ # TODO: remove ??
1661
+ # hardwire the upper limit. Currently
1662
+ # needed consistent with the usage of the upper
1663
+ # bound as part of MySQL's "syntax" for OFFSET with
1664
+ # no LIMIT.
1665
+ return " \n LIMIT %s, %s" % (
1666
+ self.process(offset_clause, **kw),
1667
+ "18446744073709551615",
1668
+ )
1669
+ else:
1670
+ return " \n LIMIT %s, %s" % (
1671
+ self.process(offset_clause, **kw),
1672
+ self.process(limit_clause, **kw),
1673
+ )
1674
+ else:
1675
+ # No offset provided, so just use the limit
1676
+ return " \n LIMIT %s" % (self.process(limit_clause, **kw),)
1677
+
1678
+ def update_limit_clause(self, update_stmt):
1679
+ limit = update_stmt.kwargs.get("%s_limit" % self.dialect.name, None)
1680
+ if limit:
1681
+ return "LIMIT %s" % limit
1682
+ else:
1683
+ return None
1684
+
1685
+ def update_tables_clause(self, update_stmt, from_table, extra_froms, **kw):
1686
+ kw["asfrom"] = True
1687
+ return ", ".join(
1688
+ t._compiler_dispatch(self, **kw)
1689
+ for t in [from_table] + list(extra_froms)
1690
+ )
1691
+
1692
+ def update_from_clause(
1693
+ self, update_stmt, from_table, extra_froms, from_hints, **kw
1694
+ ):
1695
+ return None
1696
+
1697
+ def delete_table_clause(self, delete_stmt, from_table, extra_froms, **kw):
1698
+ """If we have extra froms make sure we render any alias as hint."""
1699
+ ashint = False
1700
+ if extra_froms:
1701
+ ashint = True
1702
+ return from_table._compiler_dispatch(
1703
+ self, asfrom=True, iscrud=True, ashint=ashint, **kw
1704
+ )
1705
+
1706
+ def delete_extra_from_clause(
1707
+ self, delete_stmt, from_table, extra_froms, from_hints, **kw
1708
+ ):
1709
+ """Render the DELETE .. USING clause specific to MySQL."""
1710
+ kw["asfrom"] = True
1711
+ return "USING " + ", ".join(
1712
+ t._compiler_dispatch(self, fromhints=from_hints, **kw)
1713
+ for t in [from_table] + extra_froms
1714
+ )
1715
+
1716
+ def visit_empty_set_expr(self, element_types, **kw):
1717
+ return (
1718
+ "SELECT %(outer)s FROM (SELECT %(inner)s) "
1719
+ "as _empty_set WHERE 1!=1"
1720
+ % {
1721
+ "inner": ", ".join(
1722
+ "1 AS _in_%s" % idx
1723
+ for idx, type_ in enumerate(element_types)
1724
+ ),
1725
+ "outer": ", ".join(
1726
+ "_in_%s" % idx for idx, type_ in enumerate(element_types)
1727
+ ),
1728
+ }
1729
+ )
1730
+
1731
+ def visit_is_distinct_from_binary(self, binary, operator, **kw):
1732
+ return "NOT (%s <=> %s)" % (
1733
+ self.process(binary.left),
1734
+ self.process(binary.right),
1735
+ )
1736
+
1737
+ def visit_is_not_distinct_from_binary(self, binary, operator, **kw):
1738
+ return "%s <=> %s" % (
1739
+ self.process(binary.left),
1740
+ self.process(binary.right),
1741
+ )
1742
+
1743
+ def _mariadb_regexp_flags(self, flags, pattern, **kw):
1744
+ return "CONCAT('(?', %s, ')', %s)" % (
1745
+ self.render_literal_value(flags, sqltypes.STRINGTYPE),
1746
+ self.process(pattern, **kw),
1747
+ )
1748
+
1749
+ def _regexp_match(self, op_string, binary, operator, **kw):
1750
+ flags = binary.modifiers["flags"]
1751
+ if flags is None:
1752
+ return self._generate_generic_binary(binary, op_string, **kw)
1753
+ elif self.dialect.is_mariadb:
1754
+ return "%s%s%s" % (
1755
+ self.process(binary.left, **kw),
1756
+ op_string,
1757
+ self._mariadb_regexp_flags(flags, binary.right),
1758
+ )
1759
+ else:
1760
+ text = "REGEXP_LIKE(%s, %s, %s)" % (
1761
+ self.process(binary.left, **kw),
1762
+ self.process(binary.right, **kw),
1763
+ self.render_literal_value(flags, sqltypes.STRINGTYPE),
1764
+ )
1765
+ if op_string == " NOT REGEXP ":
1766
+ return "NOT %s" % text
1767
+ else:
1768
+ return text
1769
+
1770
+ def visit_regexp_match_op_binary(self, binary, operator, **kw):
1771
+ return self._regexp_match(" REGEXP ", binary, operator, **kw)
1772
+
1773
+ def visit_not_regexp_match_op_binary(self, binary, operator, **kw):
1774
+ return self._regexp_match(" NOT REGEXP ", binary, operator, **kw)
1775
+
1776
+ def visit_regexp_replace_op_binary(self, binary, operator, **kw):
1777
+ flags = binary.modifiers["flags"]
1778
+ if flags is None:
1779
+ return "REGEXP_REPLACE(%s, %s)" % (
1780
+ self.process(binary.left, **kw),
1781
+ self.process(binary.right, **kw),
1782
+ )
1783
+ elif self.dialect.is_mariadb:
1784
+ return "REGEXP_REPLACE(%s, %s, %s)" % (
1785
+ self.process(binary.left, **kw),
1786
+ self._mariadb_regexp_flags(flags, binary.right.clauses[0]),
1787
+ self.process(binary.right.clauses[1], **kw),
1788
+ )
1789
+ else:
1790
+ return "REGEXP_REPLACE(%s, %s, %s)" % (
1791
+ self.process(binary.left, **kw),
1792
+ self.process(binary.right, **kw),
1793
+ self.render_literal_value(flags, sqltypes.STRINGTYPE),
1794
+ )
1795
+
1796
+
1797
+ class MySQLDDLCompiler(compiler.DDLCompiler):
1798
+ def get_column_specification(self, column, **kw):
1799
+ """Builds column DDL."""
1800
+ if (
1801
+ self.dialect.is_mariadb is True
1802
+ and column.computed is not None
1803
+ and column._user_defined_nullable is SchemaConst.NULL_UNSPECIFIED
1804
+ ):
1805
+ column.nullable = True
1806
+ colspec = [
1807
+ self.preparer.format_column(column),
1808
+ self.dialect.type_compiler_instance.process(
1809
+ column.type, type_expression=column
1810
+ ),
1811
+ ]
1812
+
1813
+ if column.computed is not None:
1814
+ colspec.append(self.process(column.computed))
1815
+
1816
+ is_timestamp = isinstance(
1817
+ column.type._unwrapped_dialect_impl(self.dialect),
1818
+ sqltypes.TIMESTAMP,
1819
+ )
1820
+
1821
+ if not column.nullable:
1822
+ colspec.append("NOT NULL")
1823
+
1824
+ # see: https://docs.sqlalchemy.org/en/latest/dialects/mysql.html#mysql_timestamp_null # noqa
1825
+ elif column.nullable and is_timestamp:
1826
+ colspec.append("NULL")
1827
+
1828
+ comment = column.comment
1829
+ if comment is not None:
1830
+ literal = self.sql_compiler.render_literal_value(
1831
+ comment, sqltypes.String()
1832
+ )
1833
+ colspec.append("COMMENT " + literal)
1834
+
1835
+ if (
1836
+ column.table is not None
1837
+ and column is column.table._autoincrement_column
1838
+ and (
1839
+ column.server_default is None
1840
+ or isinstance(column.server_default, sa_schema.Identity)
1841
+ )
1842
+ and not (
1843
+ self.dialect.supports_sequences
1844
+ and isinstance(column.default, sa_schema.Sequence)
1845
+ and not column.default.optional
1846
+ )
1847
+ ):
1848
+ colspec.append("AUTO_INCREMENT")
1849
+ else:
1850
+ default = self.get_column_default_string(column)
1851
+ if default is not None:
1852
+ if (
1853
+ isinstance(
1854
+ column.server_default.arg, functions.FunctionElement
1855
+ )
1856
+ and self.dialect._support_default_function
1857
+ ):
1858
+ colspec.append(f"DEFAULT ({default})")
1859
+ else:
1860
+ colspec.append("DEFAULT " + default)
1861
+ return " ".join(colspec)
1862
+
1863
+ def post_create_table(self, table):
1864
+ """Build table-level CREATE options like ENGINE and COLLATE."""
1865
+
1866
+ table_opts = []
1867
+
1868
+ opts = {
1869
+ k[len(self.dialect.name) + 1 :].upper(): v
1870
+ for k, v in table.kwargs.items()
1871
+ if k.startswith("%s_" % self.dialect.name)
1872
+ }
1873
+
1874
+ if table.comment is not None:
1875
+ opts["COMMENT"] = table.comment
1876
+
1877
+ partition_options = [
1878
+ "PARTITION_BY",
1879
+ "PARTITIONS",
1880
+ "SUBPARTITIONS",
1881
+ "SUBPARTITION_BY",
1882
+ ]
1883
+
1884
+ nonpart_options = set(opts).difference(partition_options)
1885
+ part_options = set(opts).intersection(partition_options)
1886
+
1887
+ for opt in topological.sort(
1888
+ [
1889
+ ("DEFAULT_CHARSET", "COLLATE"),
1890
+ ("DEFAULT_CHARACTER_SET", "COLLATE"),
1891
+ ("CHARSET", "COLLATE"),
1892
+ ("CHARACTER_SET", "COLLATE"),
1893
+ ],
1894
+ nonpart_options,
1895
+ ):
1896
+ arg = opts[opt]
1897
+ if opt in _reflection._options_of_type_string:
1898
+ arg = self.sql_compiler.render_literal_value(
1899
+ arg, sqltypes.String()
1900
+ )
1901
+
1902
+ if opt in (
1903
+ "DATA_DIRECTORY",
1904
+ "INDEX_DIRECTORY",
1905
+ "DEFAULT_CHARACTER_SET",
1906
+ "CHARACTER_SET",
1907
+ "DEFAULT_CHARSET",
1908
+ "DEFAULT_COLLATE",
1909
+ ):
1910
+ opt = opt.replace("_", " ")
1911
+
1912
+ joiner = "="
1913
+ if opt in (
1914
+ "TABLESPACE",
1915
+ "DEFAULT CHARACTER SET",
1916
+ "CHARACTER SET",
1917
+ "COLLATE",
1918
+ ):
1919
+ joiner = " "
1920
+
1921
+ table_opts.append(joiner.join((opt, arg)))
1922
+
1923
+ for opt in topological.sort(
1924
+ [
1925
+ ("PARTITION_BY", "PARTITIONS"),
1926
+ ("PARTITION_BY", "SUBPARTITION_BY"),
1927
+ ("PARTITION_BY", "SUBPARTITIONS"),
1928
+ ("PARTITIONS", "SUBPARTITIONS"),
1929
+ ("PARTITIONS", "SUBPARTITION_BY"),
1930
+ ("SUBPARTITION_BY", "SUBPARTITIONS"),
1931
+ ],
1932
+ part_options,
1933
+ ):
1934
+ arg = opts[opt]
1935
+ if opt in _reflection._options_of_type_string:
1936
+ arg = self.sql_compiler.render_literal_value(
1937
+ arg, sqltypes.String()
1938
+ )
1939
+
1940
+ opt = opt.replace("_", " ")
1941
+ joiner = " "
1942
+
1943
+ table_opts.append(joiner.join((opt, arg)))
1944
+
1945
+ return " ".join(table_opts)
1946
+
1947
+ def visit_create_index(self, create, **kw):
1948
+ index = create.element
1949
+ self._verify_index_table(index)
1950
+ preparer = self.preparer
1951
+ table = preparer.format_table(index.table)
1952
+
1953
+ columns = [
1954
+ self.sql_compiler.process(
1955
+ (
1956
+ elements.Grouping(expr)
1957
+ if (
1958
+ isinstance(expr, elements.BinaryExpression)
1959
+ or (
1960
+ isinstance(expr, elements.UnaryExpression)
1961
+ and expr.modifier
1962
+ not in (operators.desc_op, operators.asc_op)
1963
+ )
1964
+ or isinstance(expr, functions.FunctionElement)
1965
+ )
1966
+ else expr
1967
+ ),
1968
+ include_table=False,
1969
+ literal_binds=True,
1970
+ )
1971
+ for expr in index.expressions
1972
+ ]
1973
+
1974
+ name = self._prepared_index_name(index)
1975
+
1976
+ text = "CREATE "
1977
+ if index.unique:
1978
+ text += "UNIQUE "
1979
+
1980
+ index_prefix = index.kwargs.get("%s_prefix" % self.dialect.name, None)
1981
+ if index_prefix:
1982
+ text += index_prefix + " "
1983
+
1984
+ text += "INDEX "
1985
+ if create.if_not_exists:
1986
+ text += "IF NOT EXISTS "
1987
+ text += "%s ON %s " % (name, table)
1988
+
1989
+ length = index.dialect_options[self.dialect.name]["length"]
1990
+ if length is not None:
1991
+ if isinstance(length, dict):
1992
+ # length value can be a (column_name --> integer value)
1993
+ # mapping specifying the prefix length for each column of the
1994
+ # index
1995
+ columns = ", ".join(
1996
+ (
1997
+ "%s(%d)" % (expr, length[col.name])
1998
+ if col.name in length
1999
+ else (
2000
+ "%s(%d)" % (expr, length[expr])
2001
+ if expr in length
2002
+ else "%s" % expr
2003
+ )
2004
+ )
2005
+ for col, expr in zip(index.expressions, columns)
2006
+ )
2007
+ else:
2008
+ # or can be an integer value specifying the same
2009
+ # prefix length for all columns of the index
2010
+ columns = ", ".join(
2011
+ "%s(%d)" % (col, length) for col in columns
2012
+ )
2013
+ else:
2014
+ columns = ", ".join(columns)
2015
+ text += "(%s)" % columns
2016
+
2017
+ parser = index.dialect_options["mysql"]["with_parser"]
2018
+ if parser is not None:
2019
+ text += " WITH PARSER %s" % (parser,)
2020
+
2021
+ using = index.dialect_options["mysql"]["using"]
2022
+ if using is not None:
2023
+ text += " USING %s" % (preparer.quote(using))
2024
+
2025
+ return text
2026
+
2027
+ def visit_primary_key_constraint(self, constraint, **kw):
2028
+ text = super().visit_primary_key_constraint(constraint)
2029
+ using = constraint.dialect_options["mysql"]["using"]
2030
+ if using:
2031
+ text += " USING %s" % (self.preparer.quote(using))
2032
+ return text
2033
+
2034
+ def visit_drop_index(self, drop, **kw):
2035
+ index = drop.element
2036
+ text = "\nDROP INDEX "
2037
+ if drop.if_exists:
2038
+ text += "IF EXISTS "
2039
+
2040
+ return text + "%s ON %s" % (
2041
+ self._prepared_index_name(index, include_schema=False),
2042
+ self.preparer.format_table(index.table),
2043
+ )
2044
+
2045
+ def visit_drop_constraint(self, drop, **kw):
2046
+ constraint = drop.element
2047
+ if isinstance(constraint, sa_schema.ForeignKeyConstraint):
2048
+ qual = "FOREIGN KEY "
2049
+ const = self.preparer.format_constraint(constraint)
2050
+ elif isinstance(constraint, sa_schema.PrimaryKeyConstraint):
2051
+ qual = "PRIMARY KEY "
2052
+ const = ""
2053
+ elif isinstance(constraint, sa_schema.UniqueConstraint):
2054
+ qual = "INDEX "
2055
+ const = self.preparer.format_constraint(constraint)
2056
+ elif isinstance(constraint, sa_schema.CheckConstraint):
2057
+ if self.dialect.is_mariadb:
2058
+ qual = "CONSTRAINT "
2059
+ else:
2060
+ qual = "CHECK "
2061
+ const = self.preparer.format_constraint(constraint)
2062
+ else:
2063
+ qual = ""
2064
+ const = self.preparer.format_constraint(constraint)
2065
+ return "ALTER TABLE %s DROP %s%s" % (
2066
+ self.preparer.format_table(constraint.table),
2067
+ qual,
2068
+ const,
2069
+ )
2070
+
2071
+ def define_constraint_match(self, constraint):
2072
+ if constraint.match is not None:
2073
+ raise exc.CompileError(
2074
+ "MySQL ignores the 'MATCH' keyword while at the same time "
2075
+ "causes ON UPDATE/ON DELETE clauses to be ignored."
2076
+ )
2077
+ return ""
2078
+
2079
+ def visit_set_table_comment(self, create, **kw):
2080
+ return "ALTER TABLE %s COMMENT %s" % (
2081
+ self.preparer.format_table(create.element),
2082
+ self.sql_compiler.render_literal_value(
2083
+ create.element.comment, sqltypes.String()
2084
+ ),
2085
+ )
2086
+
2087
+ def visit_drop_table_comment(self, create, **kw):
2088
+ return "ALTER TABLE %s COMMENT ''" % (
2089
+ self.preparer.format_table(create.element)
2090
+ )
2091
+
2092
+ def visit_set_column_comment(self, create, **kw):
2093
+ return "ALTER TABLE %s CHANGE %s %s" % (
2094
+ self.preparer.format_table(create.element.table),
2095
+ self.preparer.format_column(create.element),
2096
+ self.get_column_specification(create.element),
2097
+ )
2098
+
2099
+
2100
+ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
2101
+ def _extend_numeric(self, type_, spec):
2102
+ "Extend a numeric-type declaration with MySQL specific extensions."
2103
+
2104
+ if not self._mysql_type(type_):
2105
+ return spec
2106
+
2107
+ if type_.unsigned:
2108
+ spec += " UNSIGNED"
2109
+ if type_.zerofill:
2110
+ spec += " ZEROFILL"
2111
+ return spec
2112
+
2113
+ def _extend_string(self, type_, defaults, spec):
2114
+ """Extend a string-type declaration with standard SQL CHARACTER SET /
2115
+ COLLATE annotations and MySQL specific extensions.
2116
+
2117
+ """
2118
+
2119
+ def attr(name):
2120
+ return getattr(type_, name, defaults.get(name))
2121
+
2122
+ if attr("charset"):
2123
+ charset = "CHARACTER SET %s" % attr("charset")
2124
+ elif attr("ascii"):
2125
+ charset = "ASCII"
2126
+ elif attr("unicode"):
2127
+ charset = "UNICODE"
2128
+ else:
2129
+ charset = None
2130
+
2131
+ if attr("collation"):
2132
+ collation = "COLLATE %s" % type_.collation
2133
+ elif attr("binary"):
2134
+ collation = "BINARY"
2135
+ else:
2136
+ collation = None
2137
+
2138
+ if attr("national"):
2139
+ # NATIONAL (aka NCHAR/NVARCHAR) trumps charsets.
2140
+ return " ".join(
2141
+ [c for c in ("NATIONAL", spec, collation) if c is not None]
2142
+ )
2143
+ return " ".join(
2144
+ [c for c in (spec, charset, collation) if c is not None]
2145
+ )
2146
+
2147
+ def _mysql_type(self, type_):
2148
+ return isinstance(type_, (_StringType, _NumericType))
2149
+
2150
+ def visit_NUMERIC(self, type_, **kw):
2151
+ if type_.precision is None:
2152
+ return self._extend_numeric(type_, "NUMERIC")
2153
+ elif type_.scale is None:
2154
+ return self._extend_numeric(
2155
+ type_,
2156
+ "NUMERIC(%(precision)s)" % {"precision": type_.precision},
2157
+ )
2158
+ else:
2159
+ return self._extend_numeric(
2160
+ type_,
2161
+ "NUMERIC(%(precision)s, %(scale)s)"
2162
+ % {"precision": type_.precision, "scale": type_.scale},
2163
+ )
2164
+
2165
+ def visit_DECIMAL(self, type_, **kw):
2166
+ if type_.precision is None:
2167
+ return self._extend_numeric(type_, "DECIMAL")
2168
+ elif type_.scale is None:
2169
+ return self._extend_numeric(
2170
+ type_,
2171
+ "DECIMAL(%(precision)s)" % {"precision": type_.precision},
2172
+ )
2173
+ else:
2174
+ return self._extend_numeric(
2175
+ type_,
2176
+ "DECIMAL(%(precision)s, %(scale)s)"
2177
+ % {"precision": type_.precision, "scale": type_.scale},
2178
+ )
2179
+
2180
+ def visit_DOUBLE(self, type_, **kw):
2181
+ if type_.precision is not None and type_.scale is not None:
2182
+ return self._extend_numeric(
2183
+ type_,
2184
+ "DOUBLE(%(precision)s, %(scale)s)"
2185
+ % {"precision": type_.precision, "scale": type_.scale},
2186
+ )
2187
+ else:
2188
+ return self._extend_numeric(type_, "DOUBLE")
2189
+
2190
+ def visit_REAL(self, type_, **kw):
2191
+ if type_.precision is not None and type_.scale is not None:
2192
+ return self._extend_numeric(
2193
+ type_,
2194
+ "REAL(%(precision)s, %(scale)s)"
2195
+ % {"precision": type_.precision, "scale": type_.scale},
2196
+ )
2197
+ else:
2198
+ return self._extend_numeric(type_, "REAL")
2199
+
2200
+ def visit_FLOAT(self, type_, **kw):
2201
+ if (
2202
+ self._mysql_type(type_)
2203
+ and type_.scale is not None
2204
+ and type_.precision is not None
2205
+ ):
2206
+ return self._extend_numeric(
2207
+ type_, "FLOAT(%s, %s)" % (type_.precision, type_.scale)
2208
+ )
2209
+ elif type_.precision is not None:
2210
+ return self._extend_numeric(
2211
+ type_, "FLOAT(%s)" % (type_.precision,)
2212
+ )
2213
+ else:
2214
+ return self._extend_numeric(type_, "FLOAT")
2215
+
2216
+ def visit_INTEGER(self, type_, **kw):
2217
+ if self._mysql_type(type_) and type_.display_width is not None:
2218
+ return self._extend_numeric(
2219
+ type_,
2220
+ "INTEGER(%(display_width)s)"
2221
+ % {"display_width": type_.display_width},
2222
+ )
2223
+ else:
2224
+ return self._extend_numeric(type_, "INTEGER")
2225
+
2226
+ def visit_BIGINT(self, type_, **kw):
2227
+ if self._mysql_type(type_) and type_.display_width is not None:
2228
+ return self._extend_numeric(
2229
+ type_,
2230
+ "BIGINT(%(display_width)s)"
2231
+ % {"display_width": type_.display_width},
2232
+ )
2233
+ else:
2234
+ return self._extend_numeric(type_, "BIGINT")
2235
+
2236
+ def visit_MEDIUMINT(self, type_, **kw):
2237
+ if self._mysql_type(type_) and type_.display_width is not None:
2238
+ return self._extend_numeric(
2239
+ type_,
2240
+ "MEDIUMINT(%(display_width)s)"
2241
+ % {"display_width": type_.display_width},
2242
+ )
2243
+ else:
2244
+ return self._extend_numeric(type_, "MEDIUMINT")
2245
+
2246
+ def visit_TINYINT(self, type_, **kw):
2247
+ if self._mysql_type(type_) and type_.display_width is not None:
2248
+ return self._extend_numeric(
2249
+ type_, "TINYINT(%s)" % type_.display_width
2250
+ )
2251
+ else:
2252
+ return self._extend_numeric(type_, "TINYINT")
2253
+
2254
+ def visit_SMALLINT(self, type_, **kw):
2255
+ if self._mysql_type(type_) and type_.display_width is not None:
2256
+ return self._extend_numeric(
2257
+ type_,
2258
+ "SMALLINT(%(display_width)s)"
2259
+ % {"display_width": type_.display_width},
2260
+ )
2261
+ else:
2262
+ return self._extend_numeric(type_, "SMALLINT")
2263
+
2264
+ def visit_BIT(self, type_, **kw):
2265
+ if type_.length is not None:
2266
+ return "BIT(%s)" % type_.length
2267
+ else:
2268
+ return "BIT"
2269
+
2270
+ def visit_DATETIME(self, type_, **kw):
2271
+ if getattr(type_, "fsp", None):
2272
+ return "DATETIME(%d)" % type_.fsp
2273
+ else:
2274
+ return "DATETIME"
2275
+
2276
+ def visit_DATE(self, type_, **kw):
2277
+ return "DATE"
2278
+
2279
+ def visit_TIME(self, type_, **kw):
2280
+ if getattr(type_, "fsp", None):
2281
+ return "TIME(%d)" % type_.fsp
2282
+ else:
2283
+ return "TIME"
2284
+
2285
+ def visit_TIMESTAMP(self, type_, **kw):
2286
+ if getattr(type_, "fsp", None):
2287
+ return "TIMESTAMP(%d)" % type_.fsp
2288
+ else:
2289
+ return "TIMESTAMP"
2290
+
2291
+ def visit_YEAR(self, type_, **kw):
2292
+ if type_.display_width is None:
2293
+ return "YEAR"
2294
+ else:
2295
+ return "YEAR(%s)" % type_.display_width
2296
+
2297
+ def visit_TEXT(self, type_, **kw):
2298
+ if type_.length is not None:
2299
+ return self._extend_string(type_, {}, "TEXT(%d)" % type_.length)
2300
+ else:
2301
+ return self._extend_string(type_, {}, "TEXT")
2302
+
2303
+ def visit_TINYTEXT(self, type_, **kw):
2304
+ return self._extend_string(type_, {}, "TINYTEXT")
2305
+
2306
+ def visit_MEDIUMTEXT(self, type_, **kw):
2307
+ return self._extend_string(type_, {}, "MEDIUMTEXT")
2308
+
2309
+ def visit_LONGTEXT(self, type_, **kw):
2310
+ return self._extend_string(type_, {}, "LONGTEXT")
2311
+
2312
+ def visit_VARCHAR(self, type_, **kw):
2313
+ if type_.length is not None:
2314
+ return self._extend_string(type_, {}, "VARCHAR(%d)" % type_.length)
2315
+ else:
2316
+ raise exc.CompileError(
2317
+ "VARCHAR requires a length on dialect %s" % self.dialect.name
2318
+ )
2319
+
2320
+ def visit_CHAR(self, type_, **kw):
2321
+ if type_.length is not None:
2322
+ return self._extend_string(
2323
+ type_, {}, "CHAR(%(length)s)" % {"length": type_.length}
2324
+ )
2325
+ else:
2326
+ return self._extend_string(type_, {}, "CHAR")
2327
+
2328
+ def visit_NVARCHAR(self, type_, **kw):
2329
+ # We'll actually generate the equiv. "NATIONAL VARCHAR" instead
2330
+ # of "NVARCHAR".
2331
+ if type_.length is not None:
2332
+ return self._extend_string(
2333
+ type_,
2334
+ {"national": True},
2335
+ "VARCHAR(%(length)s)" % {"length": type_.length},
2336
+ )
2337
+ else:
2338
+ raise exc.CompileError(
2339
+ "NVARCHAR requires a length on dialect %s" % self.dialect.name
2340
+ )
2341
+
2342
+ def visit_NCHAR(self, type_, **kw):
2343
+ # We'll actually generate the equiv.
2344
+ # "NATIONAL CHAR" instead of "NCHAR".
2345
+ if type_.length is not None:
2346
+ return self._extend_string(
2347
+ type_,
2348
+ {"national": True},
2349
+ "CHAR(%(length)s)" % {"length": type_.length},
2350
+ )
2351
+ else:
2352
+ return self._extend_string(type_, {"national": True}, "CHAR")
2353
+
2354
+ def visit_UUID(self, type_, **kw):
2355
+ return "UUID"
2356
+
2357
+ def visit_VARBINARY(self, type_, **kw):
2358
+ return "VARBINARY(%d)" % type_.length
2359
+
2360
+ def visit_JSON(self, type_, **kw):
2361
+ return "JSON"
2362
+
2363
+ def visit_large_binary(self, type_, **kw):
2364
+ return self.visit_BLOB(type_)
2365
+
2366
+ def visit_enum(self, type_, **kw):
2367
+ if not type_.native_enum:
2368
+ return super().visit_enum(type_)
2369
+ else:
2370
+ return self._visit_enumerated_values("ENUM", type_, type_.enums)
2371
+
2372
+ def visit_BLOB(self, type_, **kw):
2373
+ if type_.length is not None:
2374
+ return "BLOB(%d)" % type_.length
2375
+ else:
2376
+ return "BLOB"
2377
+
2378
+ def visit_TINYBLOB(self, type_, **kw):
2379
+ return "TINYBLOB"
2380
+
2381
+ def visit_MEDIUMBLOB(self, type_, **kw):
2382
+ return "MEDIUMBLOB"
2383
+
2384
+ def visit_LONGBLOB(self, type_, **kw):
2385
+ return "LONGBLOB"
2386
+
2387
+ def _visit_enumerated_values(self, name, type_, enumerated_values):
2388
+ quoted_enums = []
2389
+ for e in enumerated_values:
2390
+ if self.dialect.identifier_preparer._double_percents:
2391
+ e = e.replace("%", "%%")
2392
+ quoted_enums.append("'%s'" % e.replace("'", "''"))
2393
+ return self._extend_string(
2394
+ type_, {}, "%s(%s)" % (name, ",".join(quoted_enums))
2395
+ )
2396
+
2397
+ def visit_ENUM(self, type_, **kw):
2398
+ return self._visit_enumerated_values("ENUM", type_, type_.enums)
2399
+
2400
+ def visit_SET(self, type_, **kw):
2401
+ return self._visit_enumerated_values("SET", type_, type_.values)
2402
+
2403
+ def visit_BOOLEAN(self, type_, **kw):
2404
+ return "BOOL"
2405
+
2406
+
2407
+ class MySQLIdentifierPreparer(compiler.IdentifierPreparer):
2408
+ reserved_words = RESERVED_WORDS_MYSQL
2409
+
2410
+ def __init__(self, dialect, server_ansiquotes=False, **kw):
2411
+ if not server_ansiquotes:
2412
+ quote = "`"
2413
+ else:
2414
+ quote = '"'
2415
+
2416
+ super().__init__(dialect, initial_quote=quote, escape_quote=quote)
2417
+
2418
+ def _quote_free_identifiers(self, *ids):
2419
+ """Unilaterally identifier-quote any number of strings."""
2420
+
2421
+ return tuple([self.quote_identifier(i) for i in ids if i is not None])
2422
+
2423
+
2424
+ class MariaDBIdentifierPreparer(MySQLIdentifierPreparer):
2425
+ reserved_words = RESERVED_WORDS_MARIADB
2426
+
2427
+
2428
+ @log.class_logger
2429
+ class MySQLDialect(default.DefaultDialect):
2430
+ """Details of the MySQL dialect.
2431
+ Not used directly in application code.
2432
+ """
2433
+
2434
+ name = "mysql"
2435
+ supports_statement_cache = True
2436
+
2437
+ supports_alter = True
2438
+
2439
+ # MySQL has no true "boolean" type; we
2440
+ # allow for the "true" and "false" keywords, however
2441
+ supports_native_boolean = False
2442
+
2443
+ # identifiers are 64, however aliases can be 255...
2444
+ max_identifier_length = 255
2445
+ max_index_name_length = 64
2446
+ max_constraint_name_length = 64
2447
+
2448
+ div_is_floordiv = False
2449
+
2450
+ supports_native_enum = True
2451
+
2452
+ returns_native_bytes = True
2453
+
2454
+ supports_sequences = False # default for MySQL ...
2455
+ # ... may be updated to True for MariaDB 10.3+ in initialize()
2456
+
2457
+ sequences_optional = False
2458
+
2459
+ supports_for_update_of = False # default for MySQL ...
2460
+ # ... may be updated to True for MySQL 8+ in initialize()
2461
+
2462
+ _requires_alias_for_on_duplicate_key = False # Only available ...
2463
+ # ... in MySQL 8+
2464
+
2465
+ # MySQL doesn't support "DEFAULT VALUES" but *does* support
2466
+ # "VALUES (DEFAULT)"
2467
+ supports_default_values = False
2468
+ supports_default_metavalue = True
2469
+
2470
+ use_insertmanyvalues: bool = True
2471
+ insertmanyvalues_implicit_sentinel = (
2472
+ InsertmanyvaluesSentinelOpts.ANY_AUTOINCREMENT
2473
+ )
2474
+
2475
+ supports_sane_rowcount = True
2476
+ supports_sane_multi_rowcount = False
2477
+ supports_multivalues_insert = True
2478
+ insert_null_pk_still_autoincrements = True
2479
+
2480
+ supports_comments = True
2481
+ inline_comments = True
2482
+ default_paramstyle = "format"
2483
+ colspecs = colspecs
2484
+
2485
+ cte_follows_insert = True
2486
+
2487
+ statement_compiler = MySQLCompiler
2488
+ ddl_compiler = MySQLDDLCompiler
2489
+ type_compiler_cls = MySQLTypeCompiler
2490
+ ischema_names = ischema_names
2491
+ preparer = MySQLIdentifierPreparer
2492
+
2493
+ is_mariadb = False
2494
+ _mariadb_normalized_version_info = None
2495
+
2496
+ # default SQL compilation settings -
2497
+ # these are modified upon initialize(),
2498
+ # i.e. first connect
2499
+ _backslash_escapes = True
2500
+ _server_ansiquotes = False
2501
+
2502
+ construct_arguments = [
2503
+ (sa_schema.Table, {"*": None}),
2504
+ (sql.Update, {"limit": None}),
2505
+ (sa_schema.PrimaryKeyConstraint, {"using": None}),
2506
+ (
2507
+ sa_schema.Index,
2508
+ {
2509
+ "using": None,
2510
+ "length": None,
2511
+ "prefix": None,
2512
+ "with_parser": None,
2513
+ },
2514
+ ),
2515
+ ]
2516
+
2517
+ def __init__(
2518
+ self,
2519
+ json_serializer=None,
2520
+ json_deserializer=None,
2521
+ is_mariadb=None,
2522
+ **kwargs,
2523
+ ):
2524
+ kwargs.pop("use_ansiquotes", None) # legacy
2525
+ default.DefaultDialect.__init__(self, **kwargs)
2526
+ self._json_serializer = json_serializer
2527
+ self._json_deserializer = json_deserializer
2528
+ self._set_mariadb(is_mariadb, None)
2529
+
2530
+ def get_isolation_level_values(self, dbapi_conn):
2531
+ return (
2532
+ "SERIALIZABLE",
2533
+ "READ UNCOMMITTED",
2534
+ "READ COMMITTED",
2535
+ "REPEATABLE READ",
2536
+ )
2537
+
2538
+ def set_isolation_level(self, dbapi_connection, level):
2539
+ cursor = dbapi_connection.cursor()
2540
+ cursor.execute(f"SET SESSION TRANSACTION ISOLATION LEVEL {level}")
2541
+ cursor.execute("COMMIT")
2542
+ cursor.close()
2543
+
2544
+ def get_isolation_level(self, dbapi_connection):
2545
+ cursor = dbapi_connection.cursor()
2546
+ if self._is_mysql and self.server_version_info >= (5, 7, 20):
2547
+ cursor.execute("SELECT @@transaction_isolation")
2548
+ else:
2549
+ cursor.execute("SELECT @@tx_isolation")
2550
+ row = cursor.fetchone()
2551
+ if row is None:
2552
+ util.warn(
2553
+ "Could not retrieve transaction isolation level for MySQL "
2554
+ "connection."
2555
+ )
2556
+ raise NotImplementedError()
2557
+ val = row[0]
2558
+ cursor.close()
2559
+ if isinstance(val, bytes):
2560
+ val = val.decode()
2561
+ return val.upper().replace("-", " ")
2562
+
2563
+ @classmethod
2564
+ def _is_mariadb_from_url(cls, url):
2565
+ dbapi = cls.import_dbapi()
2566
+ dialect = cls(dbapi=dbapi)
2567
+
2568
+ cargs, cparams = dialect.create_connect_args(url)
2569
+ conn = dialect.connect(*cargs, **cparams)
2570
+ try:
2571
+ cursor = conn.cursor()
2572
+ cursor.execute("SELECT VERSION() LIKE '%MariaDB%'")
2573
+ val = cursor.fetchone()[0]
2574
+ except:
2575
+ raise
2576
+ else:
2577
+ return bool(val)
2578
+ finally:
2579
+ conn.close()
2580
+
2581
+ def _get_server_version_info(self, connection):
2582
+ # get database server version info explicitly over the wire
2583
+ # to avoid proxy servers like MaxScale getting in the
2584
+ # way with their own values, see #4205
2585
+ dbapi_con = connection.connection
2586
+ cursor = dbapi_con.cursor()
2587
+ cursor.execute("SELECT VERSION()")
2588
+ val = cursor.fetchone()[0]
2589
+ cursor.close()
2590
+ if isinstance(val, bytes):
2591
+ val = val.decode()
2592
+
2593
+ return self._parse_server_version(val)
2594
+
2595
+ def _parse_server_version(self, val):
2596
+ version = []
2597
+ is_mariadb = False
2598
+
2599
+ r = re.compile(r"[.\-+]")
2600
+ tokens = r.split(val)
2601
+ for token in tokens:
2602
+ parsed_token = re.match(
2603
+ r"^(?:(\d+)(?:a|b|c)?|(MariaDB\w*))$", token
2604
+ )
2605
+ if not parsed_token:
2606
+ continue
2607
+ elif parsed_token.group(2):
2608
+ self._mariadb_normalized_version_info = tuple(version[-3:])
2609
+ is_mariadb = True
2610
+ else:
2611
+ digit = int(parsed_token.group(1))
2612
+ version.append(digit)
2613
+
2614
+ server_version_info = tuple(version)
2615
+
2616
+ self._set_mariadb(
2617
+ server_version_info and is_mariadb, server_version_info
2618
+ )
2619
+
2620
+ if not is_mariadb:
2621
+ self._mariadb_normalized_version_info = server_version_info
2622
+
2623
+ if server_version_info < (5, 0, 2):
2624
+ raise NotImplementedError(
2625
+ "the MySQL/MariaDB dialect supports server "
2626
+ "version info 5.0.2 and above."
2627
+ )
2628
+
2629
+ # setting it here to help w the test suite
2630
+ self.server_version_info = server_version_info
2631
+ return server_version_info
2632
+
2633
+ def _set_mariadb(self, is_mariadb, server_version_info):
2634
+ if is_mariadb is None:
2635
+ return
2636
+
2637
+ if not is_mariadb and self.is_mariadb:
2638
+ raise exc.InvalidRequestError(
2639
+ "MySQL version %s is not a MariaDB variant."
2640
+ % (".".join(map(str, server_version_info)),)
2641
+ )
2642
+ if is_mariadb:
2643
+ self.preparer = MariaDBIdentifierPreparer
2644
+ # this would have been set by the default dialect already,
2645
+ # so set it again
2646
+ self.identifier_preparer = self.preparer(self)
2647
+
2648
+ # this will be updated on first connect in initialize()
2649
+ # if using older mariadb version
2650
+ self.delete_returning = True
2651
+ self.insert_returning = True
2652
+
2653
+ self.is_mariadb = is_mariadb
2654
+
2655
+ def do_begin_twophase(self, connection, xid):
2656
+ connection.execute(sql.text("XA BEGIN :xid"), dict(xid=xid))
2657
+
2658
+ def do_prepare_twophase(self, connection, xid):
2659
+ connection.execute(sql.text("XA END :xid"), dict(xid=xid))
2660
+ connection.execute(sql.text("XA PREPARE :xid"), dict(xid=xid))
2661
+
2662
+ def do_rollback_twophase(
2663
+ self, connection, xid, is_prepared=True, recover=False
2664
+ ):
2665
+ if not is_prepared:
2666
+ connection.execute(sql.text("XA END :xid"), dict(xid=xid))
2667
+ connection.execute(sql.text("XA ROLLBACK :xid"), dict(xid=xid))
2668
+
2669
+ def do_commit_twophase(
2670
+ self, connection, xid, is_prepared=True, recover=False
2671
+ ):
2672
+ if not is_prepared:
2673
+ self.do_prepare_twophase(connection, xid)
2674
+ connection.execute(sql.text("XA COMMIT :xid"), dict(xid=xid))
2675
+
2676
+ def do_recover_twophase(self, connection):
2677
+ resultset = connection.exec_driver_sql("XA RECOVER")
2678
+ return [row["data"][0 : row["gtrid_length"]] for row in resultset]
2679
+
2680
+ def is_disconnect(self, e, connection, cursor):
2681
+ if isinstance(
2682
+ e,
2683
+ (
2684
+ self.dbapi.OperationalError,
2685
+ self.dbapi.ProgrammingError,
2686
+ self.dbapi.InterfaceError,
2687
+ ),
2688
+ ) and self._extract_error_code(e) in (
2689
+ 1927,
2690
+ 2006,
2691
+ 2013,
2692
+ 2014,
2693
+ 2045,
2694
+ 2055,
2695
+ 4031,
2696
+ ):
2697
+ return True
2698
+ elif isinstance(
2699
+ e, (self.dbapi.InterfaceError, self.dbapi.InternalError)
2700
+ ):
2701
+ # if underlying connection is closed,
2702
+ # this is the error you get
2703
+ return "(0, '')" in str(e)
2704
+ else:
2705
+ return False
2706
+
2707
+ def _compat_fetchall(self, rp, charset=None):
2708
+ """Proxy result rows to smooth over MySQL-Python driver
2709
+ inconsistencies."""
2710
+
2711
+ return [_DecodingRow(row, charset) for row in rp.fetchall()]
2712
+
2713
+ def _compat_fetchone(self, rp, charset=None):
2714
+ """Proxy a result row to smooth over MySQL-Python driver
2715
+ inconsistencies."""
2716
+
2717
+ row = rp.fetchone()
2718
+ if row:
2719
+ return _DecodingRow(row, charset)
2720
+ else:
2721
+ return None
2722
+
2723
+ def _compat_first(self, rp, charset=None):
2724
+ """Proxy a result row to smooth over MySQL-Python driver
2725
+ inconsistencies."""
2726
+
2727
+ row = rp.first()
2728
+ if row:
2729
+ return _DecodingRow(row, charset)
2730
+ else:
2731
+ return None
2732
+
2733
+ def _extract_error_code(self, exception):
2734
+ raise NotImplementedError()
2735
+
2736
+ def _get_default_schema_name(self, connection):
2737
+ return connection.exec_driver_sql("SELECT DATABASE()").scalar()
2738
+
2739
+ @reflection.cache
2740
+ def has_table(self, connection, table_name, schema=None, **kw):
2741
+ self._ensure_has_table_connection(connection)
2742
+
2743
+ if schema is None:
2744
+ schema = self.default_schema_name
2745
+
2746
+ assert schema is not None
2747
+
2748
+ full_name = ".".join(
2749
+ self.identifier_preparer._quote_free_identifiers(
2750
+ schema, table_name
2751
+ )
2752
+ )
2753
+
2754
+ # DESCRIBE *must* be used because there is no information schema
2755
+ # table that returns information on temp tables that is consistently
2756
+ # available on MariaDB / MySQL / engine-agnostic etc.
2757
+ # therefore we have no choice but to use DESCRIBE and an error catch
2758
+ # to detect "False". See issue #9058
2759
+
2760
+ try:
2761
+ with connection.exec_driver_sql(
2762
+ f"DESCRIBE {full_name}",
2763
+ execution_options={"skip_user_error_events": True},
2764
+ ) as rs:
2765
+ return rs.fetchone() is not None
2766
+ except exc.DBAPIError as e:
2767
+ # https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html # noqa: E501
2768
+ # there are a lot of codes that *may* pop up here at some point
2769
+ # but we continue to be fairly conservative. We include:
2770
+ # 1146: Table '%s.%s' doesn't exist - what every MySQL has emitted
2771
+ # for decades
2772
+ #
2773
+ # mysql 8 suddenly started emitting:
2774
+ # 1049: Unknown database '%s' - for nonexistent schema
2775
+ #
2776
+ # also added:
2777
+ # 1051: Unknown table '%s' - not known to emit
2778
+ #
2779
+ # there's more "doesn't exist" kinds of messages but they are
2780
+ # less clear if mysql 8 would suddenly start using one of those
2781
+ if self._extract_error_code(e.orig) in (1146, 1049, 1051):
2782
+ return False
2783
+ raise
2784
+
2785
+ @reflection.cache
2786
+ def has_sequence(self, connection, sequence_name, schema=None, **kw):
2787
+ if not self.supports_sequences:
2788
+ self._sequences_not_supported()
2789
+ if not schema:
2790
+ schema = self.default_schema_name
2791
+ # MariaDB implements sequences as a special type of table
2792
+ #
2793
+ cursor = connection.execute(
2794
+ sql.text(
2795
+ "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES "
2796
+ "WHERE TABLE_TYPE='SEQUENCE' and TABLE_NAME=:name AND "
2797
+ "TABLE_SCHEMA=:schema_name"
2798
+ ),
2799
+ dict(
2800
+ name=str(sequence_name),
2801
+ schema_name=str(schema),
2802
+ ),
2803
+ )
2804
+ return cursor.first() is not None
2805
+
2806
+ def _sequences_not_supported(self):
2807
+ raise NotImplementedError(
2808
+ "Sequences are supported only by the "
2809
+ "MariaDB series 10.3 or greater"
2810
+ )
2811
+
2812
+ @reflection.cache
2813
+ def get_sequence_names(self, connection, schema=None, **kw):
2814
+ if not self.supports_sequences:
2815
+ self._sequences_not_supported()
2816
+ if not schema:
2817
+ schema = self.default_schema_name
2818
+ # MariaDB implements sequences as a special type of table
2819
+ cursor = connection.execute(
2820
+ sql.text(
2821
+ "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES "
2822
+ "WHERE TABLE_TYPE='SEQUENCE' and TABLE_SCHEMA=:schema_name"
2823
+ ),
2824
+ dict(schema_name=schema),
2825
+ )
2826
+ return [
2827
+ row[0]
2828
+ for row in self._compat_fetchall(
2829
+ cursor, charset=self._connection_charset
2830
+ )
2831
+ ]
2832
+
2833
+ def initialize(self, connection):
2834
+ # this is driver-based, does not need server version info
2835
+ # and is fairly critical for even basic SQL operations
2836
+ self._connection_charset = self._detect_charset(connection)
2837
+
2838
+ # call super().initialize() because we need to have
2839
+ # server_version_info set up. in 1.4 under python 2 only this does the
2840
+ # "check unicode returns" thing, which is the one area that some
2841
+ # SQL gets compiled within initialize() currently
2842
+ default.DefaultDialect.initialize(self, connection)
2843
+
2844
+ self._detect_sql_mode(connection)
2845
+ self._detect_ansiquotes(connection) # depends on sql mode
2846
+ self._detect_casing(connection)
2847
+ if self._server_ansiquotes:
2848
+ # if ansiquotes == True, build a new IdentifierPreparer
2849
+ # with the new setting
2850
+ self.identifier_preparer = self.preparer(
2851
+ self, server_ansiquotes=self._server_ansiquotes
2852
+ )
2853
+
2854
+ self.supports_sequences = (
2855
+ self.is_mariadb and self.server_version_info >= (10, 3)
2856
+ )
2857
+
2858
+ self.supports_for_update_of = (
2859
+ self._is_mysql and self.server_version_info >= (8,)
2860
+ )
2861
+
2862
+ self._needs_correct_for_88718_96365 = (
2863
+ not self.is_mariadb and self.server_version_info >= (8,)
2864
+ )
2865
+
2866
+ self.delete_returning = (
2867
+ self.is_mariadb and self.server_version_info >= (10, 0, 5)
2868
+ )
2869
+
2870
+ self.insert_returning = (
2871
+ self.is_mariadb and self.server_version_info >= (10, 5)
2872
+ )
2873
+
2874
+ self._requires_alias_for_on_duplicate_key = (
2875
+ self._is_mysql and self.server_version_info >= (8, 0, 20)
2876
+ )
2877
+
2878
+ self._warn_for_known_db_issues()
2879
+
2880
+ def _warn_for_known_db_issues(self):
2881
+ if self.is_mariadb:
2882
+ mdb_version = self._mariadb_normalized_version_info
2883
+ if mdb_version > (10, 2) and mdb_version < (10, 2, 9):
2884
+ util.warn(
2885
+ "MariaDB %r before 10.2.9 has known issues regarding "
2886
+ "CHECK constraints, which impact handling of NULL values "
2887
+ "with SQLAlchemy's boolean datatype (MDEV-13596). An "
2888
+ "additional issue prevents proper migrations of columns "
2889
+ "with CHECK constraints (MDEV-11114). Please upgrade to "
2890
+ "MariaDB 10.2.9 or greater, or use the MariaDB 10.1 "
2891
+ "series, to avoid these issues." % (mdb_version,)
2892
+ )
2893
+
2894
+ @property
2895
+ def _support_float_cast(self):
2896
+ if not self.server_version_info:
2897
+ return False
2898
+ elif self.is_mariadb:
2899
+ # ref https://mariadb.com/kb/en/mariadb-1045-release-notes/
2900
+ return self.server_version_info >= (10, 4, 5)
2901
+ else:
2902
+ # ref https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-17.html#mysqld-8-0-17-feature # noqa
2903
+ return self.server_version_info >= (8, 0, 17)
2904
+
2905
+ @property
2906
+ def _support_default_function(self):
2907
+ if not self.server_version_info:
2908
+ return False
2909
+ elif self.is_mariadb:
2910
+ # ref https://mariadb.com/kb/en/mariadb-1021-release-notes/
2911
+ return self.server_version_info >= (10, 2, 1)
2912
+ else:
2913
+ # ref https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html # noqa
2914
+ return self.server_version_info >= (8, 0, 13)
2915
+
2916
+ @property
2917
+ def _is_mariadb(self):
2918
+ return self.is_mariadb
2919
+
2920
+ @property
2921
+ def _is_mysql(self):
2922
+ return not self.is_mariadb
2923
+
2924
+ @property
2925
+ def _is_mariadb_102(self):
2926
+ return self.is_mariadb and self._mariadb_normalized_version_info > (
2927
+ 10,
2928
+ 2,
2929
+ )
2930
+
2931
+ @reflection.cache
2932
+ def get_schema_names(self, connection, **kw):
2933
+ rp = connection.exec_driver_sql("SHOW schemas")
2934
+ return [r[0] for r in rp]
2935
+
2936
+ @reflection.cache
2937
+ def get_table_names(self, connection, schema=None, **kw):
2938
+ """Return a Unicode SHOW TABLES from a given schema."""
2939
+ if schema is not None:
2940
+ current_schema = schema
2941
+ else:
2942
+ current_schema = self.default_schema_name
2943
+
2944
+ charset = self._connection_charset
2945
+
2946
+ rp = connection.exec_driver_sql(
2947
+ "SHOW FULL TABLES FROM %s"
2948
+ % self.identifier_preparer.quote_identifier(current_schema)
2949
+ )
2950
+
2951
+ return [
2952
+ row[0]
2953
+ for row in self._compat_fetchall(rp, charset=charset)
2954
+ if row[1] == "BASE TABLE"
2955
+ ]
2956
+
2957
+ @reflection.cache
2958
+ def get_view_names(self, connection, schema=None, **kw):
2959
+ if schema is None:
2960
+ schema = self.default_schema_name
2961
+ charset = self._connection_charset
2962
+ rp = connection.exec_driver_sql(
2963
+ "SHOW FULL TABLES FROM %s"
2964
+ % self.identifier_preparer.quote_identifier(schema)
2965
+ )
2966
+ return [
2967
+ row[0]
2968
+ for row in self._compat_fetchall(rp, charset=charset)
2969
+ if row[1] in ("VIEW", "SYSTEM VIEW")
2970
+ ]
2971
+
2972
+ @reflection.cache
2973
+ def get_table_options(self, connection, table_name, schema=None, **kw):
2974
+ parsed_state = self._parsed_state_or_create(
2975
+ connection, table_name, schema, **kw
2976
+ )
2977
+ if parsed_state.table_options:
2978
+ return parsed_state.table_options
2979
+ else:
2980
+ return ReflectionDefaults.table_options()
2981
+
2982
+ @reflection.cache
2983
+ def get_columns(self, connection, table_name, schema=None, **kw):
2984
+ parsed_state = self._parsed_state_or_create(
2985
+ connection, table_name, schema, **kw
2986
+ )
2987
+ if parsed_state.columns:
2988
+ return parsed_state.columns
2989
+ else:
2990
+ return ReflectionDefaults.columns()
2991
+
2992
+ @reflection.cache
2993
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
2994
+ parsed_state = self._parsed_state_or_create(
2995
+ connection, table_name, schema, **kw
2996
+ )
2997
+ for key in parsed_state.keys:
2998
+ if key["type"] == "PRIMARY":
2999
+ # There can be only one.
3000
+ cols = [s[0] for s in key["columns"]]
3001
+ return {"constrained_columns": cols, "name": None}
3002
+ return ReflectionDefaults.pk_constraint()
3003
+
3004
+ @reflection.cache
3005
+ def get_foreign_keys(self, connection, table_name, schema=None, **kw):
3006
+ parsed_state = self._parsed_state_or_create(
3007
+ connection, table_name, schema, **kw
3008
+ )
3009
+ default_schema = None
3010
+
3011
+ fkeys = []
3012
+
3013
+ for spec in parsed_state.fk_constraints:
3014
+ ref_name = spec["table"][-1]
3015
+ ref_schema = len(spec["table"]) > 1 and spec["table"][-2] or schema
3016
+
3017
+ if not ref_schema:
3018
+ if default_schema is None:
3019
+ default_schema = connection.dialect.default_schema_name
3020
+ if schema == default_schema:
3021
+ ref_schema = schema
3022
+
3023
+ loc_names = spec["local"]
3024
+ ref_names = spec["foreign"]
3025
+
3026
+ con_kw = {}
3027
+ for opt in ("onupdate", "ondelete"):
3028
+ if spec.get(opt, False) not in ("NO ACTION", None):
3029
+ con_kw[opt] = spec[opt]
3030
+
3031
+ fkey_d = {
3032
+ "name": spec["name"],
3033
+ "constrained_columns": loc_names,
3034
+ "referred_schema": ref_schema,
3035
+ "referred_table": ref_name,
3036
+ "referred_columns": ref_names,
3037
+ "options": con_kw,
3038
+ }
3039
+ fkeys.append(fkey_d)
3040
+
3041
+ if self._needs_correct_for_88718_96365:
3042
+ self._correct_for_mysql_bugs_88718_96365(fkeys, connection)
3043
+
3044
+ return fkeys if fkeys else ReflectionDefaults.foreign_keys()
3045
+
3046
+ def _correct_for_mysql_bugs_88718_96365(self, fkeys, connection):
3047
+ # Foreign key is always in lower case (MySQL 8.0)
3048
+ # https://bugs.mysql.com/bug.php?id=88718
3049
+ # issue #4344 for SQLAlchemy
3050
+
3051
+ # table name also for MySQL 8.0
3052
+ # https://bugs.mysql.com/bug.php?id=96365
3053
+ # issue #4751 for SQLAlchemy
3054
+
3055
+ # for lower_case_table_names=2, information_schema.columns
3056
+ # preserves the original table/schema casing, but SHOW CREATE
3057
+ # TABLE does not. this problem is not in lower_case_table_names=1,
3058
+ # but use case-insensitive matching for these two modes in any case.
3059
+
3060
+ if self._casing in (1, 2):
3061
+
3062
+ def lower(s):
3063
+ return s.lower()
3064
+
3065
+ else:
3066
+ # if on case sensitive, there can be two tables referenced
3067
+ # with the same name different casing, so we need to use
3068
+ # case-sensitive matching.
3069
+ def lower(s):
3070
+ return s
3071
+
3072
+ default_schema_name = connection.dialect.default_schema_name
3073
+
3074
+ # NOTE: using (table_schema, table_name, lower(column_name)) in (...)
3075
+ # is very slow since mysql does not seem able to properly use indexse.
3076
+ # Unpack the where condition instead.
3077
+ schema_by_table_by_column = defaultdict(lambda: defaultdict(list))
3078
+ for rec in fkeys:
3079
+ sch = lower(rec["referred_schema"] or default_schema_name)
3080
+ tbl = lower(rec["referred_table"])
3081
+ for col_name in rec["referred_columns"]:
3082
+ schema_by_table_by_column[sch][tbl].append(col_name)
3083
+
3084
+ if schema_by_table_by_column:
3085
+
3086
+ condition = sql.or_(
3087
+ *(
3088
+ sql.and_(
3089
+ _info_columns.c.table_schema == schema,
3090
+ sql.or_(
3091
+ *(
3092
+ sql.and_(
3093
+ _info_columns.c.table_name == table,
3094
+ sql.func.lower(
3095
+ _info_columns.c.column_name
3096
+ ).in_(columns),
3097
+ )
3098
+ for table, columns in tables.items()
3099
+ )
3100
+ ),
3101
+ )
3102
+ for schema, tables in schema_by_table_by_column.items()
3103
+ )
3104
+ )
3105
+
3106
+ select = sql.select(
3107
+ _info_columns.c.table_schema,
3108
+ _info_columns.c.table_name,
3109
+ _info_columns.c.column_name,
3110
+ ).where(condition)
3111
+
3112
+ correct_for_wrong_fk_case = connection.execute(select)
3113
+
3114
+ # in casing=0, table name and schema name come back in their
3115
+ # exact case.
3116
+ # in casing=1, table name and schema name come back in lower
3117
+ # case.
3118
+ # in casing=2, table name and schema name come back from the
3119
+ # information_schema.columns view in the case
3120
+ # that was used in CREATE DATABASE and CREATE TABLE, but
3121
+ # SHOW CREATE TABLE converts them to *lower case*, therefore
3122
+ # not matching. So for this case, case-insensitive lookup
3123
+ # is necessary
3124
+ d = defaultdict(dict)
3125
+ for schema, tname, cname in correct_for_wrong_fk_case:
3126
+ d[(lower(schema), lower(tname))]["SCHEMANAME"] = schema
3127
+ d[(lower(schema), lower(tname))]["TABLENAME"] = tname
3128
+ d[(lower(schema), lower(tname))][cname.lower()] = cname
3129
+
3130
+ for fkey in fkeys:
3131
+ rec = d[
3132
+ (
3133
+ lower(fkey["referred_schema"] or default_schema_name),
3134
+ lower(fkey["referred_table"]),
3135
+ )
3136
+ ]
3137
+
3138
+ fkey["referred_table"] = rec["TABLENAME"]
3139
+ if fkey["referred_schema"] is not None:
3140
+ fkey["referred_schema"] = rec["SCHEMANAME"]
3141
+
3142
+ fkey["referred_columns"] = [
3143
+ rec[col.lower()] for col in fkey["referred_columns"]
3144
+ ]
3145
+
3146
+ @reflection.cache
3147
+ def get_check_constraints(self, connection, table_name, schema=None, **kw):
3148
+ parsed_state = self._parsed_state_or_create(
3149
+ connection, table_name, schema, **kw
3150
+ )
3151
+
3152
+ cks = [
3153
+ {"name": spec["name"], "sqltext": spec["sqltext"]}
3154
+ for spec in parsed_state.ck_constraints
3155
+ ]
3156
+ cks.sort(key=lambda d: d["name"] or "~") # sort None as last
3157
+ return cks if cks else ReflectionDefaults.check_constraints()
3158
+
3159
+ @reflection.cache
3160
+ def get_table_comment(self, connection, table_name, schema=None, **kw):
3161
+ parsed_state = self._parsed_state_or_create(
3162
+ connection, table_name, schema, **kw
3163
+ )
3164
+ comment = parsed_state.table_options.get(f"{self.name}_comment", None)
3165
+ if comment is not None:
3166
+ return {"text": comment}
3167
+ else:
3168
+ return ReflectionDefaults.table_comment()
3169
+
3170
+ @reflection.cache
3171
+ def get_indexes(self, connection, table_name, schema=None, **kw):
3172
+ parsed_state = self._parsed_state_or_create(
3173
+ connection, table_name, schema, **kw
3174
+ )
3175
+
3176
+ indexes = []
3177
+
3178
+ for spec in parsed_state.keys:
3179
+ dialect_options = {}
3180
+ unique = False
3181
+ flavor = spec["type"]
3182
+ if flavor == "PRIMARY":
3183
+ continue
3184
+ if flavor == "UNIQUE":
3185
+ unique = True
3186
+ elif flavor in ("FULLTEXT", "SPATIAL"):
3187
+ dialect_options["%s_prefix" % self.name] = flavor
3188
+ elif flavor is None:
3189
+ pass
3190
+ else:
3191
+ self.logger.info(
3192
+ "Converting unknown KEY type %s to a plain KEY", flavor
3193
+ )
3194
+ pass
3195
+
3196
+ if spec["parser"]:
3197
+ dialect_options["%s_with_parser" % (self.name)] = spec[
3198
+ "parser"
3199
+ ]
3200
+
3201
+ index_d = {}
3202
+
3203
+ index_d["name"] = spec["name"]
3204
+ index_d["column_names"] = [s[0] for s in spec["columns"]]
3205
+ mysql_length = {
3206
+ s[0]: s[1] for s in spec["columns"] if s[1] is not None
3207
+ }
3208
+ if mysql_length:
3209
+ dialect_options["%s_length" % self.name] = mysql_length
3210
+
3211
+ index_d["unique"] = unique
3212
+ if flavor:
3213
+ index_d["type"] = flavor
3214
+
3215
+ if dialect_options:
3216
+ index_d["dialect_options"] = dialect_options
3217
+
3218
+ indexes.append(index_d)
3219
+ indexes.sort(key=lambda d: d["name"] or "~") # sort None as last
3220
+ return indexes if indexes else ReflectionDefaults.indexes()
3221
+
3222
+ @reflection.cache
3223
+ def get_unique_constraints(
3224
+ self, connection, table_name, schema=None, **kw
3225
+ ):
3226
+ parsed_state = self._parsed_state_or_create(
3227
+ connection, table_name, schema, **kw
3228
+ )
3229
+
3230
+ ucs = [
3231
+ {
3232
+ "name": key["name"],
3233
+ "column_names": [col[0] for col in key["columns"]],
3234
+ "duplicates_index": key["name"],
3235
+ }
3236
+ for key in parsed_state.keys
3237
+ if key["type"] == "UNIQUE"
3238
+ ]
3239
+ ucs.sort(key=lambda d: d["name"] or "~") # sort None as last
3240
+ if ucs:
3241
+ return ucs
3242
+ else:
3243
+ return ReflectionDefaults.unique_constraints()
3244
+
3245
+ @reflection.cache
3246
+ def get_view_definition(self, connection, view_name, schema=None, **kw):
3247
+ charset = self._connection_charset
3248
+ full_name = ".".join(
3249
+ self.identifier_preparer._quote_free_identifiers(schema, view_name)
3250
+ )
3251
+ sql = self._show_create_table(
3252
+ connection, None, charset, full_name=full_name
3253
+ )
3254
+ if sql.upper().startswith("CREATE TABLE"):
3255
+ # it's a table, not a view
3256
+ raise exc.NoSuchTableError(full_name)
3257
+ return sql
3258
+
3259
+ def _parsed_state_or_create(
3260
+ self, connection, table_name, schema=None, **kw
3261
+ ):
3262
+ return self._setup_parser(
3263
+ connection,
3264
+ table_name,
3265
+ schema,
3266
+ info_cache=kw.get("info_cache", None),
3267
+ )
3268
+
3269
+ @util.memoized_property
3270
+ def _tabledef_parser(self):
3271
+ """return the MySQLTableDefinitionParser, generate if needed.
3272
+
3273
+ The deferred creation ensures that the dialect has
3274
+ retrieved server version information first.
3275
+
3276
+ """
3277
+ preparer = self.identifier_preparer
3278
+ return _reflection.MySQLTableDefinitionParser(self, preparer)
3279
+
3280
+ @reflection.cache
3281
+ def _setup_parser(self, connection, table_name, schema=None, **kw):
3282
+ charset = self._connection_charset
3283
+ parser = self._tabledef_parser
3284
+ full_name = ".".join(
3285
+ self.identifier_preparer._quote_free_identifiers(
3286
+ schema, table_name
3287
+ )
3288
+ )
3289
+ sql = self._show_create_table(
3290
+ connection, None, charset, full_name=full_name
3291
+ )
3292
+ if parser._check_view(sql):
3293
+ # Adapt views to something table-like.
3294
+ columns = self._describe_table(
3295
+ connection, None, charset, full_name=full_name
3296
+ )
3297
+ sql = parser._describe_to_create(table_name, columns)
3298
+ return parser.parse(sql, charset)
3299
+
3300
+ def _fetch_setting(self, connection, setting_name):
3301
+ charset = self._connection_charset
3302
+
3303
+ if self.server_version_info and self.server_version_info < (5, 6):
3304
+ sql = "SHOW VARIABLES LIKE '%s'" % setting_name
3305
+ fetch_col = 1
3306
+ else:
3307
+ sql = "SELECT @@%s" % setting_name
3308
+ fetch_col = 0
3309
+
3310
+ show_var = connection.exec_driver_sql(sql)
3311
+ row = self._compat_first(show_var, charset=charset)
3312
+ if not row:
3313
+ return None
3314
+ else:
3315
+ return row[fetch_col]
3316
+
3317
+ def _detect_charset(self, connection):
3318
+ raise NotImplementedError()
3319
+
3320
+ def _detect_casing(self, connection):
3321
+ """Sniff out identifier case sensitivity.
3322
+
3323
+ Cached per-connection. This value can not change without a server
3324
+ restart.
3325
+
3326
+ """
3327
+ # https://dev.mysql.com/doc/refman/en/identifier-case-sensitivity.html
3328
+
3329
+ setting = self._fetch_setting(connection, "lower_case_table_names")
3330
+ if setting is None:
3331
+ cs = 0
3332
+ else:
3333
+ # 4.0.15 returns OFF or ON according to [ticket:489]
3334
+ # 3.23 doesn't, 4.0.27 doesn't..
3335
+ if setting == "OFF":
3336
+ cs = 0
3337
+ elif setting == "ON":
3338
+ cs = 1
3339
+ else:
3340
+ cs = int(setting)
3341
+ self._casing = cs
3342
+ return cs
3343
+
3344
+ def _detect_collations(self, connection):
3345
+ """Pull the active COLLATIONS list from the server.
3346
+
3347
+ Cached per-connection.
3348
+ """
3349
+
3350
+ collations = {}
3351
+ charset = self._connection_charset
3352
+ rs = connection.exec_driver_sql("SHOW COLLATION")
3353
+ for row in self._compat_fetchall(rs, charset):
3354
+ collations[row[0]] = row[1]
3355
+ return collations
3356
+
3357
+ def _detect_sql_mode(self, connection):
3358
+ setting = self._fetch_setting(connection, "sql_mode")
3359
+
3360
+ if setting is None:
3361
+ util.warn(
3362
+ "Could not retrieve SQL_MODE; please ensure the "
3363
+ "MySQL user has permissions to SHOW VARIABLES"
3364
+ )
3365
+ self._sql_mode = ""
3366
+ else:
3367
+ self._sql_mode = setting or ""
3368
+
3369
+ def _detect_ansiquotes(self, connection):
3370
+ """Detect and adjust for the ANSI_QUOTES sql mode."""
3371
+
3372
+ mode = self._sql_mode
3373
+ if not mode:
3374
+ mode = ""
3375
+ elif mode.isdigit():
3376
+ mode_no = int(mode)
3377
+ mode = (mode_no | 4 == mode_no) and "ANSI_QUOTES" or ""
3378
+
3379
+ self._server_ansiquotes = "ANSI_QUOTES" in mode
3380
+
3381
+ # as of MySQL 5.0.1
3382
+ self._backslash_escapes = "NO_BACKSLASH_ESCAPES" not in mode
3383
+
3384
+ def _show_create_table(
3385
+ self, connection, table, charset=None, full_name=None
3386
+ ):
3387
+ """Run SHOW CREATE TABLE for a ``Table``."""
3388
+
3389
+ if full_name is None:
3390
+ full_name = self.identifier_preparer.format_table(table)
3391
+ st = "SHOW CREATE TABLE %s" % full_name
3392
+
3393
+ rp = None
3394
+ try:
3395
+ rp = connection.execution_options(
3396
+ skip_user_error_events=True
3397
+ ).exec_driver_sql(st)
3398
+ except exc.DBAPIError as e:
3399
+ if self._extract_error_code(e.orig) == 1146:
3400
+ raise exc.NoSuchTableError(full_name) from e
3401
+ else:
3402
+ raise
3403
+ row = self._compat_first(rp, charset=charset)
3404
+ if not row:
3405
+ raise exc.NoSuchTableError(full_name)
3406
+ return row[1].strip()
3407
+
3408
+ def _describe_table(self, connection, table, charset=None, full_name=None):
3409
+ """Run DESCRIBE for a ``Table`` and return processed rows."""
3410
+
3411
+ if full_name is None:
3412
+ full_name = self.identifier_preparer.format_table(table)
3413
+ st = "DESCRIBE %s" % full_name
3414
+
3415
+ rp, rows = None, None
3416
+ try:
3417
+ try:
3418
+ rp = connection.execution_options(
3419
+ skip_user_error_events=True
3420
+ ).exec_driver_sql(st)
3421
+ except exc.DBAPIError as e:
3422
+ code = self._extract_error_code(e.orig)
3423
+ if code == 1146:
3424
+ raise exc.NoSuchTableError(full_name) from e
3425
+
3426
+ elif code == 1356:
3427
+ raise exc.UnreflectableTableError(
3428
+ "Table or view named %s could not be "
3429
+ "reflected: %s" % (full_name, e)
3430
+ ) from e
3431
+
3432
+ else:
3433
+ raise
3434
+ rows = self._compat_fetchall(rp, charset=charset)
3435
+ finally:
3436
+ if rp:
3437
+ rp.close()
3438
+ return rows
3439
+
3440
+
3441
+ class _DecodingRow:
3442
+ """Return unicode-decoded values based on type inspection.
3443
+
3444
+ Smooth over data type issues (esp. with alpha driver versions) and
3445
+ normalize strings as Unicode regardless of user-configured driver
3446
+ encoding settings.
3447
+
3448
+ """
3449
+
3450
+ # Some MySQL-python versions can return some columns as
3451
+ # sets.Set(['value']) (seriously) but thankfully that doesn't
3452
+ # seem to come up in DDL queries.
3453
+
3454
+ _encoding_compat = {
3455
+ "koi8r": "koi8_r",
3456
+ "koi8u": "koi8_u",
3457
+ "utf16": "utf-16-be", # MySQL's uft16 is always bigendian
3458
+ "utf8mb4": "utf8", # real utf8
3459
+ "utf8mb3": "utf8", # real utf8; saw this happen on CI but I cannot
3460
+ # reproduce, possibly mariadb10.6 related
3461
+ "eucjpms": "ujis",
3462
+ }
3463
+
3464
+ def __init__(self, rowproxy, charset):
3465
+ self.rowproxy = rowproxy
3466
+ self.charset = self._encoding_compat.get(charset, charset)
3467
+
3468
+ def __getitem__(self, index):
3469
+ item = self.rowproxy[index]
3470
+ if isinstance(item, _array):
3471
+ item = item.tostring()
3472
+
3473
+ if self.charset and isinstance(item, bytes):
3474
+ return item.decode(self.charset)
3475
+ else:
3476
+ return item
3477
+
3478
+ def __getattr__(self, attr):
3479
+ item = getattr(self.rowproxy, attr)
3480
+ if isinstance(item, _array):
3481
+ item = item.tostring()
3482
+ if self.charset and isinstance(item, bytes):
3483
+ return item.decode(self.charset)
3484
+ else:
3485
+ return item
3486
+
3487
+
3488
+ _info_columns = sql.table(
3489
+ "columns",
3490
+ sql.column("table_schema", VARCHAR(64)),
3491
+ sql.column("table_name", VARCHAR(64)),
3492
+ sql.column("column_name", VARCHAR(64)),
3493
+ schema="information_schema",
3494
+ )