dml.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. # dialects/mysql/dml.py
  2. # Copyright (C) 2005-2025 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. from __future__ import annotations
  8. from typing import Any
  9. from typing import Dict
  10. from typing import List
  11. from typing import Mapping
  12. from typing import Optional
  13. from typing import Tuple
  14. from typing import Union
  15. from ... import exc
  16. from ... import util
  17. from ...sql._typing import _DMLTableArgument
  18. from ...sql.base import _exclusive_against
  19. from ...sql.base import _generative
  20. from ...sql.base import ColumnCollection
  21. from ...sql.base import ReadOnlyColumnCollection
  22. from ...sql.dml import Insert as StandardInsert
  23. from ...sql.elements import ClauseElement
  24. from ...sql.elements import KeyedColumnElement
  25. from ...sql.expression import alias
  26. from ...sql.selectable import NamedFromClause
  27. from ...util.typing import Self
  28. __all__ = ("Insert", "insert")
  29. def insert(table: _DMLTableArgument) -> Insert:
  30. """Construct a MySQL/MariaDB-specific variant :class:`_mysql.Insert`
  31. construct.
  32. .. container:: inherited_member
  33. The :func:`sqlalchemy.dialects.mysql.insert` function creates
  34. a :class:`sqlalchemy.dialects.mysql.Insert`. This class is based
  35. on the dialect-agnostic :class:`_sql.Insert` construct which may
  36. be constructed using the :func:`_sql.insert` function in
  37. SQLAlchemy Core.
  38. The :class:`_mysql.Insert` construct includes additional methods
  39. :meth:`_mysql.Insert.on_duplicate_key_update`.
  40. """
  41. return Insert(table)
  42. class Insert(StandardInsert):
  43. """MySQL-specific implementation of INSERT.
  44. Adds methods for MySQL-specific syntaxes such as ON DUPLICATE KEY UPDATE.
  45. The :class:`~.mysql.Insert` object is created using the
  46. :func:`sqlalchemy.dialects.mysql.insert` function.
  47. .. versionadded:: 1.2
  48. """
  49. stringify_dialect = "mysql"
  50. inherit_cache = False
  51. @property
  52. def inserted(
  53. self,
  54. ) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]:
  55. """Provide the "inserted" namespace for an ON DUPLICATE KEY UPDATE
  56. statement
  57. MySQL's ON DUPLICATE KEY UPDATE clause allows reference to the row
  58. that would be inserted, via a special function called ``VALUES()``.
  59. This attribute provides all columns in this row to be referenceable
  60. such that they will render within a ``VALUES()`` function inside the
  61. ON DUPLICATE KEY UPDATE clause. The attribute is named ``.inserted``
  62. so as not to conflict with the existing
  63. :meth:`_expression.Insert.values` method.
  64. .. tip:: The :attr:`_mysql.Insert.inserted` attribute is an instance
  65. of :class:`_expression.ColumnCollection`, which provides an
  66. interface the same as that of the :attr:`_schema.Table.c`
  67. collection described at :ref:`metadata_tables_and_columns`.
  68. With this collection, ordinary names are accessible like attributes
  69. (e.g. ``stmt.inserted.some_column``), but special names and
  70. dictionary method names should be accessed using indexed access,
  71. such as ``stmt.inserted["column name"]`` or
  72. ``stmt.inserted["values"]``. See the docstring for
  73. :class:`_expression.ColumnCollection` for further examples.
  74. .. seealso::
  75. :ref:`mysql_insert_on_duplicate_key_update` - example of how
  76. to use :attr:`_expression.Insert.inserted`
  77. """
  78. return self.inserted_alias.columns
  79. @util.memoized_property
  80. def inserted_alias(self) -> NamedFromClause:
  81. return alias(self.table, name="inserted")
  82. @_generative
  83. @_exclusive_against(
  84. "_post_values_clause",
  85. msgs={
  86. "_post_values_clause": "This Insert construct already "
  87. "has an ON DUPLICATE KEY clause present"
  88. },
  89. )
  90. def on_duplicate_key_update(self, *args: _UpdateArg, **kw: Any) -> Self:
  91. r"""
  92. Specifies the ON DUPLICATE KEY UPDATE clause.
  93. :param \**kw: Column keys linked to UPDATE values. The
  94. values may be any SQL expression or supported literal Python
  95. values.
  96. .. warning:: This dictionary does **not** take into account
  97. Python-specified default UPDATE values or generation functions,
  98. e.g. those specified using :paramref:`_schema.Column.onupdate`.
  99. These values will not be exercised for an ON DUPLICATE KEY UPDATE
  100. style of UPDATE, unless values are manually specified here.
  101. :param \*args: As an alternative to passing key/value parameters,
  102. a dictionary or list of 2-tuples can be passed as a single positional
  103. argument.
  104. Passing a single dictionary is equivalent to the keyword argument
  105. form::
  106. insert().on_duplicate_key_update({"name": "some name"})
  107. Passing a list of 2-tuples indicates that the parameter assignments
  108. in the UPDATE clause should be ordered as sent, in a manner similar
  109. to that described for the :class:`_expression.Update`
  110. construct overall
  111. in :ref:`tutorial_parameter_ordered_updates`::
  112. insert().on_duplicate_key_update(
  113. [
  114. ("name", "some name"),
  115. ("value", "some value"),
  116. ]
  117. )
  118. .. versionchanged:: 1.3 parameters can be specified as a dictionary
  119. or list of 2-tuples; the latter form provides for parameter
  120. ordering.
  121. .. versionadded:: 1.2
  122. .. seealso::
  123. :ref:`mysql_insert_on_duplicate_key_update`
  124. """
  125. if args and kw:
  126. raise exc.ArgumentError(
  127. "Can't pass kwargs and positional arguments simultaneously"
  128. )
  129. if args:
  130. if len(args) > 1:
  131. raise exc.ArgumentError(
  132. "Only a single dictionary or list of tuples "
  133. "is accepted positionally."
  134. )
  135. values = args[0]
  136. else:
  137. values = kw
  138. self._post_values_clause = OnDuplicateClause(
  139. self.inserted_alias, values
  140. )
  141. return self
  142. class OnDuplicateClause(ClauseElement):
  143. __visit_name__ = "on_duplicate_key_update"
  144. _parameter_ordering: Optional[List[str]] = None
  145. update: Dict[str, Any]
  146. stringify_dialect = "mysql"
  147. def __init__(
  148. self, inserted_alias: NamedFromClause, update: _UpdateArg
  149. ) -> None:
  150. self.inserted_alias = inserted_alias
  151. # auto-detect that parameters should be ordered. This is copied from
  152. # Update._proces_colparams(), however we don't look for a special flag
  153. # in this case since we are not disambiguating from other use cases as
  154. # we are in Update.values().
  155. if isinstance(update, list) and (
  156. update and isinstance(update[0], tuple)
  157. ):
  158. self._parameter_ordering = [key for key, value in update]
  159. update = dict(update)
  160. if isinstance(update, dict):
  161. if not update:
  162. raise ValueError(
  163. "update parameter dictionary must not be empty"
  164. )
  165. elif isinstance(update, ColumnCollection):
  166. update = dict(update)
  167. else:
  168. raise ValueError(
  169. "update parameter must be a non-empty dictionary "
  170. "or a ColumnCollection such as the `.c.` collection "
  171. "of a Table object"
  172. )
  173. self.update = update
  174. _UpdateArg = Union[
  175. Mapping[Any, Any], List[Tuple[str, Any]], ColumnCollection[Any, Any]
  176. ]