codekingpro/portable-devtools
115k
1# orm/context.py
2# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: ignore-errors
8
9from __future__ import annotations
10
11import itertools
12from typing import Any
13from typing import cast
14from typing import Dict
15from typing import Iterable
16from typing import List
17from typing import Optional
18from typing import Set
19from typing import Tuple
20from typing import Type
21from typing import TYPE_CHECKING
22from typing import TypeVar
23from typing import Union
24
25from . import attributes
26from . import interfaces
27from . import loading
28from .base import _is_aliased_class
29from .interfaces import ORMColumnDescription
30from .interfaces import ORMColumnsClauseRole
31from .path_registry import PathRegistry
32from .util import _entity_corresponds_to
33from .util import _ORMJoin
34from .util import _TraceAdaptRole
35from .util import AliasedClass
36from .util import Bundle
37from .util import ORMAdapter
38from .util import ORMStatementAdapter
39from .. import exc as sa_exc
40from .. import future
41from .. import inspect
42from .. import sql
43from .. import util
44from ..sql import coercions
45from ..sql import expression
46from ..sql import roles
47from ..sql import util as sql_util
48from ..sql import visitors
49from ..sql._typing import _TP
50from ..sql._typing import is_dml
51from ..sql._typing import is_insert_update
52from ..sql._typing import is_select_base
53from ..sql.base import _select_iterables
54from ..sql.base import CacheableOptions
55from ..sql.base import CompileState
56from ..sql.base import Executable
57from ..sql.base import Generative
58from ..sql.base import Options
59from ..sql.dml import UpdateBase
60from ..sql.elements import GroupedElement
61from ..sql.elements import TextClause
62from ..sql.selectable import CompoundSelectState
63from ..sql.selectable import LABEL_STYLE_DISAMBIGUATE_ONLY
64from ..sql.selectable import LABEL_STYLE_NONE
65from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
66from ..sql.selectable import Select
67from ..sql.selectable import SelectLabelStyle
68from ..sql.selectable import SelectState
69from ..sql.selectable import TypedReturnsRows
70from ..sql.visitors import InternalTraversal
71
72if TYPE_CHECKING:
73 from ._typing import _InternalEntityType
74 from ._typing import OrmExecuteOptionsParameter
75 from .loading import PostLoad
76 from .mapper import Mapper
77 from .query import Query
78 from .session import _BindArguments
79 from .session import Session
80 from ..engine import Result
81 from ..engine.interfaces import _CoreSingleExecuteParams
82 from ..sql._typing import _ColumnsClauseArgument
83 from ..sql.compiler import SQLCompiler
84 from ..sql.dml import _DMLTableElement
85 from ..sql.elements import ColumnElement
86 from ..sql.selectable import _JoinTargetElement
87 from ..sql.selectable import _LabelConventionCallable
88 from ..sql.selectable import _SetupJoinsElement
89 from ..sql.selectable import ExecutableReturnsRows
90 from ..sql.selectable import SelectBase
91 from ..sql.type_api import TypeEngine
92
93_T = TypeVar("_T", bound=Any)
94_path_registry = PathRegistry.root
95
96_EMPTY_DICT = util.immutabledict()
97
98
99LABEL_STYLE_LEGACY_ORM = SelectLabelStyle.LABEL_STYLE_LEGACY_ORM
100
101
102class QueryContext:
103 __slots__ = (
104 "top_level_context",
105 "compile_state",
106 "query",
107 "params",
108 "load_options",
109 "bind_arguments",
110 "execution_options",
111 "session",
112 "autoflush",
113 "populate_existing",
114 "invoke_all_eagers",
115 "version_check",
116 "refresh_state",
117 "create_eager_joins",
118 "propagated_loader_options",
119 "attributes",
120 "runid",
121 "partials",
122 "post_load_paths",
123 "identity_token",
124 "yield_per",
125 "loaders_require_buffering",
126 "loaders_require_uniquing",
127 )
128
129 runid: int
130 post_load_paths: Dict[PathRegistry, PostLoad]
131 compile_state: ORMCompileState
132
133 class default_load_options(Options):
134 _only_return_tuples = False
135 _populate_existing = False
136 _version_check = False
137 _invoke_all_eagers = True
138 _autoflush = True
139 _identity_token = None
140 _yield_per = None
141 _refresh_state = None
142 _lazy_loaded_from = None
143 _legacy_uniquing = False
144 _sa_top_level_orm_context = None
145 _is_user_refresh = False
146
147 def __init__(
148 self,
149 compile_state: CompileState,
150 statement: Union[Select[Any], FromStatement[Any]],
151 params: _CoreSingleExecuteParams,
152 session: Session,
153 load_options: Union[
154 Type[QueryContext.default_load_options],
155 QueryContext.default_load_options,
156 ],
157 execution_options: Optional[OrmExecuteOptionsParameter] = None,
158 bind_arguments: Optional[_BindArguments] = None,
159 ):
160 self.load_options = load_options
161 self.execution_options = execution_options or _EMPTY_DICT
162 self.bind_arguments = bind_arguments or _EMPTY_DICT
163 self.compile_state = compile_state
164 self.query = statement
165 self.session = session
166 self.loaders_require_buffering = False
167 self.loaders_require_uniquing = False
168 self.params = params
169 self.top_level_context = load_options._sa_top_level_orm_context
170
171 cached_options = compile_state.select_statement._with_options
172 uncached_options = statement._with_options
173
174 # see issue #7447 , #8399 for some background
175 # propagated loader options will be present on loaded InstanceState
176 # objects under state.load_options and are typically used by
177 # LazyLoader to apply options to the SELECT statement it emits.
178 # For compile state options (i.e. loader strategy options), these
179 # need to line up with the ".load_path" attribute which in
180 # loader.py is pulled from context.compile_state.current_path.
181 # so, this means these options have to be the ones from the
182 # *cached* statement that's travelling with compile_state, not the
183 # *current* statement which won't match up for an ad-hoc
184 # AliasedClass
185 self.propagated_loader_options = tuple(
186 opt._adapt_cached_option_to_uncached_option(self, uncached_opt)
187 for opt, uncached_opt in zip(cached_options, uncached_options)
188 if opt.propagate_to_loaders
189 )
190
191 self.attributes = dict(compile_state.attributes)
192
193 self.autoflush = load_options._autoflush
194 self.populate_existing = load_options._populate_existing
195 self.invoke_all_eagers = load_options._invoke_all_eagers
196 self.version_check = load_options._version_check
197 self.refresh_state = load_options._refresh_state
198 self.yield_per = load_options._yield_per
199 self.identity_token = load_options._identity_token
200
201 def _get_top_level_context(self) -> QueryContext:
202 return self.top_level_context or self
203
204
205_orm_load_exec_options = util.immutabledict(
206 {"_result_disable_adapt_to_context": True}
207)
208
209
210class AbstractORMCompileState(CompileState):
211 is_dml_returning = False
212
213 def _init_global_attributes(
214 self, statement, compiler, *, toplevel, process_criteria_for_toplevel
215 ):
216 self.attributes = {}
217
218 if compiler is None:
219 # this is the legacy / testing only ORM _compile_state() use case.
220 # there is no need to apply criteria options for this.
221 self.global_attributes = ga = {}
222 assert toplevel
223 return
224 else:
225 self.global_attributes = ga = compiler._global_attributes
226
227 if toplevel:
228 ga["toplevel_orm"] = True
229
230 if process_criteria_for_toplevel:
231 for opt in statement._with_options:
232 if opt._is_criteria_option:
233 opt.process_compile_state(self)
234
235 return
236 elif ga.get("toplevel_orm", False):
237 return
238
239 stack_0 = compiler.stack[0]
240
241 try:
242 toplevel_stmt = stack_0["selectable"]
243 except KeyError:
244 pass
245 else:
246 for opt in toplevel_stmt._with_options:
247 if opt._is_compile_state and opt._is_criteria_option:
248 opt.process_compile_state(self)
249
250 ga["toplevel_orm"] = True
251
252 @classmethod
253 def create_for_statement(
254 cls,
255 statement: Union[Select, FromStatement],
256 compiler: Optional[SQLCompiler],
257 **kw: Any,
258 ) -> AbstractORMCompileState:
259 """Create a context for a statement given a :class:`.Compiler`.
260
261 This method is always invoked in the context of SQLCompiler.process().
262
263 For a Select object, this would be invoked from
264 SQLCompiler.visit_select(). For the special FromStatement object used
265 by Query to indicate "Query.from_statement()", this is called by
266 FromStatement._compiler_dispatch() that would be called by
267 SQLCompiler.process().
268 """
269 return super().create_for_statement(statement, compiler, **kw)
270
271 @classmethod
272 def orm_pre_session_exec(
273 cls,
274 session,
275 statement,
276 params,
277 execution_options,
278 bind_arguments,
279 is_pre_event,
280 ):
281 raise NotImplementedError()
282
283 @classmethod
284 def orm_execute_statement(
285 cls,
286 session,
287 statement,
288 params,
289 execution_options,
290 bind_arguments,
291 conn,
292 ) -> Result:
293 result = conn.execute(
294 statement, params or {}, execution_options=execution_options
295 )
296 return cls.orm_setup_cursor_result(
297 session,
298 statement,
299 params,
300 execution_options,
301 bind_arguments,
302 result,
303 )
304
305 @classmethod
306 def orm_setup_cursor_result(
307 cls,
308 session,
309 statement,
310 params,
311 execution_options,
312 bind_arguments,
313 result,
314 ):
315 raise NotImplementedError()
316
317
318class AutoflushOnlyORMCompileState(AbstractORMCompileState):
319 """ORM compile state that is a passthrough, except for autoflush."""
320
321 @classmethod
322 def orm_pre_session_exec(
323 cls,
324 session,
325 statement,
326 params,
327 execution_options,
328 bind_arguments,
329 is_pre_event,
330 ):
331 # consume result-level load_options. These may have been set up
332 # in an ORMExecuteState hook
333 (
334 load_options,
335 execution_options,
336 ) = QueryContext.default_load_options.from_execution_options(
337 "_sa_orm_load_options",
338 {
339 "autoflush",
340 },
341 execution_options,
342 statement._execution_options,
343 )
344
345 if not is_pre_event and load_options._autoflush:
346 session._autoflush()
347
348 return statement, execution_options
349
350 @classmethod
351 def orm_setup_cursor_result(
352 cls,
353 session,
354 statement,
355 params,
356 execution_options,
357 bind_arguments,
358 result,
359 ):
360 return result
361
362
363class ORMCompileState(AbstractORMCompileState):
364 class default_compile_options(CacheableOptions):
365 _cache_key_traversal = [
366 ("_use_legacy_query_style", InternalTraversal.dp_boolean),
367 ("_for_statement", InternalTraversal.dp_boolean),
368 ("_bake_ok", InternalTraversal.dp_boolean),
369 ("_current_path", InternalTraversal.dp_has_cache_key),
370 ("_enable_single_crit", InternalTraversal.dp_boolean),
371 ("_enable_eagerloads", InternalTraversal.dp_boolean),
372 ("_only_load_props", InternalTraversal.dp_plain_obj),
373 ("_set_base_alias", InternalTraversal.dp_boolean),
374 ("_for_refresh_state", InternalTraversal.dp_boolean),
375 ("_render_for_subquery", InternalTraversal.dp_boolean),
376 ("_is_star", InternalTraversal.dp_boolean),
377 ]
378
379 # set to True by default from Query._statement_20(), to indicate
380 # the rendered query should look like a legacy ORM query. right
381 # now this basically indicates we should use tablename_columnname
382 # style labels. Generally indicates the statement originated
383 # from a Query object.
384 _use_legacy_query_style = False
385
386 # set *only* when we are coming from the Query.statement
387 # accessor, or a Query-level equivalent such as
388 # query.subquery(). this supersedes "toplevel".
389 _for_statement = False
390
391 _bake_ok = True
392 _current_path = _path_registry
393 _enable_single_crit = True
394 _enable_eagerloads = True
395 _only_load_props = None
396 _set_base_alias = False
397 _for_refresh_state = False
398 _render_for_subquery = False
399 _is_star = False
400
401 attributes: Dict[Any, Any]
402 global_attributes: Dict[Any, Any]
403
404 statement: Union[Select[Any], FromStatement[Any]]
405 select_statement: Union[Select[Any], FromStatement[Any]]
406 _entities: List[_QueryEntity]
407 _polymorphic_adapters: Dict[_InternalEntityType, ORMAdapter]
408 compile_options: Union[
409 Type[default_compile_options], default_compile_options
410 ]
411 _primary_entity: Optional[_QueryEntity]
412 use_legacy_query_style: bool
413 _label_convention: _LabelConventionCallable
414 primary_columns: List[ColumnElement[Any]]
415 secondary_columns: List[ColumnElement[Any]]
416 dedupe_columns: Set[ColumnElement[Any]]
417 create_eager_joins: List[
418 # TODO: this structure is set up by JoinedLoader
419 Tuple[Any, ...]
420 ]
421 current_path: PathRegistry = _path_registry
422 _has_mapper_entities = False
423
424 def __init__(self, *arg, **kw):
425 raise NotImplementedError()
426
427 if TYPE_CHECKING:
428
429 @classmethod
430 def create_for_statement(
431 cls,
432 statement: Union[Select, FromStatement],
433 compiler: Optional[SQLCompiler],
434 **kw: Any,
435 ) -> ORMCompileState: ...
436
437 def _append_dedupe_col_collection(self, obj, col_collection):
438 dedupe = self.dedupe_columns
439 if obj not in dedupe:
440 dedupe.add(obj)
441 col_collection.append(obj)
442
443 @classmethod
444 def _column_naming_convention(
445 cls, label_style: SelectLabelStyle, legacy: bool
446 ) -> _LabelConventionCallable:
447 if legacy:
448
449 def name(col, col_name=None):
450 if col_name:
451 return col_name
452 else:
453 return getattr(col, "key")
454
455 return name
456 else:
457 return SelectState._column_naming_convention(label_style)
458
459 @classmethod
460 def get_column_descriptions(cls, statement):
461 return _column_descriptions(statement)
462
463 @classmethod
464 def orm_pre_session_exec(
465 cls,
466 session,
467 statement,
468 params,
469 execution_options,
470 bind_arguments,
471 is_pre_event,
472 ):
473 # consume result-level load_options. These may have been set up
474 # in an ORMExecuteState hook
475 (
476 load_options,
477 execution_options,
478 ) = QueryContext.default_load_options.from_execution_options(
479 "_sa_orm_load_options",
480 {
481 "populate_existing",
482 "autoflush",
483 "yield_per",
484 "identity_token",
485 "sa_top_level_orm_context",
486 },
487 execution_options,
488 statement._execution_options,
489 )
490
491 # default execution options for ORM results:
492 # 1. _result_disable_adapt_to_context=True
493 # this will disable the ResultSetMetadata._adapt_to_context()
494 # step which we don't need, as we have result processors cached
495 # against the original SELECT statement before caching.
496
497 if "sa_top_level_orm_context" in execution_options:
498 ctx = execution_options["sa_top_level_orm_context"]
499 execution_options = ctx.query._execution_options.merge_with(
500 ctx.execution_options, execution_options
501 )
502
503 if not execution_options:
504 execution_options = _orm_load_exec_options
505 else:
506 execution_options = execution_options.union(_orm_load_exec_options)
507
508 # would have been placed here by legacy Query only
509 if load_options._yield_per:
510 execution_options = execution_options.union(
511 {"yield_per": load_options._yield_per}
512 )
513
514 if (
515 getattr(statement._compile_options, "_current_path", None)
516 and len(statement._compile_options._current_path) > 10
517 and execution_options.get("compiled_cache", True) is not None
518 ):
519 execution_options: util.immutabledict[str, Any] = (
520 execution_options.union(
521 {
522 "compiled_cache": None,
523 "_cache_disable_reason": "excess depth for "
524 "ORM loader options",
525 }
526 )
527 )
528
529 bind_arguments["clause"] = statement
530
531 # new in 1.4 - the coercions system is leveraged to allow the
532 # "subject" mapper of a statement be propagated to the top
533 # as the statement is built. "subject" mapper is the generally
534 # standard object used as an identifier for multi-database schemes.
535
536 # we are here based on the fact that _propagate_attrs contains
537 # "compile_state_plugin": "orm". The "plugin_subject"
538 # needs to be present as well.
539
540 try:
541 plugin_subject = statement._propagate_attrs["plugin_subject"]
542 except KeyError:
543 assert False, "statement had 'orm' plugin but no plugin_subject"
544 else:
545 if plugin_subject:
546 bind_arguments["mapper"] = plugin_subject.mapper
547
548 if not is_pre_event and load_options._autoflush:
549 session._autoflush()
550
551 return statement, execution_options
552
553 @classmethod
554 def orm_setup_cursor_result(
555 cls,
556 session,
557 statement,
558 params,
559 execution_options,
560 bind_arguments,
561 result,
562 ):
563 execution_context = result.context
564 compile_state = execution_context.compiled.compile_state
565
566 # cover edge case where ORM entities used in legacy select
567 # were passed to session.execute:
568 # session.execute(legacy_select([User.id, User.name]))
569 # see test_query->test_legacy_tuple_old_select
570
571 load_options = execution_options.get(
572 "_sa_orm_load_options", QueryContext.default_load_options
573 )
574
575 if compile_state.compile_options._is_star:
576 return result
577
578 querycontext = QueryContext(
579 compile_state,
580 statement,
581 params,
582 session,
583 load_options,
584 execution_options,
585 bind_arguments,
586 )
587 return loading.instances(result, querycontext)
588
589 @property
590 def _lead_mapper_entities(self):
591 """return all _MapperEntity objects in the lead entities collection.
592
593 Does **not** include entities that have been replaced by
594 with_entities(), with_only_columns()
595
596 """
597 return [
598 ent for ent in self._entities if isinstance(ent, _MapperEntity)
599 ]
600
601 def _create_with_polymorphic_adapter(self, ext_info, selectable):
602 """given MapperEntity or ORMColumnEntity, setup polymorphic loading
603 if called for by the Mapper.
604
605 As of #8168 in 2.0.0rc1, polymorphic adapters, which greatly increase
606 the complexity of the query creation process, are not used at all
607 except in the quasi-legacy cases of with_polymorphic referring to an
608 alias and/or subquery. This would apply to concrete polymorphic
609 loading, and joined inheritance where a subquery is
610 passed to with_polymorphic (which is completely unnecessary in modern
611 use).
612
613 """
614 if (
615 not ext_info.is_aliased_class
616 and ext_info.mapper.persist_selectable
617 not in self._polymorphic_adapters
618 ):
619 for mp in ext_info.mapper.iterate_to_root():
620 self._mapper_loads_polymorphically_with(
621 mp,
622 ORMAdapter(
623 _TraceAdaptRole.WITH_POLYMORPHIC_ADAPTER,
624 mp,
625 equivalents=mp._equivalent_columns,
626 selectable=selectable,
627 ),
628 )
629
630 def _mapper_loads_polymorphically_with(self, mapper, adapter):
631 for m2 in mapper._with_polymorphic_mappers or [mapper]:
632 self._polymorphic_adapters[m2] = adapter
633
634 for m in m2.iterate_to_root():
635 self._polymorphic_adapters[m.local_table] = adapter
636
637 @classmethod
638 def _create_entities_collection(cls, query, legacy):
639 raise NotImplementedError(
640 "this method only works for ORMSelectCompileState"
641 )
642
643
644class DMLReturningColFilter:
645 """an adapter used for the DML RETURNING case.
646
647 Has a subset of the interface used by
648 :class:`.ORMAdapter` and is used for :class:`._QueryEntity`
649 instances to set up their columns as used in RETURNING for a
650 DML statement.
651
652 """
653
654 __slots__ = ("mapper", "columns", "__weakref__")
655
656 def __init__(self, target_mapper, immediate_dml_mapper):
657 if (
658 immediate_dml_mapper is not None
659 and target_mapper.local_table
660 is not immediate_dml_mapper.local_table
661 ):
662 # joined inh, or in theory other kinds of multi-table mappings
663 self.mapper = immediate_dml_mapper
664 else:
665 # single inh, normal mappings, etc.
666 self.mapper = target_mapper
667 self.columns = self.columns = util.WeakPopulateDict(
668 self.adapt_check_present # type: ignore
669 )
670
671 def __call__(self, col, as_filter):
672 for cc in sql_util._find_columns(col):
673 c2 = self.adapt_check_present(cc)
674 if c2 is not None:
675 return col
676 else:
677 return None
678
679 def adapt_check_present(self, col):
680 mapper = self.mapper
681 prop = mapper._columntoproperty.get(col, None)
682 if prop is None:
683 return None
684 return mapper.local_table.c.corresponding_column(col)
685
686
687@sql.base.CompileState.plugin_for("orm", "orm_from_statement")
688class ORMFromStatementCompileState(ORMCompileState):
689 _from_obj_alias = None
690 _has_mapper_entities = False
691
692 statement_container: FromStatement
693 requested_statement: Union[SelectBase, TextClause, UpdateBase]
694 dml_table: Optional[_DMLTableElement] = None
695
696 _has_orm_entities = False
697 multi_row_eager_loaders = False
698 eager_adding_joins = False
699 compound_eager_adapter = None
700
701 extra_criteria_entities = _EMPTY_DICT
702 eager_joins = _EMPTY_DICT
703
704 @classmethod
705 def create_for_statement(
706 cls,
707 statement_container: Union[Select, FromStatement],
708 compiler: Optional[SQLCompiler],
709 **kw: Any,
710 ) -> ORMFromStatementCompileState:
711 assert isinstance(statement_container, FromStatement)
712
713 if compiler is not None and compiler.stack:
714 raise sa_exc.CompileError(
715 "The ORM FromStatement construct only supports being "
716 "invoked as the topmost statement, as it is only intended to "
717 "define how result rows should be returned."
718 )
719
720 self = cls.__new__(cls)
721 self._primary_entity = None
722
723 self.use_legacy_query_style = (
724 statement_container._compile_options._use_legacy_query_style
725 )
726 self.statement_container = self.select_statement = statement_container
727 self.requested_statement = statement = statement_container.element
728
729 if statement.is_dml:
730 self.dml_table = statement.table
731 self.is_dml_returning = True
732
733 self._entities = []
734 self._polymorphic_adapters = {}
735
736 self.compile_options = statement_container._compile_options
737
738 if (
739 self.use_legacy_query_style
740 and isinstance(statement, expression.SelectBase)
741 and not statement._is_textual
742 and not statement.is_dml
743 and statement._label_style is LABEL_STYLE_NONE
744 ):
745 self.statement = statement.set_label_style(
746 LABEL_STYLE_TABLENAME_PLUS_COL
747 )
748 else:
749 self.statement = statement
750
751 self._label_convention = self._column_naming_convention(
752 (
753 statement._label_style
754 if not statement._is_textual and not statement.is_dml
755 else LABEL_STYLE_NONE
756 ),
757 self.use_legacy_query_style,
758 )
759
760 _QueryEntity.to_compile_state(
761 self,
762 statement_container._raw_columns,
763 self._entities,
764 is_current_entities=True,
765 )
766
767 self.current_path = statement_container._compile_options._current_path
768
769 self._init_global_attributes(
770 statement_container,
771 compiler,
772 process_criteria_for_toplevel=False,
773 toplevel=True,
774 )
775
776 if statement_container._with_options:
777 for opt in statement_container._with_options:
778 if opt._is_compile_state:
779 opt.process_compile_state(self)
780
781 if statement_container._with_context_options:
782 for fn, key in statement_container._with_context_options:
783 fn(self)
784
785 self.primary_columns = []
786 self.secondary_columns = []
787 self.dedupe_columns = set()
788 self.create_eager_joins = []
789 self._fallback_from_clauses = []
790
791 self.order_by = None
792
793 if isinstance(self.statement, expression.TextClause):
794 # TextClause has no "column" objects at all. for this case,
795 # we generate columns from our _QueryEntity objects, then
796 # flip on all the "please match no matter what" parameters.
797 self.extra_criteria_entities = {}
798
799 for entity in self._entities:
800 entity.setup_compile_state(self)
801
802 compiler._ordered_columns = compiler._textual_ordered_columns = (
803 False
804 )
805
806 # enable looser result column matching. this is shown to be
807 # needed by test_query.py::TextTest
808 compiler._loose_column_name_matching = True
809
810 for c in self.primary_columns:
811 compiler.process(
812 c,
813 within_columns_clause=True,
814 add_to_result_map=compiler._add_to_result_map,
815 )
816 else:
817 # for everyone else, Select, Insert, Update, TextualSelect, they
818 # have column objects already. After much
819 # experimentation here, the best approach seems to be, use
820 # those columns completely, don't interfere with the compiler
821 # at all; just in ORM land, use an adapter to convert from
822 # our ORM columns to whatever columns are in the statement,
823 # before we look in the result row. Adapt on names
824 # to accept cases such as issue #9217, however also allow
825 # this to be overridden for cases such as #9273.
826 self._from_obj_alias = ORMStatementAdapter(
827 _TraceAdaptRole.ADAPT_FROM_STATEMENT,
828 self.statement,
829 adapt_on_names=statement_container._adapt_on_names,
830 )
831
832 return self
833
834 def _adapt_col_list(self, cols, current_adapter):
835 return cols
836
837 def _get_current_adapter(self):
838 return None
839
840 def setup_dml_returning_compile_state(self, dml_mapper):
841 """used by BulkORMInsert (and Update / Delete?) to set up a handler
842 for RETURNING to return ORM objects and expressions
843
844 """
845 target_mapper = self.statement._propagate_attrs.get(
846 "plugin_subject", None
847 )
848 adapter = DMLReturningColFilter(target_mapper, dml_mapper)
849
850 if self.compile_options._is_star and (len(self._entities) != 1):
851 raise sa_exc.CompileError(
852 "Can't generate ORM query that includes multiple expressions "
853 "at the same time as '*'; query for '*' alone if present"
854 )
855
856 for entity in self._entities:
857 entity.setup_dml_returning_compile_state(self, adapter)
858
859
860class FromStatement(GroupedElement, Generative, TypedReturnsRows[_TP]):
861 """Core construct that represents a load of ORM objects from various
862 :class:`.ReturnsRows` and other classes including:
863
864 :class:`.Select`, :class:`.TextClause`, :class:`.TextualSelect`,
865 :class:`.CompoundSelect`, :class`.Insert`, :class:`.Update`,
866 and in theory, :class:`.Delete`.
867
868 """
869
870 __visit_name__ = "orm_from_statement"
871
872 _compile_options = ORMFromStatementCompileState.default_compile_options
873
874 _compile_state_factory = ORMFromStatementCompileState.create_for_statement
875
876 _for_update_arg = None
877
878 element: Union[ExecutableReturnsRows, TextClause]
879
880 _adapt_on_names: bool
881
882 _traverse_internals = [
883 ("_raw_columns", InternalTraversal.dp_clauseelement_list),
884 ("element", InternalTraversal.dp_clauseelement),
885 ] + Executable._executable_traverse_internals
886
887 _cache_key_traversal = _traverse_internals + [
888 ("_compile_options", InternalTraversal.dp_has_cache_key)
889 ]
890
891 is_from_statement = True
892
893 def __init__(
894 self,
895 entities: Iterable[_ColumnsClauseArgument[Any]],
896 element: Union[ExecutableReturnsRows, TextClause],
897 _adapt_on_names: bool = True,
898 ):
899 self._raw_columns = [
900 coercions.expect(
901 roles.ColumnsClauseRole,
902 ent,
903 apply_propagate_attrs=self,
904 post_inspect=True,
905 )
906 for ent in util.to_list(entities)
907 ]
908 self.element = element
909 self.is_dml = element.is_dml
910 self.is_select = element.is_select
911 self.is_delete = element.is_delete
912 self.is_insert = element.is_insert
913 self.is_update = element.is_update
914 self._label_style = (
915 element._label_style if is_select_base(element) else None
916 )
917 self._adapt_on_names = _adapt_on_names
918
919 def _compiler_dispatch(self, compiler, **kw):
920 """provide a fixed _compiler_dispatch method.
921
922 This is roughly similar to using the sqlalchemy.ext.compiler
923 ``@compiles`` extension.
924
925 """
926
927 compile_state = self._compile_state_factory(self, compiler, **kw)
928
929 toplevel = not compiler.stack
930
931 if toplevel:
932 compiler.compile_state = compile_state
933
934 return compiler.process(compile_state.statement, **kw)
935
936 @property
937 def column_descriptions(self):
938 """Return a :term:`plugin-enabled` 'column descriptions' structure
939 referring to the columns which are SELECTed by this statement.
940
941 See the section :ref:`queryguide_inspection` for an overview
942 of this feature.
943
944 .. seealso::
945
946 :ref:`queryguide_inspection` - ORM background
947
948 """
949 meth = cast(
950 ORMSelectCompileState, SelectState.get_plugin_class(self)
951 ).get_column_descriptions
952 return meth(self)
953
954 def _ensure_disambiguated_names(self):
955 return self
956
957 def get_children(self, **kw):
958 yield from itertools.chain.from_iterable(
959 element._from_objects for element in self._raw_columns
960 )
961 yield from super().get_children(**kw)
962
963 @property
964 def _all_selected_columns(self):
965 return self.element._all_selected_columns
966
967 @property
968 def _return_defaults(self):
969 return self.element._return_defaults if is_dml(self.element) else None
970
971 @property
972 def _returning(self):
973 return self.element._returning if is_dml(self.element) else None
974
975 @property
976 def _inline(self):
977 return self.element._inline if is_insert_update(self.element) else None
978
979
980@sql.base.CompileState.plugin_for("orm", "compound_select")
981class CompoundSelectCompileState(
982 AutoflushOnlyORMCompileState, CompoundSelectState
983):
984 pass
985
986
987@sql.base.CompileState.plugin_for("orm", "select")
988class ORMSelectCompileState(ORMCompileState, SelectState):
989 _already_joined_edges = ()
990
991 _memoized_entities = _EMPTY_DICT
992
993 _from_obj_alias = None
994 _has_mapper_entities = False
995
996 _has_orm_entities = False
997 multi_row_eager_loaders = False
998 eager_adding_joins = False
999 compound_eager_adapter = None
1000
1001 correlate = None
1002 correlate_except = None
1003 _where_criteria = ()
1004 _having_criteria = ()
1005
1006 @classmethod
1007 def create_for_statement(
1008 cls,
1009 statement: Union[Select, FromStatement],
1010 compiler: Optional[SQLCompiler],
1011 **kw: Any,
1012 ) -> ORMSelectCompileState:
1013 """compiler hook, we arrive here from compiler.visit_select() only."""
1014
1015 self = cls.__new__(cls)
1016
1017 if compiler is not None:
1018 toplevel = not compiler.stack
1019 else:
1020 toplevel = True
1021
1022 select_statement = statement
1023
1024 # if we are a select() that was never a legacy Query, we won't
1025 # have ORM level compile options.
1026 statement._compile_options = cls.default_compile_options.safe_merge(
1027 statement._compile_options
1028 )
1029
1030 if select_statement._execution_options:
1031 # execution options should not impact the compilation of a
1032 # query, and at the moment subqueryloader is putting some things
1033 # in here that we explicitly don't want stuck in a cache.
1034 self.select_statement = select_statement._clone()
1035 self.select_statement._execution_options = util.immutabledict()
1036 else:
1037 self.select_statement = select_statement
1038
1039 # indicates this select() came from Query.statement
1040 self.for_statement = select_statement._compile_options._for_statement
1041
1042 # generally if we are from Query or directly from a select()
1043 self.use_legacy_query_style = (
1044 select_statement._compile_options._use_legacy_query_style
1045 )
1046
1047 self._entities = []
1048 self._primary_entity = None
1049 self._polymorphic_adapters = {}
1050
1051 self.compile_options = select_statement._compile_options
1052
1053 if not toplevel:
1054 # for subqueries, turn off eagerloads and set
1055 # "render_for_subquery".
1056 self.compile_options += {
1057 "_enable_eagerloads": False,
1058 "_render_for_subquery": True,
1059 }
1060
1061 # determine label style. we can make different decisions here.
1062 # at the moment, trying to see if we can always use DISAMBIGUATE_ONLY
1063 # rather than LABEL_STYLE_NONE, and if we can use disambiguate style
1064 # for new style ORM selects too.
1065 if (
1066 self.use_legacy_query_style
1067 and self.select_statement._label_style is LABEL_STYLE_LEGACY_ORM
1068 ):
1069 if not self.for_statement:
1070 self.label_style = LABEL_STYLE_TABLENAME_PLUS_COL
1071 else:
1072 self.label_style = LABEL_STYLE_DISAMBIGUATE_ONLY
1073 else:
1074 self.label_style = self.select_statement._label_style
1075
1076 if select_statement._memoized_select_entities:
1077 self._memoized_entities = {
1078 memoized_entities: _QueryEntity.to_compile_state(
1079 self,
1080 memoized_entities._raw_columns,
1081 [],
1082 is_current_entities=False,
1083 )
1084 for memoized_entities in (
1085 select_statement._memoized_select_entities
1086 )
1087 }
1088
1089 # label_convention is stateful and will yield deduping keys if it
1090 # sees the same key twice. therefore it's important that it is not
1091 # invoked for the above "memoized" entities that aren't actually
1092 # in the columns clause
1093 self._label_convention = self._column_naming_convention(
1094 statement._label_style, self.use_legacy_query_style
1095 )
1096
1097 _QueryEntity.to_compile_state(
1098 self,
1099 select_statement._raw_columns,
1100 self._entities,
1101 is_current_entities=True,
1102 )
1103
1104 self.current_path = select_statement._compile_options._current_path
1105
1106 self.eager_order_by = ()
1107
1108 self._init_global_attributes(
1109 select_statement,
1110 compiler,
1111 toplevel=toplevel,
1112 process_criteria_for_toplevel=False,
1113 )
1114
1115 if toplevel and (
1116 select_statement._with_options
1117 or select_statement._memoized_select_entities
1118 ):
1119 for (
1120 memoized_entities
1121 ) in select_statement._memoized_select_entities:
1122 for opt in memoized_entities._with_options:
1123 if opt._is_compile_state:
1124 opt.process_compile_state_replaced_entities(
1125 self,
1126 [
1127 ent
1128 for ent in self._memoized_entities[
1129 memoized_entities
1130 ]
1131 if isinstance(ent, _MapperEntity)
1132 ],
1133 )
1134
1135 for opt in self.select_statement._with_options:
1136 if opt._is_compile_state:
1137 opt.process_compile_state(self)
1138
1139 # uncomment to print out the context.attributes structure
1140 # after it's been set up above
1141 # self._dump_option_struct()
1142
1143 if select_statement._with_context_options:
1144 for fn, key in select_statement._with_context_options:
1145 fn(self)
1146
1147 self.primary_columns = []
1148 self.secondary_columns = []
1149 self.dedupe_columns = set()
1150 self.eager_joins = {}
1151 self.extra_criteria_entities = {}
1152 self.create_eager_joins = []
1153 self._fallback_from_clauses = []
1154
1155 # normalize the FROM clauses early by themselves, as this makes
1156 # it an easier job when we need to assemble a JOIN onto these,
1157 # for select.join() as well as joinedload(). As of 1.4 there are now
1158 # potentially more complex sets of FROM objects here as the use
1159 # of lambda statements for lazyload, load_on_pk etc. uses more
1160 # cloning of the select() construct. See #6495
1161 self.from_clauses = self._normalize_froms(
1162 info.selectable for info in select_statement._from_obj
1163 )
1164
1165 # this is a fairly arbitrary break into a second method,
1166 # so it might be nicer to break up create_for_statement()
1167 # and _setup_for_generate into three or four logical sections
1168 self._setup_for_generate()
1169
1170 SelectState.__init__(self, self.statement, compiler, **kw)
1171 return self
1172
1173 def _dump_option_struct(self):
1174 print("\n---------------------------------------------------\n")
1175 print(f"current path: {self.current_path}")
1176 for key in self.attributes:
1177 if isinstance(key, tuple) and key[0] == "loader":
1178 print(f"\nLoader: {PathRegistry.coerce(key[1])}")
1179 print(f" {self.attributes[key]}")
1180 print(f" {self.attributes[key].__dict__}")
1181 elif isinstance(key, tuple) and key[0] == "path_with_polymorphic":
1182 print(f"\nWith Polymorphic: {PathRegistry.coerce(key[1])}")
1183 print(f" {self.attributes[key]}")
1184
1185 def _setup_for_generate(self):
1186 query = self.select_statement
1187
1188 self.statement = None
1189 self._join_entities = ()
1190
1191 if self.compile_options._set_base_alias:
1192 # legacy Query only
1193 self._set_select_from_alias()
1194
1195 for memoized_entities in query._memoized_select_entities:
1196 if memoized_entities._setup_joins:
1197 self._join(
1198 memoized_entities._setup_joins,
1199 self._memoized_entities[memoized_entities],
1200 )
