1# orm/persistence.py
2# Copyright (C) 2005-2026 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"""private module containing functions used to emit INSERT, UPDATE
11and DELETE statements on behalf of a :class:`_orm.Mapper` and its descending
12mappers.
13
14The functions here are called only by the unit of work functions
15in unitofwork.py.
16
17"""
18
19from __future__ import annotations
20
21from itertools import chain
22from itertools import groupby
23from itertools import zip_longest
24import operator
25
26from . import attributes
27from . import exc as orm_exc
28from . import loading
29from . import sync
30from .base import state_str
31from .. import exc as sa_exc
32from .. import sql
33from .. import util
34from ..engine import cursor as _cursor
35from ..sql import operators
36from ..sql.elements import BooleanClauseList
37
38
39def _save_obj(base_mapper, states, uowtransaction, single=False):
40 """Issue ``INSERT`` and/or ``UPDATE`` statements for a list
41 of objects.
42
43 This is called within the context of a UOWTransaction during a
44 flush operation, given a list of states to be flushed. The
45 base mapper in an inheritance hierarchy handles the inserts/
46 updates for all descendant mappers.
47
48 """
49
50 # if batch=false, call _save_obj separately for each object
51 if not single and not base_mapper.batch:
52 for state in _sort_states(base_mapper, states):
53 _save_obj(base_mapper, [state], uowtransaction, single=True)
54 return
55
56 states_to_update = []
57 states_to_insert = []
58
59 for (
60 state,
61 dict_,
62 mapper,
63 connection,
64 has_identity,
65 row_switch,
66 update_version_id,
67 ) in _organize_states_for_save(base_mapper, states, uowtransaction):
68 if has_identity or row_switch:
69 states_to_update.append(
70 (state, dict_, mapper, connection, update_version_id)
71 )
72 else:
73 states_to_insert.append((state, dict_, mapper, connection))
74
75 for table, mapper in base_mapper._sorted_tables.items():
76 if table not in mapper._pks_by_table:
77 continue
78 insert = _collect_insert_commands(table, states_to_insert)
79
80 update = _collect_update_commands(
81 uowtransaction, table, states_to_update
82 )
83
84 _emit_update_statements(
85 base_mapper,
86 uowtransaction,
87 mapper,
88 table,
89 update,
90 )
91
92 _emit_insert_statements(
93 base_mapper,
94 uowtransaction,
95 mapper,
96 table,
97 insert,
98 )
99
100 _finalize_insert_update_commands(
101 base_mapper,
102 uowtransaction,
103 chain(
104 (
105 (state, state_dict, mapper, connection, False)
106 for (state, state_dict, mapper, connection) in states_to_insert
107 ),
108 (
109 (state, state_dict, mapper, connection, True)
110 for (
111 state,
112 state_dict,
113 mapper,
114 connection,
115 update_version_id,
116 ) in states_to_update
117 ),
118 ),
119 )
120
121
122def _post_update(base_mapper, states, uowtransaction, post_update_cols):
123 """Issue UPDATE statements on behalf of a relationship() which
124 specifies post_update.
125
126 """
127
128 states_to_update = list(
129 _organize_states_for_post_update(base_mapper, states, uowtransaction)
130 )
131
132 for table, mapper in base_mapper._sorted_tables.items():
133 if table not in mapper._pks_by_table:
134 continue
135
136 update = (
137 (
138 state,
139 state_dict,
140 sub_mapper,
141 connection,
142 (
143 mapper._get_committed_state_attr_by_column(
144 state, state_dict, mapper.version_id_col
145 )
146 if mapper.version_id_col is not None
147 else None
148 ),
149 )
150 for state, state_dict, sub_mapper, connection in states_to_update
151 if table in sub_mapper._pks_by_table
152 )
153
154 update = _collect_post_update_commands(
155 base_mapper, uowtransaction, table, update, post_update_cols
156 )
157
158 _emit_post_update_statements(
159 base_mapper,
160 uowtransaction,
161 mapper,
162 table,
163 update,
164 )
165
166
167def _delete_obj(base_mapper, states, uowtransaction):
168 """Issue ``DELETE`` statements for a list of objects.
169
170 This is called within the context of a UOWTransaction during a
171 flush operation.
172
173 """
174
175 states_to_delete = list(
176 _organize_states_for_delete(base_mapper, states, uowtransaction)
177 )
178
179 table_to_mapper = base_mapper._sorted_tables
180
181 for table in reversed(list(table_to_mapper.keys())):
182 mapper = table_to_mapper[table]
183 if table not in mapper._pks_by_table:
184 continue
185 elif mapper.inherits and mapper.passive_deletes:
186 continue
187
188 delete = _collect_delete_commands(
189 base_mapper, uowtransaction, table, states_to_delete
190 )
191
192 _emit_delete_statements(
193 base_mapper,
194 uowtransaction,
195 mapper,
196 table,
197 delete,
198 )
199
200 for (
201 state,
202 state_dict,
203 mapper,
204 connection,
205 update_version_id,
206 ) in states_to_delete:
207 mapper.dispatch.after_delete(mapper, connection, state)
208
209
210def _organize_states_for_save(base_mapper, states, uowtransaction):
211 """Make an initial pass across a set of states for INSERT or
212 UPDATE.
213
214 This includes splitting out into distinct lists for
215 each, calling before_insert/before_update, obtaining
216 key information for each state including its dictionary,
217 mapper, the connection to use for the execution per state,
218 and the identity flag.
219
220 """
221
222 for state, dict_, mapper, connection in _connections_for_states(
223 base_mapper, uowtransaction, states
224 ):
225 has_identity = bool(state.key)
226
227 instance_key = state.key or mapper._identity_key_from_state(state)
228
229 row_switch = update_version_id = None
230
231 # call before_XXX extensions
232 if not has_identity:
233 mapper.dispatch.before_insert(mapper, connection, state)
234 else:
235 mapper.dispatch.before_update(mapper, connection, state)
236
237 if mapper._validate_polymorphic_identity:
238 mapper._validate_polymorphic_identity(mapper, state, dict_)
239
240 # detect if we have a "pending" instance (i.e. has
241 # no instance_key attached to it), and another instance
242 # with the same identity key already exists as persistent.
243 # convert to an UPDATE if so.
244 if (
245 not has_identity
246 and instance_key in uowtransaction.session.identity_map
247 ):
248 instance = uowtransaction.session.identity_map[instance_key]
249 existing = attributes.instance_state(instance)
250
251 if not uowtransaction.was_already_deleted(existing):
252 if not uowtransaction.is_deleted(existing):
253 util.warn(
254 "New instance %s with identity key %s conflicts "
255 "with persistent instance %s"
256 % (state_str(state), instance_key, state_str(existing))
257 )
258 else:
259 base_mapper._log_debug(
260 "detected row switch for identity %s. "
261 "will update %s, remove %s from "
262 "transaction",
263 instance_key,
264 state_str(state),
265 state_str(existing),
266 )
267
268 # remove the "delete" flag from the existing element
269 uowtransaction.remove_state_actions(existing)
270 row_switch = existing
271
272 if (has_identity or row_switch) and mapper.version_id_col is not None:
273 update_version_id = mapper._get_committed_state_attr_by_column(
274 row_switch if row_switch else state,
275 row_switch.dict if row_switch else dict_,
276 mapper.version_id_col,
277 )
278
279 yield (
280 state,
281 dict_,
282 mapper,
283 connection,
284 has_identity,
285 row_switch,
286 update_version_id,
287 )
288
289
290def _organize_states_for_post_update(base_mapper, states, uowtransaction):
291 """Make an initial pass across a set of states for UPDATE
292 corresponding to post_update.
293
294 This includes obtaining key information for each state
295 including its dictionary, mapper, the connection to use for
296 the execution per state.
297
298 """
299 return _connections_for_states(base_mapper, uowtransaction, states)
300
301
302def _organize_states_for_delete(base_mapper, states, uowtransaction):
303 """Make an initial pass across a set of states for DELETE.
304
305 This includes calling out before_delete and obtaining
306 key information for each state including its dictionary,
307 mapper, the connection to use for the execution per state.
308
309 """
310 for state, dict_, mapper, connection in _connections_for_states(
311 base_mapper, uowtransaction, states
312 ):
313 mapper.dispatch.before_delete(mapper, connection, state)
314
315 if mapper.version_id_col is not None:
316 update_version_id = mapper._get_committed_state_attr_by_column(
317 state, dict_, mapper.version_id_col
318 )
319 else:
320 update_version_id = None
321
322 yield (state, dict_, mapper, connection, update_version_id)
323
324
325def _set_insert_value(col, value, params, value_params):
326 """Place a value for an INSERT into either ``value_params``, if it is a
327 SQL expression, or ``params``, if it is a plain value.
328
329 """
330 if hasattr(value, "__clause_element__"):
331 value_params[col] = value.__clause_element__()
332 elif isinstance(value, sql.ClauseElement):
333 value_params[col] = value
334 else:
335 params[col.key] = value
336
337
338def _collect_insert_commands(
339 table,
340 states_to_insert,
341 *,
342 bulk=False,
343 return_defaults=False,
344 render_nulls=False,
345 include_bulk_keys=(),
346):
347 """Identify sets of values to use in INSERT statements for a
348 list of states.
349
350 """
351 for state, state_dict, mapper, connection in states_to_insert:
352 if table not in mapper._pks_by_table:
353 continue
354
355 params = {}
356 value_params = {}
357
358 propkey_to_col = mapper._propkey_to_col[table]
359
360 eval_none = mapper._insert_cols_evaluating_none[table]
361
362 for propkey in set(propkey_to_col).intersection(state_dict):
363 value = state_dict[propkey]
364 col = propkey_to_col[propkey]
365 if value is None and col not in eval_none and not render_nulls:
366 continue
367 elif bulk:
368 params[col.key] = value
369 else:
370 _set_insert_value(col, value, params, value_params)
371
372 if not bulk:
373 # ORM-level defaults, i.e. dataclass defaults, for columns that
374 # have no value, where the column's own default does not
375 # deliver the value, #13748
376 for propkey, col, value in mapper._insert_orm_defaults[table]:
377 if col.key not in params and col not in value_params:
378 _set_insert_value(col, value, params, value_params)
379
380 # for all the columns that have no default and we don't have
381 # a value and where "None" is not a special value, add
382 # explicit None to the INSERT. This is a legacy behavior
383 # which might be worth removing, as it should not be necessary
384 # and also produces confusion, given that "missing" and None
385 # now have distinct meanings
386 for colkey in (
387 mapper._insert_cols_as_none[table]
388 .difference(params)
389 .difference([c.key for c in value_params])
390 ):
391 params[colkey] = None
392
393 if not bulk or return_defaults:
394 # params are in terms of Column key objects, so
395 # compare to pk_keys_by_table
396 has_all_pks = mapper._pk_keys_by_table[table].issubset(params)
397
398 if mapper.base_mapper._prefer_eager_defaults(
399 connection.dialect, table
400 ):
401 has_all_defaults = mapper._server_default_col_keys[
402 table
403 ].issubset(params)
404 else:
405 has_all_defaults = True
406 else:
407 has_all_defaults = has_all_pks = True
408
409 if (
410 mapper.version_id_generator is not False
411 and mapper.version_id_col is not None
412 and mapper.version_id_col in mapper._cols_by_table[table]
413 ):
414 params[mapper.version_id_col.key] = mapper.version_id_generator(
415 None
416 )
417
418 if bulk:
419 if mapper._set_polymorphic_identity:
420 params.setdefault(
421 mapper._polymorphic_attr_key, mapper.polymorphic_identity
422 )
423
424 if include_bulk_keys:
425 params.update((k, state_dict[k]) for k in include_bulk_keys)
426
427 yield (
428 state,
429 state_dict,
430 params,
431 mapper,
432 connection,
433 value_params,
434 has_all_pks,
435 has_all_defaults,
436 )
437
438
439def _collect_update_commands(
440 uowtransaction,
441 table,
442 states_to_update,
443 *,
444 bulk=False,
445 use_orm_update_stmt=None,
446 include_bulk_keys=(),
447):
448 """Identify sets of values to use in UPDATE statements for a
449 list of states.
450
451 This function works intricately with the history system
452 to determine exactly what values should be updated
453 as well as how the row should be matched within an UPDATE
454 statement. Includes some tricky scenarios where the primary
455 key of an object might have been changed.
456
457 """
458
459 for (
460 state,
461 state_dict,
462 mapper,
463 connection,
464 update_version_id,
465 ) in states_to_update:
466 if table not in mapper._pks_by_table:
467 continue
468
469 pks = mapper._pks_by_table[table]
470
471 if (
472 use_orm_update_stmt is not None
473 and not use_orm_update_stmt._maintain_values_ordering
474 ):
475 # TODO: ordered values, etc
476 # ORM bulk_persistence will raise for the maintain_values_ordering
477 # case right now
478 value_params = use_orm_update_stmt._values
479 else:
480 value_params = {}
481
482 propkey_to_col = mapper._propkey_to_col[table]
483
484 if bulk:
485 # keys here are mapped attribute keys, so
486 # look at mapper attribute keys for pk
487 params = {
488 propkey_to_col[propkey].key: state_dict[propkey]
489 for propkey in set(propkey_to_col)
490 .intersection(state_dict)
491 .difference(mapper._pk_attr_keys_by_table[table])
492 }
493 has_all_defaults = True
494 else:
495 params = {}
496 for propkey in set(propkey_to_col).intersection(
497 state.committed_state
498 ):
499 value = state_dict[propkey]
500 col = propkey_to_col[propkey]
501
502 if hasattr(value, "__clause_element__") or isinstance(
503 value, sql.ClauseElement
504 ):
505 value_params[col] = (
506 value.__clause_element__()
507 if hasattr(value, "__clause_element__")
508 else value
509 )
510 # guard against values that generate non-__nonzero__
511 # objects for __eq__()
512 elif (
513 state.manager[propkey].impl.is_equal(
514 value, state.committed_state[propkey]
515 )
516 is not True
517 ):
518 params[col.key] = value
519
520 if mapper.base_mapper.eager_defaults is True:
521 has_all_defaults = (
522 mapper._server_onupdate_default_col_keys[table]
523 ).issubset(params)
524 else:
525 has_all_defaults = True
526
527 if (
528 update_version_id is not None
529 and mapper.version_id_col in mapper._cols_by_table[table]
530 ):
531 if not bulk and not (params or value_params):
532 # HACK: check for history in other tables, in case the
533 # history is only in a different table than the one
534 # where the version_id_col is. This logic was lost
535 # from 0.9 -> 1.0.0 and restored in 1.0.6.
536 for prop in mapper._columntoproperty.values():
537 history = state.manager[prop.key].impl.get_history(
538 state, state_dict, attributes.PASSIVE_NO_INITIALIZE
539 )
540 if history.added:
541 break
542 else:
543 # no net change, break
544 continue
545
546 col = mapper.version_id_col
547 no_params = not params and not value_params
548 params[col._label] = update_version_id
549
550 if (
551 bulk or col.key not in params
552 ) and mapper.version_id_generator is not False:
553 val = mapper.version_id_generator(update_version_id)
554 params[col.key] = val
555 elif mapper.version_id_generator is False and no_params:
556 # no version id generator, no values set on the table,
557 # and version id wasn't manually incremented.
558 # set version id to itself so we get an UPDATE
559 # statement
560 params[col.key] = update_version_id
561
562 elif not (params or value_params):
563 continue
564
565 has_all_pks = True
566 expect_pk_cascaded = False
567 if bulk:
568 # keys here are mapped attribute keys, so
569 # look at mapper attribute keys for pk
570 pk_params = {
571 propkey_to_col[propkey]._label: state_dict.get(propkey)
572 for propkey in set(propkey_to_col).intersection(
573 mapper._pk_attr_keys_by_table[table]
574 )
575 }
576 if util.NONE_SET.intersection(pk_params.values()):
577 raise sa_exc.InvalidRequestError(
578 f"No primary key value supplied for column(s) "
579 f"""{
580 ', '.join(
581 str(c) for c in pks if pk_params[c._label] is None
582 )
583 }; """
584 "per-row ORM Bulk UPDATE by Primary Key requires that "
585 "records contain primary key values",
586 code="bupq",
587 )
588
589 else:
590 pk_params = {}
591 for col in pks:
592 propkey = mapper._columntoproperty[col].key
593
594 history = state.manager[propkey].impl.get_history(
595 state, state_dict, attributes.PASSIVE_OFF
596 )
597
598 if history.added:
599 if (
600 not history.deleted
601 or ("pk_cascaded", state, col)
602 in uowtransaction.attributes
603 ):
604 expect_pk_cascaded = True
605 pk_params[col._label] = history.added[0]
606 params.pop(col.key, None)
607 else:
608 # else, use the old value to locate the row
609 pk_params[col._label] = history.deleted[0]
610 if col in value_params:
611 has_all_pks = False
612 else:
613 pk_params[col._label] = history.unchanged[0]
614 if pk_params[col._label] is None:
615 raise orm_exc.FlushError(
616 "Can't update table %s using NULL for primary "
617 "key value on column %s" % (table, col)
618 )
619
620 if include_bulk_keys:
621 params.update((k, state_dict[k]) for k in include_bulk_keys)
622
623 if params or value_params:
624 params.update(pk_params)
625 yield (
626 state,
627 state_dict,
628 params,
629 mapper,
630 connection,
631 value_params,
632 has_all_defaults,
633 has_all_pks,
634 )
635 elif expect_pk_cascaded:
636 # no UPDATE occurs on this table, but we expect that CASCADE rules
637 # have changed the primary key of the row; propagate this event to
638 # other columns that expect to have been modified. this normally
639 # occurs after the UPDATE is emitted however we invoke it here
640 # explicitly in the absence of our invoking an UPDATE
641 for m, equated_pairs in mapper._table_to_equated[table]:
642 sync._populate(
643 state,
644 m,
645 state,
646 m,
647 equated_pairs,
648 uowtransaction,
649 mapper.passive_updates,
650 )
651
652
653def _collect_post_update_commands(
654 base_mapper, uowtransaction, table, states_to_update, post_update_cols
655):
656 """Identify sets of values to use in UPDATE statements for a
657 list of states within a post_update operation.
658
659 """
660
661 for (
662 state,
663 state_dict,
664 mapper,
665 connection,
666 update_version_id,
667 ) in states_to_update:
668 # assert table in mapper._pks_by_table
669
670 pks = mapper._pks_by_table[table]
671 params = {}
672 hasdata = False
673
674 for col in mapper._cols_by_table[table]:
675 if col in pks:
676 params[col._label] = mapper._get_state_attr_by_column(
677 state, state_dict, col, passive=attributes.PASSIVE_OFF
678 )
679
680 elif col in post_update_cols or col.onupdate is not None:
681 prop = mapper._columntoproperty[col]
682 history = state.manager[prop.key].impl.get_history(
683 state, state_dict, attributes.PASSIVE_NO_INITIALIZE
684 )
685 if history.added:
686 value = history.added[0]
687 params[col.key] = value
688 hasdata = True
689 if hasdata:
690 if (
691 update_version_id is not None
692 and mapper.version_id_col in mapper._cols_by_table[table]
693 ):
694 col = mapper.version_id_col
695 params[col._label] = update_version_id
696
697 if (
698 bool(state.key)
699 and col.key not in params
700 and mapper.version_id_generator is not False
701 ):
702 val = mapper.version_id_generator(update_version_id)
703 params[col.key] = val
704 yield state, state_dict, mapper, connection, params
705
706
707def _collect_delete_commands(
708 base_mapper, uowtransaction, table, states_to_delete
709):
710 """Identify values to use in DELETE statements for a list of
711 states to be deleted."""
712
713 for (
714 state,
715 state_dict,
716 mapper,
717 connection,
718 update_version_id,
719 ) in states_to_delete:
720 if table not in mapper._pks_by_table:
721 continue
722
723 params = {}
724 for col in mapper._pks_by_table[table]:
725 params[col.key] = value = (
726 mapper._get_committed_state_attr_by_column(
727 state, state_dict, col
728 )
729 )
730 if value is None:
731 raise orm_exc.FlushError(
732 "Can't delete from table %s "
733 "using NULL for primary "
734 "key value on column %s" % (table, col)
735 )
736
737 if (
738 update_version_id is not None
739 and mapper.version_id_col in mapper._cols_by_table[table]
740 ):
741 params[mapper.version_id_col.key] = update_version_id
742 yield params, connection
743
744
745def _emit_update_statements(
746 base_mapper,
747 uowtransaction,
748 mapper,
749 table,
750 update,
751 *,
752 bookkeeping=True,
753 use_orm_update_stmt=None,
754 enable_check_rowcount=True,
755):
756 """Emit UPDATE statements corresponding to value lists collected
757 by _collect_update_commands()."""
758
759 needs_version_id = (
760 mapper.version_id_col is not None
761 and mapper.version_id_col in mapper._cols_by_table[table]
762 )
763
764 execution_options = {"compiled_cache": base_mapper._compiled_cache}
765
766 def update_stmt(existing_stmt=None):
767 clauses = BooleanClauseList._construct_raw(operators.and_)
768
769 for col in mapper._pks_by_table[table]:
770 clauses._append_inplace(
771 col == sql.bindparam(col._label, type_=col.type)
772 )
773
774 if needs_version_id:
775 clauses._append_inplace(
776 mapper.version_id_col
777 == sql.bindparam(
778 mapper.version_id_col._label,
779 type_=mapper.version_id_col.type,
780 )
781 )
782
783 if existing_stmt is not None:
784 stmt = existing_stmt.where(clauses)
785 else:
786 stmt = table.update().where(clauses)
787 return stmt
788
789 if use_orm_update_stmt is not None:
790 cached_stmt = update_stmt(use_orm_update_stmt)
791
792 else:
793 cached_stmt = base_mapper._memo(("update", table), update_stmt)
794
795 for (
796 (connection, paramkeys, hasvalue, has_all_defaults, has_all_pks),
797 records,
798 ) in groupby(
799 update,
800 lambda rec: (
801 rec[4], # connection
802 set(rec[2]), # set of parameter keys
803 bool(rec[5]), # whether or not we have "value" parameters
804 rec[6], # has_all_defaults
805 rec[7], # has all pks
806 ),
807 ):
808 rows = 0
809 records = list(records)
810
811 statement = cached_stmt
812
813 if use_orm_update_stmt is not None:
814 statement = statement._annotate(
815 {
816 "_emit_update_table": table,
817 "_emit_update_mapper": mapper,
818 }
819 )
820
821 return_defaults = False
822
823 if not has_all_pks:
824 statement = statement.return_defaults(*mapper._pks_by_table[table])
825 return_defaults = True
826
827 if (
828 bookkeeping
829 and not has_all_defaults
830 and mapper.base_mapper.eager_defaults is True
831 # change as of #8889 - if RETURNING is not going to be used anyway,
832 # (applies to MySQL, MariaDB which lack UPDATE RETURNING) ensure
833 # we can do an executemany UPDATE which is more efficient
834 and table.implicit_returning
835 and connection.dialect.update_returning
836 ):
837 statement = statement.return_defaults(
838 *mapper._server_onupdate_default_cols[table]
839 )
840 return_defaults = True
841
842 if mapper._version_id_has_server_side_value:
843 statement = statement.return_defaults(mapper.version_id_col)
844 return_defaults = True
845
846 assert_singlerow = connection.dialect.supports_sane_rowcount
847
848 assert_multirow = (
849 assert_singlerow
850 and connection.dialect.supports_sane_multi_rowcount
851 )
852
853 # change as of #8889 - if RETURNING is not going to be used anyway,
854 # (applies to MySQL, MariaDB which lack UPDATE RETURNING) ensure
855 # we can do an executemany UPDATE which is more efficient
856 allow_executemany = not return_defaults and not needs_version_id
857
858 if hasvalue:
859 for (
860 state,
861 state_dict,
862 params,
863 mapper,
864 connection,
865 value_params,
866 has_all_defaults,
867 has_all_pks,
868 ) in records:
869 c = connection.execute(
870 statement.values(value_params),
871 params,
872 execution_options=execution_options,
873 )
874 if bookkeeping:
875 _postfetch(
876 mapper,
877 uowtransaction,
878 table,
879 state,
880 state_dict,
881 c,
882 c.context.compiled_parameters[0],
883 value_params,
884 True,
885 c.returned_defaults,
886 )
887 rows += c.rowcount
888 check_rowcount = enable_check_rowcount and assert_singlerow
889 else:
890 if not allow_executemany:
891 check_rowcount = enable_check_rowcount and assert_singlerow
892 for (
893 state,
894 state_dict,
895 params,
896 mapper,
897 connection,
898 value_params,
899 has_all_defaults,
900 has_all_pks,
901 ) in records:
902 c = connection.execute(
903 statement, params, execution_options=execution_options
904 )
905
906 # TODO: why with bookkeeping=False?
907 if bookkeeping:
908 _postfetch(
909 mapper,
910 uowtransaction,
911 table,
912 state,
913 state_dict,
914 c,
915 c.context.compiled_parameters[0],
916 value_params,
917 True,
918 c.returned_defaults,
919 )
920 rows += c.rowcount
921 else:
922 multiparams = [rec[2] for rec in records]
923
924 check_rowcount = enable_check_rowcount and (
925 assert_multirow
926 or (assert_singlerow and len(multiparams) == 1)
927 )
928
929 c = connection.execute(
930 statement, multiparams, execution_options=execution_options
931 )
932
933 rows += c.rowcount
934
935 for (
936 state,
937 state_dict,
938 params,
939 mapper,
940 connection,
941 value_params,
942 has_all_defaults,
943 has_all_pks,
944 ) in records:
945 if bookkeeping:
946 _postfetch(
947 mapper,
948 uowtransaction,
949 table,
950 state,
951 state_dict,
952 c,
953 c.context.compiled_parameters[0],
954 value_params,
955 True,
956 (
957 c.returned_defaults
958 if not c.context.executemany
959 else None
960 ),
961 )
962
963 if check_rowcount:
964 if rows != len(records):
965 raise orm_exc.StaleDataError(
966 "UPDATE statement on table '%s' expected to "
967 "update %d row(s); %d were matched."
968 % (table.description, len(records), rows)
969 )
970
971 elif needs_version_id:
972 util.warn(
973 "Dialect %s does not support updated rowcount "
974 "- versioning cannot be verified."
975 % c.dialect.dialect_description
976 )
977
978
979def _emit_insert_statements(
980 base_mapper,
981 uowtransaction,
982 mapper,
983 table,
984 insert,
985 *,
986 bookkeeping=True,
987 use_orm_insert_stmt=None,
988 execution_options=None,
989):
990 """Emit INSERT statements corresponding to value lists collected
991 by _collect_insert_commands()."""
992
993 if use_orm_insert_stmt is not None:
994 cached_stmt = use_orm_insert_stmt
995 exec_opt = util.EMPTY_DICT
996
997 # if a user query with RETURNING was passed, we definitely need
998 # to use RETURNING.
999 returning_is_required_anyway = bool(use_orm_insert_stmt._returning)
1000 deterministic_results_reqd = (
1001 returning_is_required_anyway
1002 and use_orm_insert_stmt._sort_by_parameter_order
1003 ) or bookkeeping
1004 else:
1005 returning_is_required_anyway = False
1006 deterministic_results_reqd = bookkeeping
1007 cached_stmt = base_mapper._memo(("insert", table), table.insert)
1008 exec_opt = {"compiled_cache": base_mapper._compiled_cache}
1009
1010 if execution_options:
1011 execution_options = util.EMPTY_DICT.merge_with(
1012 exec_opt, execution_options
1013 )
1014 else:
1015 execution_options = exec_opt
1016
1017 return_result = None
1018
1019 for (
1020 (connection, _, hasvalue, has_all_pks, has_all_defaults),
1021 records,
1022 ) in groupby(
1023 insert,
1024 lambda rec: (
1025 rec[4], # connection
1026 set(rec[2]), # parameter keys
1027 bool(rec[5]), # whether we have "value" parameters
1028 rec[6],
1029 rec[7],
1030 ),
1031 ):
1032 statement = cached_stmt
1033
1034 if use_orm_insert_stmt is not None:
1035 statement = statement._annotate(
1036 {
1037 "_emit_insert_table": table,
1038 "_emit_insert_mapper": mapper,
1039 }
1040 )
1041
1042 if (
1043 (
1044 not bookkeeping
1045 or (
1046 has_all_defaults
1047 or not base_mapper._prefer_eager_defaults(
1048 connection.dialect, table
1049 )
1050 or not table.implicit_returning
1051 or not connection.dialect.insert_returning
1052 )
1053 )
1054 and not returning_is_required_anyway
1055 and has_all_pks
1056 and not hasvalue
1057 ):
1058 # the "we don't need newly generated values back" section.
1059 # here we have all the PKs, all the defaults or we don't want
1060 # to fetch them, or the dialect doesn't support RETURNING at all
1061 # so we have to post-fetch / use lastrowid anyway.
1062 records = list(records)
1063 multiparams = [rec[2] for rec in records]
1064
1065 result = connection.execute(
1066 statement, multiparams, execution_options=execution_options
1067 )
1068 if bookkeeping:
1069 for (
1070 (
1071 state,
1072 state_dict,
1073 params,
1074 mapper_rec,
1075 conn,
1076 value_params,
1077 has_all_pks,
1078 has_all_defaults,
1079 ),
1080 last_inserted_params,
1081 ) in zip(records, result.context.compiled_parameters):
1082 if state:
1083 _postfetch(
1084 mapper_rec,
1085 uowtransaction,
1086 table,
1087 state,
1088 state_dict,
1089 result,
1090 last_inserted_params,
1091 value_params,
1092 False,
1093 (
1094 result.returned_defaults
1095 if not result.context.executemany
1096 else None
1097 ),
1098 )
1099 else:
1100 _postfetch_bulk_save(mapper_rec, state_dict, table)
1101
1102 else:
1103 # here, we need defaults and/or pk values back or we otherwise
1104 # know that we are using RETURNING in any case
1105
1106 records = list(records)
1107
1108 if returning_is_required_anyway or (
1109 table.implicit_returning and not hasvalue and len(records) > 1
1110 ):
1111 if (
1112 deterministic_results_reqd
1113 and connection.dialect.insert_executemany_returning_sort_by_parameter_order # noqa: E501
1114 ) or (
1115 not deterministic_results_reqd
1116 and connection.dialect.insert_executemany_returning
1117 ):
1118 do_executemany = True
1119 elif returning_is_required_anyway:
1120 if deterministic_results_reqd:
1121 dt = " with RETURNING and sort by parameter order"
1122 else:
1123 dt = " with RETURNING"
1124 raise sa_exc.InvalidRequestError(
1125 f"Can't use explicit RETURNING for bulk INSERT "
1126 f"operation with "
1127 f"{connection.dialect.dialect_description} backend; "
1128 f"executemany{dt} is not enabled for this dialect."
1129 )
1130 else:
1131 do_executemany = False
1132 else:
1133 do_executemany = False
1134
1135 if use_orm_insert_stmt is None:
1136 if (
1137 not has_all_defaults
1138 and base_mapper._prefer_eager_defaults(
1139 connection.dialect, table
1140 )
1141 ):
1142 statement = statement.return_defaults(
1143 *mapper._server_default_cols[table],
1144 sort_by_parameter_order=bookkeeping,
1145 )
1146
1147 if mapper.version_id_col is not None:
1148 statement = statement.return_defaults(
1149 mapper.version_id_col,
1150 sort_by_parameter_order=bookkeeping,
1151 )
1152 elif do_executemany:
1153 statement = statement.return_defaults(
1154 *table.primary_key, sort_by_parameter_order=bookkeeping
1155 )
1156
1157 if do_executemany:
1158 multiparams = [rec[2] for rec in records]
1159
1160 result = connection.execute(
1161 statement, multiparams, execution_options=execution_options
1162 )
1163
1164 if use_orm_insert_stmt is not None:
1165 if return_result is None:
1166 return_result = result
1167 else:
1168 return_result = return_result.splice_vertically(result)
1169
1170 if bookkeeping:
1171 for (
1172 (
1173 state,
1174 state_dict,
1175 params,
1176 mapper_rec,
1177 conn,
1178 value_params,
1179 has_all_pks,
1180 has_all_defaults,
1181 ),
1182 last_inserted_params,
1183 inserted_primary_key,
1184 returned_defaults,
1185 ) in zip_longest(
1186 records,
1187 result.context.compiled_parameters,
1188 result.inserted_primary_key_rows,
1189 result.returned_defaults_rows or (),
1190 ):
1191 if inserted_primary_key is None:
1192 # this is a real problem and means that we didn't
1193 # get back as many PK rows. we can't continue
1194 # since this indicates PK rows were missing, which
1195 # means we likely mis-populated records starting
1196 # at that point with incorrectly matched PK
1197 # values.
1198 raise orm_exc.FlushError(
1199 "Multi-row INSERT statement for %s did not "
1200 "produce "
1201 "the correct number of INSERTed rows for "
1202 "RETURNING. Ensure there are no triggers or "
1203 "special driver issues preventing INSERT from "
1204 "functioning properly." % mapper_rec
1205 )
1206
1207 for pk, col in zip(
1208 inserted_primary_key,
1209 mapper._pks_by_table[table],
1210 ):
1211 prop = mapper_rec._columntoproperty[col]
1212 if state_dict.get(prop.key) is None:
1213 state_dict[prop.key] = pk
1214
1215 if state:
1216 _postfetch(
1217 mapper_rec,
1218 uowtransaction,
1219 table,
1220 state,
1221 state_dict,
1222 result,
1223 last_inserted_params,
1224 value_params,
1225 False,
1226 returned_defaults,
1227 )
1228 else:
1229 _postfetch_bulk_save(mapper_rec, state_dict, table)
1230 else:
1231 assert not returning_is_required_anyway
1232
1233 for (
1234 state,
1235 state_dict,
1236 params,
1237 mapper_rec,
1238 connection,
1239 value_params,
1240 has_all_pks,
1241 has_all_defaults,
1242 ) in records:
1243 if value_params:
1244 result = connection.execute(
1245 statement.values(value_params),
1246 params,
1247 execution_options=execution_options,
1248 )
1249 else:
1250 result = connection.execute(
1251 statement,
1252 params,
1253 execution_options=execution_options,
1254 )
1255
1256 primary_key = result.inserted_primary_key
1257 if primary_key is None:
1258 raise orm_exc.FlushError(
1259 "Single-row INSERT statement for %s "
1260 "did not produce a "
1261 "new primary key result "
1262 "being invoked. Ensure there are no triggers or "
1263 "special driver issues preventing INSERT from "
1264 "functioning properly." % (mapper_rec,)
1265 )
1266 for pk, col in zip(
1267 primary_key, mapper._pks_by_table[table]
1268 ):
1269 prop = mapper_rec._columntoproperty[col]
1270 if (
1271 col in value_params
1272 or state_dict.get(prop.key) is None
1273 ):
1274 state_dict[prop.key] = pk
1275 if bookkeeping:
1276 if state:
1277 _postfetch(
1278 mapper_rec,
1279 uowtransaction,
1280 table,
1281 state,
1282 state_dict,
1283 result,
1284 result.context.compiled_parameters[0],
1285 value_params,
1286 False,
1287 (
1288 result.returned_defaults
1289 if not result.context.executemany
1290 else None
1291 ),
1292 )
1293 else:
1294 _postfetch_bulk_save(mapper_rec, state_dict, table)
1295
1296 if use_orm_insert_stmt is not None:
1297 if return_result is None:
1298 return _cursor.null_dml_result()
1299 else:
1300 return return_result
1301
1302
1303def _emit_post_update_statements(
1304 base_mapper, uowtransaction, mapper, table, update
1305):
1306 """Emit UPDATE statements corresponding to value lists collected
1307 by _collect_post_update_commands()."""
1308
1309 execution_options = {"compiled_cache": base_mapper._compiled_cache}
1310
1311 needs_version_id = (
1312 mapper.version_id_col is not None
1313 and mapper.version_id_col in mapper._cols_by_table[table]
1314 )
1315
1316 def update_stmt():
1317 clauses = BooleanClauseList._construct_raw(operators.and_)
1318
1319 for col in mapper._pks_by_table[table]:
1320 clauses._append_inplace(
1321 col == sql.bindparam(col._label, type_=col.type)
1322 )
1323
1324 if needs_version_id:
1325 clauses._append_inplace(
1326 mapper.version_id_col
1327 == sql.bindparam(
1328 mapper.version_id_col._label,
1329 type_=mapper.version_id_col.type,
1330 )
1331 )
1332
1333 stmt = table.update().where(clauses)
1334
1335 return stmt
1336
1337 statement = base_mapper._memo(("post_update", table), update_stmt)
1338
1339 if mapper._version_id_has_server_side_value:
1340 statement = statement.return_defaults(mapper.version_id_col)
1341
1342 # execute each UPDATE in the order according to the original
1343 # list of states to guarantee row access order, but
1344 # also group them into common (connection, cols) sets
1345 # to support executemany().
1346 for key, records in groupby(
1347 update,
1348 lambda rec: (rec[3], set(rec[4])), # connection # parameter keys
1349 ):
1350 rows = 0
1351
1352 records = list(records)
1353 connection = key[0]
1354
1355 assert_singlerow = connection.dialect.supports_sane_rowcount
1356 assert_multirow = (
1357 assert_singlerow
1358 and connection.dialect.supports_sane_multi_rowcount
1359 )
1360 allow_executemany = not needs_version_id or assert_multirow
1361
1362 if not allow_executemany:
1363 check_rowcount = assert_singlerow
1364 for state, state_dict, mapper_rec, connection, params in records:
1365 c = connection.execute(
1366 statement, params, execution_options=execution_options
1367 )
1368
1369 _postfetch_post_update(
1370 mapper_rec,
1371 uowtransaction,
1372 table,
1373 state,
1374 state_dict,
1375 c,
1376 c.context.compiled_parameters[0],
1377 )
1378 rows += c.rowcount
1379 else:
1380 multiparams = [
1381 params
1382 for state, state_dict, mapper_rec, conn, params in records
1383 ]
1384
1385 check_rowcount = assert_multirow or (
1386 assert_singlerow and len(multiparams) == 1
1387 )
1388
1389 c = connection.execute(
1390 statement, multiparams, execution_options=execution_options
1391 )
1392
1393 rows += c.rowcount
1394 for i, (
1395 state,
1396 state_dict,
1397 mapper_rec,
1398 connection,
1399 params,
1400 ) in enumerate(records):
1401 _postfetch_post_update(
1402 mapper_rec,
1403 uowtransaction,
1404 table,
1405 state,
1406 state_dict,
1407 c,
1408 c.context.compiled_parameters[i],
1409 )
1410
1411 if check_rowcount:
1412 if rows != len(records):
1413 raise orm_exc.StaleDataError(
1414 "UPDATE statement on table '%s' expected to "
1415 "update %d row(s); %d were matched."
1416 % (table.description, len(records), rows)
1417 )
1418
1419 elif needs_version_id:
1420 util.warn(
1421 "Dialect %s does not support updated rowcount "
1422 "- versioning cannot be verified."
1423 % c.dialect.dialect_description
1424 )
1425
1426
1427def _emit_delete_statements(
1428 base_mapper, uowtransaction, mapper, table, delete
1429):
1430 """Emit DELETE statements corresponding to value lists collected
1431 by _collect_delete_commands()."""
1432
1433 need_version_id = (
1434 mapper.version_id_col is not None
1435 and mapper.version_id_col in mapper._cols_by_table[table]
1436 )
1437
1438 def delete_stmt():
1439 clauses = BooleanClauseList._construct_raw(operators.and_)
1440
1441 for col in mapper._pks_by_table[table]:
1442 clauses._append_inplace(
1443 col == sql.bindparam(col.key, type_=col.type)
1444 )
1445
1446 if need_version_id:
1447 clauses._append_inplace(
1448 mapper.version_id_col
1449 == sql.bindparam(
1450 mapper.version_id_col.key, type_=mapper.version_id_col.type
1451 )
1452 )
1453
1454 return table.delete().where(clauses)
1455
1456 statement = base_mapper._memo(("delete", table), delete_stmt)
1457 for connection, recs in groupby(delete, lambda rec: rec[1]): # connection
1458 del_objects = [params for params, connection in recs]
1459
1460 execution_options = {"compiled_cache": base_mapper._compiled_cache}
1461 expected = len(del_objects)
1462 rows_matched = -1
1463 only_warn = False
1464
1465 if (
1466 need_version_id
1467 and not connection.dialect.supports_sane_multi_rowcount
1468 ):
1469 if connection.dialect.supports_sane_rowcount:
1470 rows_matched = 0
1471 # execute deletes individually so that versioned
1472 # rows can be verified
1473 for params in del_objects:
1474 c = connection.execute(
1475 statement, params, execution_options=execution_options
1476 )
1477 rows_matched += c.rowcount
1478 else:
1479 util.warn(
1480 "Dialect %s does not support deleted rowcount "
1481 "- versioning cannot be verified."
1482 % connection.dialect.dialect_description
1483 )
1484 connection.execute(
1485 statement, del_objects, execution_options=execution_options
1486 )
1487 else:
1488 c = connection.execute(
1489 statement, del_objects, execution_options=execution_options
1490 )
1491
1492 if not need_version_id:
1493 only_warn = True
1494
1495 rows_matched = c.rowcount
1496
1497 if (
1498 base_mapper.confirm_deleted_rows
1499 and rows_matched > -1
1500 and expected != rows_matched
1501 and (
1502 connection.dialect.supports_sane_multi_rowcount
1503 or len(del_objects) == 1
1504 )
1505 ):
1506 # TODO: why does this "only warn" if versioning is turned off,
1507 # whereas the UPDATE raises?
1508 if only_warn:
1509 util.warn(
1510 "DELETE statement on table '%s' expected to "
1511 "delete %d row(s); %d were matched. Please set "
1512 "confirm_deleted_rows=False within the mapper "
1513 "configuration to prevent this warning."
1514 % (table.description, expected, rows_matched)
1515 )
1516 else:
1517 raise orm_exc.StaleDataError(
1518 "DELETE statement on table '%s' expected to "
1519 "delete %d row(s); %d were matched. Please set "
1520 "confirm_deleted_rows=False within the mapper "
1521 "configuration to prevent this warning."
1522 % (table.description, expected, rows_matched)
1523 )
1524
1525
1526def _finalize_insert_update_commands(base_mapper, uowtransaction, states):
1527 """finalize state on states that have been inserted or updated,
1528 including calling after_insert/after_update events.
1529
1530 """
1531 for state, state_dict, mapper, connection, has_identity in states:
1532 if mapper._readonly_props:
1533 readonly = state.unmodified_intersection(
1534 [
1535 p.key
1536 for p in mapper._readonly_props
1537 if (
1538 p.expire_on_flush
1539 and (not p.deferred or p.key in state.dict)
1540 )
1541 or (
1542 not p.expire_on_flush
1543 and not p.deferred
1544 and p.key not in state.dict
1545 )
1546 ]
1547 )
1548 if readonly:
1549 state._expire_attributes(state.dict, readonly)
1550
1551 # if eager_defaults option is enabled, load
1552 # all expired cols. Else if we have a version_id_col, make sure
1553 # it isn't expired.
1554 toload_now = []
1555
1556 # this is specifically to emit a second SELECT for eager_defaults,
1557 # so only if it's set to True, not "auto"
1558 if base_mapper.eager_defaults is True:
1559 toload_now.extend(
1560 state._unloaded_non_object.intersection(
1561 mapper._server_default_plus_onupdate_propkeys
1562 )
1563 )
1564
1565 if (
1566 mapper.version_id_col is not None
1567 and mapper.version_id_generator is False
1568 ):
1569 if mapper._version_id_prop.key in state.unloaded:
1570 toload_now.extend([mapper._version_id_prop.key])
1571
1572 if toload_now:
1573 state.key = base_mapper._identity_key_from_state(state)
1574 stmt = sql.select(mapper)
1575 loading._load_on_ident(
1576 uowtransaction.session,
1577 stmt,
1578 state.key,
1579 refresh_state=state,
1580 only_load_props=toload_now,
1581 )
1582
1583 # call after_XXX extensions
1584 if not has_identity:
1585 mapper.dispatch.after_insert(mapper, connection, state)
1586 else:
1587 mapper.dispatch.after_update(mapper, connection, state)
1588
1589 if (
1590 mapper.version_id_generator is False
1591 and mapper.version_id_col is not None
1592 ):
1593 if state_dict[mapper._version_id_prop.key] is None:
1594 raise orm_exc.FlushError(
1595 "Instance does not contain a non-NULL version value"
1596 )
1597
1598
1599def _postfetch_post_update(
1600 mapper, uowtransaction, table, state, dict_, result, params
1601):
1602 needs_version_id = (
1603 mapper.version_id_col is not None
1604 and mapper.version_id_col in mapper._cols_by_table[table]
1605 )
1606
1607 if not uowtransaction.is_deleted(state):
1608 # post updating after a regular INSERT or UPDATE, do a full postfetch
1609 prefetch_cols = result.context.compiled.prefetch
1610 postfetch_cols = result.context.compiled.postfetch
1611 elif needs_version_id:
1612 # post updating before a DELETE with a version_id_col, need to
1613 # postfetch just version_id_col
1614 prefetch_cols = postfetch_cols = ()
1615 else:
1616 # post updating before a DELETE without a version_id_col,
1617 # don't need to postfetch
1618 return
1619
1620 if needs_version_id:
1621 prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
1622
1623 refresh_flush = bool(mapper.class_manager.dispatch.refresh_flush)
1624 if refresh_flush:
1625 load_evt_attrs = []
1626
1627 for c in prefetch_cols:
1628 if c.key in params and c in mapper._columntoproperty:
1629 dict_[mapper._columntoproperty[c].key] = params[c.key]
1630 if refresh_flush:
1631 load_evt_attrs.append(mapper._columntoproperty[c].key)
1632
1633 if refresh_flush and load_evt_attrs:
1634 mapper.class_manager.dispatch.refresh_flush(
1635 state, uowtransaction, load_evt_attrs
1636 )
1637
1638 if postfetch_cols:
1639 state._expire_attributes(
1640 state.dict,
1641 [
1642 mapper._columntoproperty[c].key
1643 for c in postfetch_cols
1644 if c in mapper._columntoproperty
1645 ],
1646 )
1647
1648
1649def _postfetch(
1650 mapper,
1651 uowtransaction,
1652 table,
1653 state,
1654 dict_,
1655 result,
1656 params,
1657 value_params,
1658 isupdate,
1659 returned_defaults,
1660):
1661 """Expire attributes in need of newly persisted database state,
1662 after an INSERT or UPDATE statement has proceeded for that
1663 state."""
1664
1665 prefetch_cols = result.context.compiled.prefetch
1666 postfetch_cols = result.context.compiled.postfetch
1667 returning_cols = result.context.compiled.effective_returning
1668
1669 if (
1670 mapper.version_id_col is not None
1671 and mapper.version_id_col in mapper._cols_by_table[table]
1672 ):
1673 prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
1674
1675 if not isupdate:
1676 # ORM-level defaults that _collect_insert_commands delivered to the
1677 # INSERT for attributes with no value are handled in the same way as
1678 # prefetched column defaults, #13748
1679 orm_default_cols = [
1680 col
1681 for pkey, col, _ in mapper._insert_orm_defaults[table]
1682 if dict_.get(pkey) is None and params.get(col.key) is not None
1683 ]
1684 if orm_default_cols:
1685 prefetch_cols = list(prefetch_cols) + orm_default_cols
1686
1687 refresh_flush = bool(mapper.class_manager.dispatch.refresh_flush)
1688 if refresh_flush:
1689 load_evt_attrs = []
1690
1691 if returning_cols:
1692 row = returned_defaults
1693 if row is not None:
1694 for row_value, col in zip(row, returning_cols):
1695 # pk cols returned from insert are handled
1696 # distinctly, don't step on the values here
1697 if col.primary_key and result.context.isinsert:
1698 continue
1699
1700 # note that columns can be in the "return defaults" that are
1701 # not mapped to this mapper, typically because they are
1702 # "excluded", which can be specified directly or also occurs
1703 # when using declarative w/ single table inheritance
1704 prop = mapper._columntoproperty.get(col)
1705 if prop:
1706 dict_[prop.key] = row_value
1707 if refresh_flush:
1708 load_evt_attrs.append(prop.key)
1709
1710 for c in prefetch_cols:
1711 if c.key in params and c in mapper._columntoproperty:
1712 pkey = mapper._columntoproperty[c].key
1713
1714 # set prefetched value in dict and also pop from committed_state,
1715 # since this is new database state that replaces whatever might
1716 # have previously been fetched (see #10800). this is essentially a
1717 # shorthand version of set_committed_value(), which could also be
1718 # used here directly (with more overhead)
1719 dict_[pkey] = params[c.key]
1720 state.committed_state.pop(pkey, None)
1721
1722 if refresh_flush:
1723 load_evt_attrs.append(pkey)
1724
1725 if refresh_flush and load_evt_attrs:
1726 mapper.class_manager.dispatch.refresh_flush(
1727 state, uowtransaction, load_evt_attrs
1728 )
1729
1730 if isupdate and value_params:
1731 # explicitly suit the use case specified by
1732 # [ticket:3801], PK SQL expressions for UPDATE on non-RETURNING
1733 # database which are set to themselves in order to do a version bump.
1734 postfetch_cols.extend(
1735 [
1736 col
1737 for col in value_params
1738 if col.primary_key and col not in returning_cols
1739 ]
1740 )
1741
1742 if postfetch_cols:
1743 state._expire_attributes(
1744 state.dict,
1745 [
1746 mapper._columntoproperty[c].key
1747 for c in postfetch_cols
1748 if c in mapper._columntoproperty
1749 ],
1750 )
1751
1752 # synchronize newly inserted ids from one table to the next
1753 # TODO: this still goes a little too often. would be nice to
1754 # have definitive list of "columns that changed" here
1755 for m, equated_pairs in mapper._table_to_equated[table]:
1756 sync._populate(
1757 state,
1758 m,
1759 state,
1760 m,
1761 equated_pairs,
1762 uowtransaction,
1763 mapper.passive_updates,
1764 )
1765
1766
1767def _postfetch_bulk_save(mapper, dict_, table):
1768 for m, equated_pairs in mapper._table_to_equated[table]:
1769 sync._bulk_populate_inherit_keys(dict_, m, equated_pairs)
1770
1771
1772def _connections_for_states(base_mapper, uowtransaction, states):
1773 """Return an iterator of (state, state.dict, mapper, connection).
1774
1775 The states are sorted according to _sort_states, then paired
1776 with the connection they should be using for the given
1777 unit of work transaction.
1778
1779 """
1780 # if session has a connection callable,
1781 # organize individual states with the connection
1782 # to use for update
1783 if uowtransaction.session.connection_callable:
1784 connection_callable = uowtransaction.session.connection_callable
1785 else:
1786 connection = uowtransaction.transaction.connection(base_mapper)
1787 connection_callable = None
1788
1789 for state in _sort_states(base_mapper, states):
1790 if connection_callable:
1791 connection = connection_callable(base_mapper, state.obj())
1792
1793 mapper = state.manager.mapper
1794
1795 yield state, state.dict, mapper, connection
1796
1797
1798def _sort_states(mapper, states):
1799 pending = set(states)
1800 persistent = {s for s in pending if s.key is not None}
1801 pending.difference_update(persistent)
1802
1803 try:
1804 persistent_sorted = sorted(
1805 persistent, key=mapper._persistent_sortkey_fn
1806 )
1807 except TypeError as err:
1808 raise sa_exc.InvalidRequestError(
1809 "Could not sort objects by primary key; primary key "
1810 "values must be sortable in Python (was: %s)" % err
1811 ) from err
1812 return (
1813 sorted(pending, key=operator.attrgetter("insert_order"))
1814 + persistent_sorted
1815 )