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