Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
bulk_persistence.py2136 linesDownload Raw Back to orm
1# orm/bulk_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"""additional ORM persistence classes related to "bulk" operations,
11specifically outside of the flush() process.
12
13"""
14
15from __future__ import annotations
16
17from typing import Any
18from typing import cast
19from typing import Dict
20from typing import Iterable
21from typing import Optional
22from typing import overload
23from typing import TYPE_CHECKING
24from typing import TypeVar
25from typing import Union
26
27from . import attributes
28from . import context
29from . import evaluator
30from . import exc as orm_exc
31from . import loading
32from . import persistence
33from .base import NO_VALUE
34from .context import AbstractORMCompileState
35from .context import FromStatement
36from .context import ORMFromStatementCompileState
37from .context import QueryContext
38from .. import exc as sa_exc
39from .. import util
40from ..engine import Dialect
41from ..engine import result as _result
42from ..sql import coercions
43from ..sql import dml
44from ..sql import expression
45from ..sql import roles
46from ..sql import select
47from ..sql import sqltypes
48from ..sql.base import _entity_namespace_key
49from ..sql.base import CompileState
50from ..sql.base import Options
51from ..sql.dml import DeleteDMLState
52from ..sql.dml import InsertDMLState
53from ..sql.dml import UpdateDMLState
54from ..util import EMPTY_DICT
55from ..util.typing import Literal
56
57if TYPE_CHECKING:
58    from ._typing import DMLStrategyArgument
59    from ._typing import OrmExecuteOptionsParameter
60    from ._typing import SynchronizeSessionArgument
61    from .mapper import Mapper
62    from .session import _BindArguments
63    from .session import ORMExecuteState
64    from .session import Session
65    from .session import SessionTransaction
66    from .state import InstanceState
67    from ..engine import Connection
68    from ..engine import cursor
69    from ..engine.interfaces import _CoreAnyExecuteParams
70
71_O = TypeVar("_O", bound=object)
72
73
74@overload
75def _bulk_insert(
76    mapper: Mapper[_O],
77    mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
78    session_transaction: SessionTransaction,
79    *,
80    isstates: bool,
81    return_defaults: bool,
82    render_nulls: bool,
83    use_orm_insert_stmt: Literal[None] = ...,
84    execution_options: Optional[OrmExecuteOptionsParameter] = ...,
85) -> None: ...
86
87
88@overload
89def _bulk_insert(
90    mapper: Mapper[_O],
91    mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
92    session_transaction: SessionTransaction,
93    *,
94    isstates: bool,
95    return_defaults: bool,
96    render_nulls: bool,
97    use_orm_insert_stmt: Optional[dml.Insert] = ...,
98    execution_options: Optional[OrmExecuteOptionsParameter] = ...,
99) -> cursor.CursorResult[Any]: ...
100
101
102def _bulk_insert(
103    mapper: Mapper[_O],
104    mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
105    session_transaction: SessionTransaction,
106    *,
107    isstates: bool,
108    return_defaults: bool,
109    render_nulls: bool,
110    use_orm_insert_stmt: Optional[dml.Insert] = None,
111    execution_options: Optional[OrmExecuteOptionsParameter] = None,
112) -> Optional[cursor.CursorResult[Any]]:
113    base_mapper = mapper.base_mapper
114
115    if session_transaction.session.connection_callable:
116        raise NotImplementedError(
117            "connection_callable / per-instance sharding "
118            "not supported in bulk_insert()"
119        )
120
121    if isstates:
122        if TYPE_CHECKING:
123            mappings = cast(Iterable[InstanceState[_O]], mappings)
124
125        if return_defaults:
126            # list of states allows us to attach .key for return_defaults case
127            states = [(state, state.dict) for state in mappings]
128            mappings = [dict_ for (state, dict_) in states]
129        else:
130            mappings = [state.dict for state in mappings]
131    else:
132        if TYPE_CHECKING:
133            mappings = cast(Iterable[Dict[str, Any]], mappings)
134
135        if return_defaults:
136            # use dictionaries given, so that newly populated defaults
137            # can be delivered back to the caller (see #11661). This is **not**
138            # compatible with other use cases such as a session-executed
139            # insert() construct, as this will confuse the case of
140            # insert-per-subclass for joined inheritance cases (see
141            # test_bulk_statements.py::BulkDMLReturningJoinedInhTest).
142            #
143            # So in this conditional, we have **only** called
144            # session.bulk_insert_mappings() which does not have this
145            # requirement
146            mappings = list(mappings)
147        else:
148            # for all other cases we need to establish a local dictionary
149            # so that the incoming dictionaries aren't mutated
150            mappings = [dict(m) for m in mappings]
151        _expand_composites(mapper, mappings)
152
153    connection = session_transaction.connection(base_mapper)
154
155    return_result: Optional[cursor.CursorResult[Any]] = None
156
157    mappers_to_run = [
158        (table, mp)
159        for table, mp in base_mapper._sorted_tables.items()
160        if table in mapper._pks_by_table
161    ]
162
163    if return_defaults:
164        # not used by new-style bulk inserts, only used for legacy
165        bookkeeping = True
166    elif len(mappers_to_run) > 1:
167        # if we have more than one table, mapper to run where we will be
168        # either horizontally splicing, or copying values between tables,
169        # we need the "bookkeeping" / deterministic returning order
170        bookkeeping = True
171    else:
172        bookkeeping = False
173
174    for table, super_mapper in mappers_to_run:
175        # find bindparams in the statement. For bulk, we don't really know if
176        # a key in the params applies to a different table since we are
177        # potentially inserting for multiple tables here; looking at the
178        # bindparam() is a lot more direct.   in most cases this will
179        # use _generate_cache_key() which is memoized, although in practice
180        # the ultimate statement that's executed is probably not the same
181        # object so that memoization might not matter much.
182        extra_bp_names = (
183            [
184                b.key
185                for b in use_orm_insert_stmt._get_embedded_bindparams()
186                if b.key in mappings[0]
187            ]
188            if use_orm_insert_stmt is not None
189            else ()
190        )
191
192        records = (
193            (
194                None,
195                state_dict,
196                params,
197                mapper,
198                connection,
199                value_params,
200                has_all_pks,
201                has_all_defaults,
202            )
203            for (
204                state,
205                state_dict,
206                params,
207                mp,
208                conn,
209                value_params,
210                has_all_pks,
211                has_all_defaults,
212            ) in persistence._collect_insert_commands(
213                table,
214                ((None, mapping, mapper, connection) for mapping in mappings),
215                bulk=True,
216                return_defaults=bookkeeping,
217                render_nulls=render_nulls,
218                include_bulk_keys=extra_bp_names,
219            )
220        )
221
222        result = persistence._emit_insert_statements(
223            base_mapper,
224            None,
225            super_mapper,
226            table,
227            records,
228            bookkeeping=bookkeeping,
229            use_orm_insert_stmt=use_orm_insert_stmt,
230            execution_options=execution_options,
231        )
232        if use_orm_insert_stmt is not None:
233            if not use_orm_insert_stmt._returning or return_result is None:
234                return_result = result
235            elif result.returns_rows:
236                assert bookkeeping
237                return_result = return_result.splice_horizontally(result)
238
239    if return_defaults and isstates:
240        identity_cls = mapper._identity_class
241        identity_props = [p.key for p in mapper._identity_key_props]
242        for state, dict_ in states:
243            state.key = (
244                identity_cls,
245                tuple([dict_[key] for key in identity_props]),
246                None,
247            )
248
249    if use_orm_insert_stmt is not None:
250        assert return_result is not None
251        return return_result
252
253
254@overload
255def _bulk_update(
256    mapper: Mapper[Any],
257    mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
258    session_transaction: SessionTransaction,
259    *,
260    isstates: bool,
261    update_changed_only: bool,
262    use_orm_update_stmt: Literal[None] = ...,
263    enable_check_rowcount: bool = True,
264) -> None: ...
265
266
267@overload
268def _bulk_update(
269    mapper: Mapper[Any],
270    mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
271    session_transaction: SessionTransaction,
272    *,
273    isstates: bool,
274    update_changed_only: bool,
275    use_orm_update_stmt: Optional[dml.Update] = ...,
276    enable_check_rowcount: bool = True,
277) -> _result.Result[Any]: ...
278
279
280def _bulk_update(
281    mapper: Mapper[Any],
282    mappings: Union[Iterable[InstanceState[_O]], Iterable[Dict[str, Any]]],
283    session_transaction: SessionTransaction,
284    *,
285    isstates: bool,
286    update_changed_only: bool,
287    use_orm_update_stmt: Optional[dml.Update] = None,
288    enable_check_rowcount: bool = True,
289) -> Optional[_result.Result[Any]]:
290    base_mapper = mapper.base_mapper
291
292    search_keys = mapper._primary_key_propkeys
293    if mapper._version_id_prop:
294        search_keys = {mapper._version_id_prop.key}.union(search_keys)
295
296    def _changed_dict(mapper, state):
297        return {
298            k: v
299            for k, v in state.dict.items()
300            if k in state.committed_state or k in search_keys
301        }
302
303    if isstates:
304        if update_changed_only:
305            mappings = [_changed_dict(mapper, state) for state in mappings]
306        else:
307            mappings = [state.dict for state in mappings]
308    else:
309        mappings = [dict(m) for m in mappings]
310        _expand_composites(mapper, mappings)
311
312    if session_transaction.session.connection_callable:
313        raise NotImplementedError(
314            "connection_callable / per-instance sharding "
315            "not supported in bulk_update()"
316        )
317
318    connection = session_transaction.connection(base_mapper)
319
320    # find bindparams in the statement. see _bulk_insert for similar
321    # notes for the insert case
322    extra_bp_names = (
323        [
324            b.key
325            for b in use_orm_update_stmt._get_embedded_bindparams()
326            if b.key in mappings[0]
327        ]
328        if use_orm_update_stmt is not None
329        else ()
330    )
331
332    for table, super_mapper in base_mapper._sorted_tables.items():
333        if not mapper.isa(super_mapper) or table not in mapper._pks_by_table:
334            continue
335
336        records = persistence._collect_update_commands(
337            None,
338            table,
339            (
340                (
341                    None,
342                    mapping,
343                    mapper,
344                    connection,
345                    (
346                        mapping[mapper._version_id_prop.key]
347                        if mapper._version_id_prop
348                        else None
349                    ),
350                )
351                for mapping in mappings
352            ),
353            bulk=True,
354            use_orm_update_stmt=use_orm_update_stmt,
355            include_bulk_keys=extra_bp_names,
356        )
357        persistence._emit_update_statements(
358            base_mapper,
359            None,
360            super_mapper,
361            table,
362            records,
363            bookkeeping=False,
364            use_orm_update_stmt=use_orm_update_stmt,
365            enable_check_rowcount=enable_check_rowcount,
366        )
367
368    if use_orm_update_stmt is not None:
369        return _result.null_result()
370
371
372def _expand_composites(mapper, mappings):
373    composite_attrs = mapper.composites
374    if not composite_attrs:
375        return
376
377    composite_keys = set(composite_attrs.keys())
378    populators = {
379        key: composite_attrs[key]._populate_composite_bulk_save_mappings_fn()
380        for key in composite_keys
381    }
382    for mapping in mappings:
383        for key in composite_keys.intersection(mapping):
384            populators[key](mapping)
385
386
387class ORMDMLState(AbstractORMCompileState):
388    is_dml_returning = True
389    from_statement_ctx: Optional[ORMFromStatementCompileState] = None
390
391    @classmethod
392    def _get_orm_crud_kv_pairs(
393        cls, mapper, statement, kv_iterator, needs_to_be_cacheable
394    ):
395        core_get_crud_kv_pairs = UpdateDMLState._get_crud_kv_pairs
396
397        for k, v in kv_iterator:
398            k = coercions.expect(roles.DMLColumnRole, k)
399
400            if isinstance(k, str):
401                desc = _entity_namespace_key(mapper, k, default=NO_VALUE)
402                if desc is NO_VALUE:
403                    yield (
404                        coercions.expect(roles.DMLColumnRole, k),
405                        (
406                            coercions.expect(
407                                roles.ExpressionElementRole,
408                                v,
409                                type_=sqltypes.NullType(),
410                                is_crud=True,
411                            )
412                            if needs_to_be_cacheable
413                            else v
414                        ),
415                    )
416                else:
417                    yield from core_get_crud_kv_pairs(
418                        statement,
419                        desc._bulk_update_tuples(v),
420                        needs_to_be_cacheable,
421                    )
422            elif "entity_namespace" in k._annotations:
423                k_anno = k._annotations
424                attr = _entity_namespace_key(
425                    k_anno["entity_namespace"], k_anno["proxy_key"]
426                )
427                yield from core_get_crud_kv_pairs(
428                    statement,
429                    attr._bulk_update_tuples(v),
430                    needs_to_be_cacheable,
431                )
432            else:
433                yield (
434                    k,
435                    (
436                        v
437                        if not needs_to_be_cacheable
438                        else coercions.expect(
439                            roles.ExpressionElementRole,
440                            v,
441                            type_=sqltypes.NullType(),
442                            is_crud=True,
443                        )
444                    ),
445                )
446
447    @classmethod
448    def _get_dml_plugin_subject(cls, statement):
449        plugin_subject = statement.table._propagate_attrs.get("plugin_subject")
450
451        if (
452            not plugin_subject
453            or not plugin_subject.mapper
454            or plugin_subject
455            is not statement._propagate_attrs["plugin_subject"]
456        ):
457            return None
458        return plugin_subject
459
460    @classmethod
461    def _get_multi_crud_kv_pairs(cls, statement, kv_iterator):
462        plugin_subject = cls._get_dml_plugin_subject(statement)
463
464        if not plugin_subject:
465            return UpdateDMLState._get_multi_crud_kv_pairs(
466                statement, kv_iterator
467            )
468
469        return [
470            dict(
471                cls._get_orm_crud_kv_pairs(
472                    plugin_subject.mapper, statement, value_dict.items(), False
473                )
474            )
475            for value_dict in kv_iterator
476        ]
477
478    @classmethod
479    def _get_crud_kv_pairs(cls, statement, kv_iterator, needs_to_be_cacheable):
480        assert (
481            needs_to_be_cacheable
482        ), "no test coverage for needs_to_be_cacheable=False"
483
484        plugin_subject = cls._get_dml_plugin_subject(statement)
485
486        if not plugin_subject:
487            return UpdateDMLState._get_crud_kv_pairs(
488                statement, kv_iterator, needs_to_be_cacheable
489            )
490        return list(
491            cls._get_orm_crud_kv_pairs(
492                plugin_subject.mapper,
493                statement,
494                kv_iterator,
495                needs_to_be_cacheable,
496            )
497        )
498
499    @classmethod
500    def get_entity_description(cls, statement):
501        ext_info = statement.table._annotations["parententity"]
502        mapper = ext_info.mapper
503        if ext_info.is_aliased_class:
504            _label_name = ext_info.name
505        else:
506            _label_name = mapper.class_.__name__
507
508        return {
509            "name": _label_name,
510            "type": mapper.class_,
511            "expr": ext_info.entity,
512            "entity": ext_info.entity,
513            "table": mapper.local_table,
514        }
515
516    @classmethod
517    def get_returning_column_descriptions(cls, statement):
518        def _ent_for_col(c):
519            return c._annotations.get("parententity", None)
520
521        def _attr_for_col(c, ent):
522            if ent is None:
523                return c
524            proxy_key = c._annotations.get("proxy_key", None)
525            if not proxy_key:
526                return c
527            else:
528                return getattr(ent.entity, proxy_key, c)
529
530        return [
531            {
532                "name": c.key,
533                "type": c.type,
534                "expr": _attr_for_col(c, ent),
535                "aliased": ent.is_aliased_class,
536                "entity": ent.entity,
537            }
538            for c, ent in [
539                (c, _ent_for_col(c)) for c in statement._all_selected_columns
540            ]
541        ]
542
543    def _setup_orm_returning(
544        self,
545        compiler,
546        orm_level_statement,
547        dml_level_statement,
548        dml_mapper,
549        *,
550        use_supplemental_cols=True,
551    ):
552        """establish ORM column handlers for an INSERT, UPDATE, or DELETE
553        which uses explicit returning().
554
555        called within compilation level create_for_statement.
556
557        The _return_orm_returning() method then receives the Result
558        after the statement was executed, and applies ORM loading to the
559        state that we first established here.
560
561        """
562
563        if orm_level_statement._returning:
564            fs = FromStatement(
565                orm_level_statement._returning,
566                dml_level_statement,
567                _adapt_on_names=False,
568            )
569            fs = fs.execution_options(**orm_level_statement._execution_options)
570            fs = fs.options(*orm_level_statement._with_options)
571            self.select_statement = fs
572            self.from_statement_ctx = fsc = (
573                ORMFromStatementCompileState.create_for_statement(fs, compiler)
574            )
575            fsc.setup_dml_returning_compile_state(dml_mapper)
576
577            dml_level_statement = dml_level_statement._generate()
578            dml_level_statement._returning = ()
579
580            cols_to_return = [c for c in fsc.primary_columns if c is not None]
581
582            # since we are splicing result sets together, make sure there
583            # are columns of some kind returned in each result set
584            if not cols_to_return:
585                cols_to_return.extend(dml_mapper.primary_key)
586
587            if use_supplemental_cols:
588                dml_level_statement = dml_level_statement.return_defaults(
589                    # this is a little weird looking, but by passing
590                    # primary key as the main list of cols, this tells
591                    # return_defaults to omit server-default cols (and
592                    # actually all cols, due to some weird thing we should
593                    # clean up in crud.py).
594                    # Since we have cols_to_return, just return what we asked
595                    # for (plus primary key, which ORM persistence needs since
596                    # we likely set bookkeeping=True here, which is another
597                    # whole thing...).   We dont want to clutter the
598                    # statement up with lots of other cols the user didn't
599                    # ask for.  see #9685
600                    *dml_mapper.primary_key,
601                    supplemental_cols=cols_to_return,
602                )
603            else:
604                dml_level_statement = dml_level_statement.returning(
605                    *cols_to_return
606                )
607
608        return dml_level_statement
609
610    @classmethod
611    def _return_orm_returning(
612        cls,
613        session,
614        statement,
615        params,
616        execution_options,
617        bind_arguments,
618        result,
619    ):
620        execution_context = result.context
621        compile_state = execution_context.compiled.compile_state
622
623        if (
624            compile_state.from_statement_ctx
625            and not compile_state.from_statement_ctx.compile_options._is_star
626        ):
627            load_options = execution_options.get(
628                "_sa_orm_load_options", QueryContext.default_load_options
629            )
630
631            querycontext = QueryContext(
632                compile_state.from_statement_ctx,
633                compile_state.select_statement,
634                statement,
635                params,
636                session,
637                load_options,
638                execution_options,
639                bind_arguments,
640            )
641            return loading.instances(result, querycontext)
642        else:
643            return result
644
645
646class BulkUDCompileState(ORMDMLState):
647    class default_update_options(Options):
648        _dml_strategy: DMLStrategyArgument = "auto"
649        _synchronize_session: SynchronizeSessionArgument = "auto"
650        _can_use_returning: bool = False
651        _is_delete_using: bool = False
652        _is_update_from: bool = False
653        _autoflush: bool = True
654        _subject_mapper: Optional[Mapper[Any]] = None
655        _resolved_values = EMPTY_DICT
656        _eval_condition = None
657        _matched_rows = None
658        _identity_token = None
659        _populate_existing: bool = False
660
661    @classmethod
662    def can_use_returning(
663        cls,
664        dialect: Dialect,
665        mapper: Mapper[Any],
666        *,
667        is_multitable: bool = False,
668        is_update_from: bool = False,
669        is_delete_using: bool = False,
670        is_executemany: bool = False,
671    ) -> bool:
672        raise NotImplementedError()
673
674    @classmethod
675    def orm_pre_session_exec(
676        cls,
677        session,
678        statement,
679        params,
680        execution_options,
681        bind_arguments,
682        is_pre_event,
683    ):
684        (
685            update_options,
686            execution_options,
687        ) = BulkUDCompileState.default_update_options.from_execution_options(
688            "_sa_orm_update_options",
689            {
690                "synchronize_session",
691                "autoflush",
692                "populate_existing",
693                "identity_token",
694                "is_delete_using",
695                "is_update_from",
696                "dml_strategy",
697            },
698            execution_options,
699            statement._execution_options,
700        )
701        bind_arguments["clause"] = statement
702        try:
703            plugin_subject = statement._propagate_attrs["plugin_subject"]
704        except KeyError:
705            assert False, "statement had 'orm' plugin but no plugin_subject"
706        else:
707            if plugin_subject:
708                bind_arguments["mapper"] = plugin_subject.mapper
709                update_options += {"_subject_mapper": plugin_subject.mapper}
710
711        if "parententity" not in statement.table._annotations:
712            update_options += {"_dml_strategy": "core_only"}
713        elif not isinstance(params, list):
714            if update_options._dml_strategy == "auto":
715                update_options += {"_dml_strategy": "orm"}
716            elif update_options._dml_strategy == "bulk":
717                raise sa_exc.InvalidRequestError(
718                    'Can\'t use "bulk" ORM insert strategy without '
719                    "passing separate parameters"
720                )
721        else:
722            if update_options._dml_strategy == "auto":
723                update_options += {"_dml_strategy": "bulk"}
724
725        sync = update_options._synchronize_session
726        if sync is not None:
727            if sync not in ("auto", "evaluate", "fetch", False):
728                raise sa_exc.ArgumentError(
729                    "Valid strategies for session synchronization "
730                    "are 'auto', 'evaluate', 'fetch', False"
731                )
732            if update_options._dml_strategy == "bulk" and sync == "fetch":
733                raise sa_exc.InvalidRequestError(
734                    "The 'fetch' synchronization strategy is not available "
735                    "for 'bulk' ORM updates (i.e. multiple parameter sets)"
736                )
737
738        if not is_pre_event:
739            if update_options._autoflush:
740                session._autoflush()
741
742            if update_options._dml_strategy == "orm":
743                if update_options._synchronize_session == "auto":
744                    update_options = cls._do_pre_synchronize_auto(
745                        session,
746                        statement,
747                        params,
748                        execution_options,
749                        bind_arguments,
750                        update_options,
751                    )
752                elif update_options._synchronize_session == "evaluate":
753                    update_options = cls._do_pre_synchronize_evaluate(
754                        session,
755                        statement,
756                        params,
757                        execution_options,
758                        bind_arguments,
759                        update_options,
760                    )
761                elif update_options._synchronize_session == "fetch":
762                    update_options = cls._do_pre_synchronize_fetch(
763                        session,
764                        statement,
765                        params,
766                        execution_options,
767                        bind_arguments,
768                        update_options,
769                    )
770            elif update_options._dml_strategy == "bulk":
771                if update_options._synchronize_session == "auto":
772                    update_options += {"_synchronize_session": "evaluate"}
773
774            # indicators from the "pre exec" step that are then
775            # added to the DML statement, which will also be part of the cache
776            # key.  The compile level create_for_statement() method will then
777            # consume these at compiler time.
778            statement = statement._annotate(
779                {
780                    "synchronize_session": update_options._synchronize_session,
781                    "is_delete_using": update_options._is_delete_using,
782                    "is_update_from": update_options._is_update_from,
783                    "dml_strategy": update_options._dml_strategy,
784                    "can_use_returning": update_options._can_use_returning,
785                }
786            )
787
788        return (
789            statement,
790            util.immutabledict(execution_options).union(
791                {"_sa_orm_update_options": update_options}
792            ),
793        )
794
795    @classmethod
796    def orm_setup_cursor_result(
797        cls,
798        session,
799        statement,
800        params,
801        execution_options,
802        bind_arguments,
803        result,
804    ):
805        # this stage of the execution is called after the
806        # do_orm_execute event hook.  meaning for an extension like
807        # horizontal sharding, this step happens *within* the horizontal
808        # sharding event handler which calls session.execute() re-entrantly
809        # and will occur for each backend individually.
810        # the sharding extension then returns its own merged result from the
811        # individual ones we return here.
812
813        update_options = execution_options["_sa_orm_update_options"]
814        if update_options._dml_strategy == "orm":
815            if update_options._synchronize_session == "evaluate":
816                cls._do_post_synchronize_evaluate(
817                    session, statement, result, update_options
818                )
819            elif update_options._synchronize_session == "fetch":
820                cls._do_post_synchronize_fetch(
821                    session, statement, result, update_options
822                )
823        elif update_options._dml_strategy == "bulk":
824            if update_options._synchronize_session == "evaluate":
825                cls._do_post_synchronize_bulk_evaluate(
826                    session, params, result, update_options
827                )
828            return result
829
830        return cls._return_orm_returning(
831            session,
832            statement,
833            params,
834            execution_options,
835            bind_arguments,
836            result,
837        )
838
839    @classmethod
840    def _adjust_for_extra_criteria(cls, global_attributes, ext_info):
841        """Apply extra criteria filtering.
842
843        For all distinct single-table-inheritance mappers represented in the
844        table being updated or deleted, produce additional WHERE criteria such
845        that only the appropriate subtypes are selected from the total results.
846
847        Additionally, add WHERE criteria originating from LoaderCriteriaOptions
848        collected from the statement.
849
850        """
851
852        return_crit = ()
853
854        adapter = ext_info._adapter if ext_info.is_aliased_class else None
855
856        if (
857            "additional_entity_criteria",
858            ext_info.mapper,
859        ) in global_attributes:
860            return_crit += tuple(
861                ae._resolve_where_criteria(ext_info)
862                for ae in global_attributes[
863                    ("additional_entity_criteria", ext_info.mapper)
864                ]
865                if ae.include_aliases or ae.entity is ext_info
866            )
867
868        if ext_info.mapper._single_table_criterion is not None:
869            return_crit += (ext_info.mapper._single_table_criterion,)
870
871        if adapter:
872            return_crit = tuple(adapter.traverse(crit) for crit in return_crit)
873
874        return return_crit
875
876    @classmethod
877    def _interpret_returning_rows(cls, result, mapper, rows):
878        """return rows that indicate PK cols in mapper.primary_key position
879        for RETURNING rows.
880
881        Prior to 2.0.36, this method seemed to be written for some kind of
882        inheritance scenario but the scenario was unused for actual joined
883        inheritance, and the function instead seemed to perform some kind of
884        partial translation that would remove non-PK cols if the PK cols
885        happened to be first in the row, but not otherwise.  The joined
886        inheritance walk feature here seems to have never been used as it was
887        always skipped by the "local_table" check.
888
889        As of 2.0.36 the function strips away non-PK cols and provides the
890        PK cols for the table in mapper PK order.
891
892        """
893
894        try:
895            if mapper.local_table is not mapper.base_mapper.local_table:
896                # TODO: dive more into how a local table PK is used for fetch
897                # sync, not clear if this is correct as it depends on the
898                # downstream routine to fetch rows using
899                # local_table.primary_key order
900                pk_keys = result._tuple_getter(mapper.local_table.primary_key)
901            else:
902                pk_keys = result._tuple_getter(mapper.primary_key)
903        except KeyError:
904            # can't use these rows, they don't have PK cols in them
905            # this is an unusual case where the user would have used
906            # .return_defaults()
907            return []
908
909        return [pk_keys(row) for row in rows]
910
911    @classmethod
912    def _get_matched_objects_on_criteria(cls, update_options, states):
913        mapper = update_options._subject_mapper
914        eval_condition = update_options._eval_condition
915
916        raw_data = [
917            (state.obj(), state, state.dict)
918            for state in states
919            if state.mapper.isa(mapper) and not state.expired
920        ]
921
922        identity_token = update_options._identity_token
923        if identity_token is not None:
924            raw_data = [
925                (obj, state, dict_)
926                for obj, state, dict_ in raw_data
927                if state.identity_token == identity_token
928            ]
929
930        result = []
931        for obj, state, dict_ in raw_data:
932            evaled_condition = eval_condition(obj)
933
934            # caution: don't use "in ()" or == here, _EXPIRE_OBJECT
935            # evaluates as True for all comparisons
936            if (
937                evaled_condition is True
938                or evaled_condition is evaluator._EXPIRED_OBJECT
939            ):
940                result.append(
941                    (
942                        obj,
943                        state,
944                        dict_,
945                        evaled_condition is evaluator._EXPIRED_OBJECT,
946                    )
947                )
948        return result
949
950    @classmethod
951    def _eval_condition_from_statement(cls, update_options, statement):
952        mapper = update_options._subject_mapper
953        target_cls = mapper.class_
954
955        evaluator_compiler = evaluator._EvaluatorCompiler(target_cls)
956        crit = ()
957        if statement._where_criteria:
958            crit += statement._where_criteria
959
960        global_attributes = {}
961        for opt in statement._with_options:
962            if opt._is_criteria_option:
963                opt.get_global_criteria(global_attributes)
964
965        if global_attributes:
966            crit += cls._adjust_for_extra_criteria(global_attributes, mapper)
967
968        if crit:
969            eval_condition = evaluator_compiler.process(*crit)
970        else:
971            # workaround for mypy https://github.com/python/mypy/issues/14027
972            def _eval_condition(obj):
973                return True
974
975            eval_condition = _eval_condition
976
977        return eval_condition
978
979    @classmethod
980    def _do_pre_synchronize_auto(
981        cls,
982        session,
983        statement,
984        params,
985        execution_options,
986        bind_arguments,
987        update_options,
988    ):
989        """setup auto sync strategy
990
991
992        "auto" checks if we can use "evaluate" first, then falls back
993        to "fetch"
994
995        evaluate is vastly more efficient for the common case
996        where session is empty, only has a few objects, and the UPDATE
997        statement can potentially match thousands/millions of rows.
998
999        OTOH more complex criteria that fails to work with "evaluate"
1000        we would hope usually correlates with fewer net rows.
1001
1002        """
1003
1004        try:
1005            eval_condition = cls._eval_condition_from_statement(
1006                update_options, statement
1007            )
1008
1009        except evaluator.UnevaluatableError:
1010            pass
1011        else:
1012            return update_options + {
1013                "_eval_condition": eval_condition,
1014                "_synchronize_session": "evaluate",
1015            }
1016
1017        update_options += {"_synchronize_session": "fetch"}
1018        return cls._do_pre_synchronize_fetch(
1019            session,
1020            statement,
1021            params,
1022            execution_options,
1023            bind_arguments,
1024            update_options,
1025        )
1026
1027    @classmethod
1028    def _do_pre_synchronize_evaluate(
1029        cls,
1030        session,
1031        statement,
1032        params,
1033        execution_options,
1034        bind_arguments,
1035        update_options,
1036    ):
1037        try:
1038            eval_condition = cls._eval_condition_from_statement(
1039                update_options, statement
1040            )
1041
1042        except evaluator.UnevaluatableError as err:
1043            raise sa_exc.InvalidRequestError(
1044                'Could not evaluate current criteria in Python: "%s". '
1045                "Specify 'fetch' or False for the "
1046                "synchronize_session execution option." % err
1047            ) from err
1048
1049        return update_options + {
1050            "_eval_condition": eval_condition,
1051        }
1052
1053    @classmethod
1054    def _get_resolved_values(cls, mapper, statement):
1055        if statement._multi_values:
1056            return []
1057        elif statement._ordered_values:
1058            return list(statement._ordered_values)
1059        elif statement._values:
1060            return list(statement._values.items())
1061        else:
1062            return []
1063
1064    @classmethod
1065    def _resolved_keys_as_propnames(cls, mapper, resolved_values):
1066        values = []
1067        for k, v in resolved_values:
1068            if mapper and isinstance(k, expression.ColumnElement):
1069                try:
1070                    attr = mapper._columntoproperty[k]
1071                except orm_exc.UnmappedColumnError:
1072                    pass
1073                else:
1074                    values.append((attr.key, v))
1075            else:
1076                raise sa_exc.InvalidRequestError(
1077                    "Attribute name not found, can't be "
1078                    "synchronized back to objects: %r" % k
1079                )
1080        return values
1081
1082    @classmethod
1083    def _do_pre_synchronize_fetch(
1084        cls,
1085        session,
1086        statement,
1087        params,
1088        execution_options,
1089        bind_arguments,
1090        update_options,
1091    ):
1092        mapper = update_options._subject_mapper
1093
1094        select_stmt = (
1095            select(*(mapper.primary_key + (mapper.select_identity_token,)))
1096            .select_from(mapper)
1097            .options(*statement._with_options)
1098        )
1099        select_stmt._where_criteria = statement._where_criteria
1100
1101        # conditionally run the SELECT statement for pre-fetch, testing the
1102        # "bind" for if we can use RETURNING or not using the do_orm_execute
1103        # event.  If RETURNING is available, the do_orm_execute event
1104        # will cancel the SELECT from being actually run.
1105        #
1106        # The way this is organized seems strange, why don't we just
1107        # call can_use_returning() before invoking the statement and get
1108        # answer?, why does this go through the whole execute phase using an
1109        # event?  Answer: because we are integrating with extensions such
1110        # as the horizontal sharding extension that "multiplexes" an individual
1111        # statement run through multiple engines, and it uses
1112        # do_orm_execute() to do that.
1113
1114        can_use_returning = None
1115
1116        def skip_for_returning(orm_context: ORMExecuteState) -> Any:
1117            bind = orm_context.session.get_bind(**orm_context.bind_arguments)
1118            nonlocal can_use_returning
1119
1120            per_bind_result = cls.can_use_returning(
1121                bind.dialect,
1122                mapper,
1123                is_update_from=update_options._is_update_from,
1124                is_delete_using=update_options._is_delete_using,
1125                is_executemany=orm_context.is_executemany,
1126            )
1127
1128            if can_use_returning is not None:
1129                if can_use_returning != per_bind_result:
1130                    raise sa_exc.InvalidRequestError(
1131                        "For synchronize_session='fetch', can't mix multiple "
1132                        "backends where some support RETURNING and others "
1133                        "don't"
1134                    )
1135            elif orm_context.is_executemany and not per_bind_result:
1136                raise sa_exc.InvalidRequestError(
1137                    "For synchronize_session='fetch', can't use multiple "
1138                    "parameter sets in ORM mode, which this backend does not "
1139                    "support with RETURNING"
1140                )
1141            else:
1142                can_use_returning = per_bind_result
1143
1144            if per_bind_result:
1145                return _result.null_result()
1146            else:
1147                return None
1148
1149        result = session.execute(
1150            select_stmt,
1151            params,
1152            execution_options=execution_options,
1153            bind_arguments=bind_arguments,
1154            _add_event=skip_for_returning,
1155        )
1156        matched_rows = result.fetchall()
1157
1158        return update_options + {
1159            "_matched_rows": matched_rows,
1160            "_can_use_returning": can_use_returning,
1161        }
1162
1163
1164@CompileState.plugin_for("orm", "insert")
1165class BulkORMInsert(ORMDMLState, InsertDMLState):
1166    class default_insert_options(Options):
1167        _dml_strategy: DMLStrategyArgument = "auto"
1168        _render_nulls: bool = False
1169        _return_defaults: bool = False
1170        _subject_mapper: Optional[Mapper[Any]] = None
1171        _autoflush: bool = True
1172        _populate_existing: bool = False
1173
1174    select_statement: Optional[FromStatement] = None
1175
1176    @classmethod
1177    def orm_pre_session_exec(
1178        cls,
1179        session,
1180        statement,
1181        params,
1182        execution_options,
1183        bind_arguments,
1184        is_pre_event,
1185    ):
1186        (
1187            insert_options,
1188            execution_options,
1189        ) = BulkORMInsert.default_insert_options.from_execution_options(
1190            "_sa_orm_insert_options",
1191            {"dml_strategy", "autoflush", "populate_existing", "render_nulls"},
1192            execution_options,
1193            statement._execution_options,
1194        )
1195        bind_arguments["clause"] = statement
1196        try:
1197            plugin_subject = statement._propagate_attrs["plugin_subject"]
1198        except KeyError:
1199            assert False, "statement had 'orm' plugin but no plugin_subject"
1200        else:

Showing the first 1,200 of 2136 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai