dml.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. # dialects/postgresql/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 List
  10. from typing import Optional
  11. from typing import Tuple
  12. from typing import Union
  13. from . import ext
  14. from .._typing import _OnConflictConstraintT
  15. from .._typing import _OnConflictIndexElementsT
  16. from .._typing import _OnConflictIndexWhereT
  17. from .._typing import _OnConflictSetT
  18. from .._typing import _OnConflictWhereT
  19. from ... import util
  20. from ...sql import coercions
  21. from ...sql import roles
  22. from ...sql import schema
  23. from ...sql._typing import _DMLTableArgument
  24. from ...sql.base import _exclusive_against
  25. from ...sql.base import _generative
  26. from ...sql.base import ColumnCollection
  27. from ...sql.base import ReadOnlyColumnCollection
  28. from ...sql.dml import Insert as StandardInsert
  29. from ...sql.elements import ClauseElement
  30. from ...sql.elements import ColumnElement
  31. from ...sql.elements import KeyedColumnElement
  32. from ...sql.elements import TextClause
  33. from ...sql.expression import alias
  34. from ...util.typing import Self
  35. __all__ = ("Insert", "insert")
  36. def insert(table: _DMLTableArgument) -> Insert:
  37. """Construct a PostgreSQL-specific variant :class:`_postgresql.Insert`
  38. construct.
  39. .. container:: inherited_member
  40. The :func:`sqlalchemy.dialects.postgresql.insert` function creates
  41. a :class:`sqlalchemy.dialects.postgresql.Insert`. This class is based
  42. on the dialect-agnostic :class:`_sql.Insert` construct which may
  43. be constructed using the :func:`_sql.insert` function in
  44. SQLAlchemy Core.
  45. The :class:`_postgresql.Insert` construct includes additional methods
  46. :meth:`_postgresql.Insert.on_conflict_do_update`,
  47. :meth:`_postgresql.Insert.on_conflict_do_nothing`.
  48. """
  49. return Insert(table)
  50. class Insert(StandardInsert):
  51. """PostgreSQL-specific implementation of INSERT.
  52. Adds methods for PG-specific syntaxes such as ON CONFLICT.
  53. The :class:`_postgresql.Insert` object is created using the
  54. :func:`sqlalchemy.dialects.postgresql.insert` function.
  55. """
  56. stringify_dialect = "postgresql"
  57. inherit_cache = False
  58. @util.memoized_property
  59. def excluded(
  60. self,
  61. ) -> ReadOnlyColumnCollection[str, KeyedColumnElement[Any]]:
  62. """Provide the ``excluded`` namespace for an ON CONFLICT statement
  63. PG's ON CONFLICT clause allows reference to the row that would
  64. be inserted, known as ``excluded``. This attribute provides
  65. all columns in this row to be referenceable.
  66. .. tip:: The :attr:`_postgresql.Insert.excluded` attribute is an
  67. instance of :class:`_expression.ColumnCollection`, which provides
  68. an interface the same as that of the :attr:`_schema.Table.c`
  69. collection described at :ref:`metadata_tables_and_columns`.
  70. With this collection, ordinary names are accessible like attributes
  71. (e.g. ``stmt.excluded.some_column``), but special names and
  72. dictionary method names should be accessed using indexed access,
  73. such as ``stmt.excluded["column name"]`` or
  74. ``stmt.excluded["values"]``. See the docstring for
  75. :class:`_expression.ColumnCollection` for further examples.
  76. .. seealso::
  77. :ref:`postgresql_insert_on_conflict` - example of how
  78. to use :attr:`_expression.Insert.excluded`
  79. """
  80. return alias(self.table, name="excluded").columns
  81. _on_conflict_exclusive = _exclusive_against(
  82. "_post_values_clause",
  83. msgs={
  84. "_post_values_clause": "This Insert construct already has "
  85. "an ON CONFLICT clause established"
  86. },
  87. )
  88. @_generative
  89. @_on_conflict_exclusive
  90. def on_conflict_do_update(
  91. self,
  92. constraint: _OnConflictConstraintT = None,
  93. index_elements: _OnConflictIndexElementsT = None,
  94. index_where: _OnConflictIndexWhereT = None,
  95. set_: _OnConflictSetT = None,
  96. where: _OnConflictWhereT = None,
  97. ) -> Self:
  98. r"""
  99. Specifies a DO UPDATE SET action for ON CONFLICT clause.
  100. Either the ``constraint`` or ``index_elements`` argument is
  101. required, but only one of these can be specified.
  102. :param constraint:
  103. The name of a unique or exclusion constraint on the table,
  104. or the constraint object itself if it has a .name attribute.
  105. :param index_elements:
  106. A sequence consisting of string column names, :class:`_schema.Column`
  107. objects, or other column expression objects that will be used
  108. to infer a target index.
  109. :param index_where:
  110. Additional WHERE criterion that can be used to infer a
  111. conditional target index.
  112. :param set\_:
  113. A dictionary or other mapping object
  114. where the keys are either names of columns in the target table,
  115. or :class:`_schema.Column` objects or other ORM-mapped columns
  116. matching that of the target table, and expressions or literals
  117. as values, specifying the ``SET`` actions to take.
  118. .. versionadded:: 1.4 The
  119. :paramref:`_postgresql.Insert.on_conflict_do_update.set_`
  120. parameter supports :class:`_schema.Column` objects from the target
  121. :class:`_schema.Table` as keys.
  122. .. warning:: This dictionary does **not** take into account
  123. Python-specified default UPDATE values or generation functions,
  124. e.g. those specified using :paramref:`_schema.Column.onupdate`.
  125. These values will not be exercised for an ON CONFLICT style of
  126. UPDATE, unless they are manually specified in the
  127. :paramref:`.Insert.on_conflict_do_update.set_` dictionary.
  128. :param where:
  129. Optional argument. An expression object representing a ``WHERE``
  130. clause that restricts the rows affected by ``DO UPDATE SET``. Rows not
  131. meeting the ``WHERE`` condition will not be updated (effectively a
  132. ``DO NOTHING`` for those rows).
  133. .. seealso::
  134. :ref:`postgresql_insert_on_conflict`
  135. """
  136. self._post_values_clause = OnConflictDoUpdate(
  137. constraint, index_elements, index_where, set_, where
  138. )
  139. return self
  140. @_generative
  141. @_on_conflict_exclusive
  142. def on_conflict_do_nothing(
  143. self,
  144. constraint: _OnConflictConstraintT = None,
  145. index_elements: _OnConflictIndexElementsT = None,
  146. index_where: _OnConflictIndexWhereT = None,
  147. ) -> Self:
  148. """
  149. Specifies a DO NOTHING action for ON CONFLICT clause.
  150. The ``constraint`` and ``index_elements`` arguments
  151. are optional, but only one of these can be specified.
  152. :param constraint:
  153. The name of a unique or exclusion constraint on the table,
  154. or the constraint object itself if it has a .name attribute.
  155. :param index_elements:
  156. A sequence consisting of string column names, :class:`_schema.Column`
  157. objects, or other column expression objects that will be used
  158. to infer a target index.
  159. :param index_where:
  160. Additional WHERE criterion that can be used to infer a
  161. conditional target index.
  162. .. seealso::
  163. :ref:`postgresql_insert_on_conflict`
  164. """
  165. self._post_values_clause = OnConflictDoNothing(
  166. constraint, index_elements, index_where
  167. )
  168. return self
  169. class OnConflictClause(ClauseElement):
  170. stringify_dialect = "postgresql"
  171. constraint_target: Optional[str]
  172. inferred_target_elements: Optional[List[Union[str, schema.Column[Any]]]]
  173. inferred_target_whereclause: Optional[
  174. Union[ColumnElement[Any], TextClause]
  175. ]
  176. def __init__(
  177. self,
  178. constraint: _OnConflictConstraintT = None,
  179. index_elements: _OnConflictIndexElementsT = None,
  180. index_where: _OnConflictIndexWhereT = None,
  181. ):
  182. if constraint is not None:
  183. if not isinstance(constraint, str) and isinstance(
  184. constraint,
  185. (schema.Constraint, ext.ExcludeConstraint),
  186. ):
  187. constraint = getattr(constraint, "name") or constraint
  188. if constraint is not None:
  189. if index_elements is not None:
  190. raise ValueError(
  191. "'constraint' and 'index_elements' are mutually exclusive"
  192. )
  193. if isinstance(constraint, str):
  194. self.constraint_target = constraint
  195. self.inferred_target_elements = None
  196. self.inferred_target_whereclause = None
  197. elif isinstance(constraint, schema.Index):
  198. index_elements = constraint.expressions
  199. index_where = constraint.dialect_options["postgresql"].get(
  200. "where"
  201. )
  202. elif isinstance(constraint, ext.ExcludeConstraint):
  203. index_elements = constraint.columns
  204. index_where = constraint.where
  205. else:
  206. index_elements = constraint.columns
  207. index_where = constraint.dialect_options["postgresql"].get(
  208. "where"
  209. )
  210. if index_elements is not None:
  211. self.constraint_target = None
  212. self.inferred_target_elements = [
  213. coercions.expect(roles.DDLConstraintColumnRole, column)
  214. for column in index_elements
  215. ]
  216. self.inferred_target_whereclause = (
  217. coercions.expect(
  218. (
  219. roles.StatementOptionRole
  220. if isinstance(constraint, ext.ExcludeConstraint)
  221. else roles.WhereHavingRole
  222. ),
  223. index_where,
  224. )
  225. if index_where is not None
  226. else None
  227. )
  228. elif constraint is None:
  229. self.constraint_target = self.inferred_target_elements = (
  230. self.inferred_target_whereclause
  231. ) = None
  232. class OnConflictDoNothing(OnConflictClause):
  233. __visit_name__ = "on_conflict_do_nothing"
  234. class OnConflictDoUpdate(OnConflictClause):
  235. __visit_name__ = "on_conflict_do_update"
  236. update_values_to_set: List[Tuple[Union[schema.Column[Any], str], Any]]
  237. update_whereclause: Optional[ColumnElement[Any]]
  238. def __init__(
  239. self,
  240. constraint: _OnConflictConstraintT = None,
  241. index_elements: _OnConflictIndexElementsT = None,
  242. index_where: _OnConflictIndexWhereT = None,
  243. set_: _OnConflictSetT = None,
  244. where: _OnConflictWhereT = None,
  245. ):
  246. super().__init__(
  247. constraint=constraint,
  248. index_elements=index_elements,
  249. index_where=index_where,
  250. )
  251. if (
  252. self.inferred_target_elements is None
  253. and self.constraint_target is None
  254. ):
  255. raise ValueError(
  256. "Either constraint or index_elements, "
  257. "but not both, must be specified unless DO NOTHING"
  258. )
  259. if isinstance(set_, dict):
  260. if not set_:
  261. raise ValueError("set parameter dictionary must not be empty")
  262. elif isinstance(set_, ColumnCollection):
  263. set_ = dict(set_)
  264. else:
  265. raise ValueError(
  266. "set parameter must be a non-empty dictionary "
  267. "or a ColumnCollection such as the `.c.` collection "
  268. "of a Table object"
  269. )
  270. self.update_values_to_set = [
  271. (coercions.expect(roles.DMLColumnRole, key), value)
  272. for key, value in set_.items()
  273. ]
  274. self.update_whereclause = (
  275. coercions.expect(roles.WhereHavingRole, where)
  276. if where is not None
  277. else None
  278. )