You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
9.3KB

  1. # postgresql/on_conflict.py
  2. # Copyright (C) 2005-2021 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: http://www.opensource.org/licenses/mit-license.php
  7. from . import ext
  8. from ... import util
  9. from ...sql import coercions
  10. from ...sql import roles
  11. from ...sql import schema
  12. from ...sql.base import _exclusive_against
  13. from ...sql.base import _generative
  14. from ...sql.base import ColumnCollection
  15. from ...sql.dml import Insert as StandardInsert
  16. from ...sql.elements import ClauseElement
  17. from ...sql.expression import alias
  18. from ...util.langhelpers import public_factory
  19. __all__ = ("Insert", "insert")
  20. class Insert(StandardInsert):
  21. """PostgreSQL-specific implementation of INSERT.
  22. Adds methods for PG-specific syntaxes such as ON CONFLICT.
  23. The :class:`_postgresql.Insert` object is created using the
  24. :func:`sqlalchemy.dialects.postgresql.insert` function.
  25. .. versionadded:: 1.1
  26. """
  27. stringify_dialect = "postgresql"
  28. @util.memoized_property
  29. def excluded(self):
  30. """Provide the ``excluded`` namespace for an ON CONFLICT statement
  31. PG's ON CONFLICT clause allows reference to the row that would
  32. be inserted, known as ``excluded``. This attribute provides
  33. all columns in this row to be referenceable.
  34. .. tip:: The :attr:`_postgresql.Insert.excluded` attribute is an
  35. instance of :class:`_expression.ColumnCollection`, which provides
  36. an interface the same as that of the :attr:`_schema.Table.c`
  37. collection described at :ref:`metadata_tables_and_columns`.
  38. With this collection, ordinary names are accessible like attributes
  39. (e.g. ``stmt.excluded.some_column``), but special names and
  40. dictionary method names should be accessed using indexed access,
  41. such as ``stmt.excluded["column name"]`` or
  42. ``stmt.excluded["values"]``. See the docstring for
  43. :class:`_expression.ColumnCollection` for further examples.
  44. .. seealso::
  45. :ref:`postgresql_insert_on_conflict` - example of how
  46. to use :attr:`_expression.Insert.excluded`
  47. """
  48. return alias(self.table, name="excluded").columns
  49. _on_conflict_exclusive = _exclusive_against(
  50. "_post_values_clause",
  51. msgs={
  52. "_post_values_clause": "This Insert construct already has "
  53. "an ON CONFLICT clause established"
  54. },
  55. )
  56. @_generative
  57. @_on_conflict_exclusive
  58. def on_conflict_do_update(
  59. self,
  60. constraint=None,
  61. index_elements=None,
  62. index_where=None,
  63. set_=None,
  64. where=None,
  65. ):
  66. r"""
  67. Specifies a DO UPDATE SET action for ON CONFLICT clause.
  68. Either the ``constraint`` or ``index_elements`` argument is
  69. required, but only one of these can be specified.
  70. :param constraint:
  71. The name of a unique or exclusion constraint on the table,
  72. or the constraint object itself if it has a .name attribute.
  73. :param index_elements:
  74. A sequence consisting of string column names, :class:`_schema.Column`
  75. objects, or other column expression objects that will be used
  76. to infer a target index.
  77. :param index_where:
  78. Additional WHERE criterion that can be used to infer a
  79. conditional target index.
  80. :param set\_:
  81. A dictionary or other mapping object
  82. where the keys are either names of columns in the target table,
  83. or :class:`_schema.Column` objects or other ORM-mapped columns
  84. matching that of the target table, and expressions or literals
  85. as values, specifying the ``SET`` actions to take.
  86. .. versionadded:: 1.4 The
  87. :paramref:`_postgresql.Insert.on_conflict_do_update.set_`
  88. parameter supports :class:`_schema.Column` objects from the target
  89. :class:`_schema.Table` as keys.
  90. .. warning:: This dictionary does **not** take into account
  91. Python-specified default UPDATE values or generation functions,
  92. e.g. those specified using :paramref:`_schema.Column.onupdate`.
  93. These values will not be exercised for an ON CONFLICT style of
  94. UPDATE, unless they are manually specified in the
  95. :paramref:`.Insert.on_conflict_do_update.set_` dictionary.
  96. :param where:
  97. Optional argument. If present, can be a literal SQL
  98. string or an acceptable expression for a ``WHERE`` clause
  99. that restricts the rows affected by ``DO UPDATE SET``. Rows
  100. not meeting the ``WHERE`` condition will not be updated
  101. (effectively a ``DO NOTHING`` for those rows).
  102. .. versionadded:: 1.1
  103. .. seealso::
  104. :ref:`postgresql_insert_on_conflict`
  105. """
  106. self._post_values_clause = OnConflictDoUpdate(
  107. constraint, index_elements, index_where, set_, where
  108. )
  109. @_generative
  110. @_on_conflict_exclusive
  111. def on_conflict_do_nothing(
  112. self, constraint=None, index_elements=None, index_where=None
  113. ):
  114. """
  115. Specifies a DO NOTHING action for ON CONFLICT clause.
  116. The ``constraint`` and ``index_elements`` arguments
  117. are optional, but only one of these can be specified.
  118. :param constraint:
  119. The name of a unique or exclusion constraint on the table,
  120. or the constraint object itself if it has a .name attribute.
  121. :param index_elements:
  122. A sequence consisting of string column names, :class:`_schema.Column`
  123. objects, or other column expression objects that will be used
  124. to infer a target index.
  125. :param index_where:
  126. Additional WHERE criterion that can be used to infer a
  127. conditional target index.
  128. .. versionadded:: 1.1
  129. .. seealso::
  130. :ref:`postgresql_insert_on_conflict`
  131. """
  132. self._post_values_clause = OnConflictDoNothing(
  133. constraint, index_elements, index_where
  134. )
  135. insert = public_factory(
  136. Insert, ".dialects.postgresql.insert", ".dialects.postgresql.Insert"
  137. )
  138. class OnConflictClause(ClauseElement):
  139. stringify_dialect = "postgresql"
  140. def __init__(self, constraint=None, index_elements=None, index_where=None):
  141. if constraint is not None:
  142. if not isinstance(constraint, util.string_types) and isinstance(
  143. constraint,
  144. (schema.Index, schema.Constraint, ext.ExcludeConstraint),
  145. ):
  146. constraint = getattr(constraint, "name") or constraint
  147. if constraint is not None:
  148. if index_elements is not None:
  149. raise ValueError(
  150. "'constraint' and 'index_elements' are mutually exclusive"
  151. )
  152. if isinstance(constraint, util.string_types):
  153. self.constraint_target = constraint
  154. self.inferred_target_elements = None
  155. self.inferred_target_whereclause = None
  156. elif isinstance(constraint, schema.Index):
  157. index_elements = constraint.expressions
  158. index_where = constraint.dialect_options["postgresql"].get(
  159. "where"
  160. )
  161. elif isinstance(constraint, ext.ExcludeConstraint):
  162. index_elements = constraint.columns
  163. index_where = constraint.where
  164. else:
  165. index_elements = constraint.columns
  166. index_where = constraint.dialect_options["postgresql"].get(
  167. "where"
  168. )
  169. if index_elements is not None:
  170. self.constraint_target = None
  171. self.inferred_target_elements = index_elements
  172. self.inferred_target_whereclause = index_where
  173. elif constraint is None:
  174. self.constraint_target = (
  175. self.inferred_target_elements
  176. ) = self.inferred_target_whereclause = None
  177. class OnConflictDoNothing(OnConflictClause):
  178. __visit_name__ = "on_conflict_do_nothing"
  179. class OnConflictDoUpdate(OnConflictClause):
  180. __visit_name__ = "on_conflict_do_update"
  181. def __init__(
  182. self,
  183. constraint=None,
  184. index_elements=None,
  185. index_where=None,
  186. set_=None,
  187. where=None,
  188. ):
  189. super(OnConflictDoUpdate, self).__init__(
  190. constraint=constraint,
  191. index_elements=index_elements,
  192. index_where=index_where,
  193. )
  194. if (
  195. self.inferred_target_elements is None
  196. and self.constraint_target is None
  197. ):
  198. raise ValueError(
  199. "Either constraint or index_elements, "
  200. "but not both, must be specified unless DO NOTHING"
  201. )
  202. if isinstance(set_, dict):
  203. if not set_:
  204. raise ValueError("set parameter dictionary must not be empty")
  205. elif isinstance(set_, ColumnCollection):
  206. set_ = dict(set_)
  207. else:
  208. raise ValueError(
  209. "set parameter must be a non-empty dictionary "
  210. "or a ColumnCollection such as the `.c.` collection "
  211. "of a Table object"
  212. )
  213. self.update_values_to_set = [
  214. (coercions.expect(roles.DMLColumnRole, key), value)
  215. for key, value in set_.items()
  216. ]
  217. self.update_whereclause = where