1# orm/context.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
9from __future__ import annotations
10
11import collections
12import itertools
13from typing import Any
14from typing import cast
15from typing import Dict
16from typing import Iterable
17from typing import List
18from typing import Optional
19from typing import Set
20from typing import Tuple
21from typing import Type
22from typing import TYPE_CHECKING
23from typing import TypeVar
24from typing import Union
25
26from . import attributes
27from . import interfaces
28from . import loading
29from .base import _is_aliased_class
30from .interfaces import ORMColumnDescription
31from .interfaces import ORMColumnsClauseRole
32from .path_registry import PathRegistry
33from .util import _entity_corresponds_to
34from .util import _ORMJoin
35from .util import _TraceAdaptRole
36from .util import AliasedClass
37from .util import Bundle
38from .util import ORMAdapter
39from .util import ORMStatementAdapter
40from .. import exc as sa_exc
41from .. import future
42from .. import inspect
43from .. import sql
44from .. import util
45from ..sql import coercions
46from ..sql import expression
47from ..sql import roles
48from ..sql import util as sql_util
49from ..sql import visitors
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 ExecutableStatement
58from ..sql.base import Generative
59from ..sql.base import Options
60from ..sql.dml import UpdateBase
61from ..sql.elements import GroupedElement
62from ..sql.elements import TextClause
63from ..sql.selectable import CompoundSelectState
64from ..sql.selectable import LABEL_STYLE_DISAMBIGUATE_ONLY
65from ..sql.selectable import LABEL_STYLE_NONE
66from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
67from ..sql.selectable import Select
68from ..sql.selectable import SelectLabelStyle
69from ..sql.selectable import SelectState
70from ..sql.selectable import TypedReturnsRows
71from ..sql.visitors import InternalTraversal
72from ..util.typing import TupleAny
73from ..util.typing import TypeVarTuple
74from ..util.typing import Unpack
75
76if TYPE_CHECKING:
77 from ._typing import _InternalEntityType
78 from ._typing import OrmExecuteOptionsParameter
79 from .loading import _PostLoad
80 from .mapper import Mapper
81 from .query import Query
82 from .session import _BindArguments
83 from .session import Session
84 from ..engine import Result
85 from ..engine.interfaces import _CoreSingleExecuteParams
86 from ..sql._typing import _ColumnsClauseArgument
87 from ..sql.compiler import SQLCompiler
88 from ..sql.dml import _DMLTableElement
89 from ..sql.elements import ColumnElement
90 from ..sql.selectable import _JoinTargetElement
91 from ..sql.selectable import _LabelConventionCallable
92 from ..sql.selectable import _SetupJoinsElement
93 from ..sql.selectable import ExecutableReturnsRows
94 from ..sql.selectable import SelectBase
95 from ..sql.type_api import TypeEngine
96
97_T = TypeVar("_T", bound=Any)
98_Ts = TypeVarTuple("_Ts")
99_path_registry = PathRegistry.root
100
101LABEL_STYLE_LEGACY_ORM = SelectLabelStyle.LABEL_STYLE_LEGACY_ORM
102
103
104class QueryContext:
105 __slots__ = (
106 "top_level_context",
107 "compile_state",
108 "query",
109 "user_passed_query",
110 "params",
111 "load_options",
112 "bind_arguments",
113 "execution_options",
114 "session",
115 "autoflush",
116 "populate_existing",
117 "invoke_all_eagers",
118 "version_check",
119 "refresh_state",
120 "create_eager_joins",
121 "propagated_loader_options",
122 "attributes",
123 "runid",
124 "partials",
125 "post_load_paths",
126 "identity_token",
127 "yield_per",
128 "loaders_require_buffering",
129 "loaders_require_uniquing",
130 )
131
132 runid: int
133 post_load_paths: Dict[PathRegistry, _PostLoad]
134 compile_state: _ORMCompileState
135
136 class default_load_options(Options):
137 _only_return_tuples = False
138 _populate_existing = False
139 _version_check = False
140 _invoke_all_eagers = True
141 _autoflush = True
142 _identity_token = None
143 _yield_per = None
144 _refresh_state = None
145 _lazy_loaded_from = None
146 _legacy_uniquing = False
147 _sa_top_level_orm_context = None
148 _is_user_refresh = False
149
150 def __init__(
151 self,
152 compile_state: CompileState,
153 statement: Union[
154 Select[Unpack[TupleAny]],
155 FromStatement[Unpack[TupleAny]],
156 UpdateBase,
157 ],
158 user_passed_query: Union[
159 Select[Unpack[TupleAny]],
160 FromStatement[Unpack[TupleAny]],
161 UpdateBase,
162 ],
163 params: _CoreSingleExecuteParams,
164 session: Session,
165 load_options: Union[
166 Type[QueryContext.default_load_options],
167 QueryContext.default_load_options,
168 ],
169 execution_options: Optional[OrmExecuteOptionsParameter] = None,
170 bind_arguments: Optional[_BindArguments] = None,
171 ):
172 self.load_options = load_options
173 self.execution_options = execution_options or util.EMPTY_DICT
174 self.bind_arguments = bind_arguments or util.EMPTY_DICT
175 self.compile_state = compile_state
176 self.query = statement
177
178 # the query that the end user passed to Session.execute() or similar.
179 # this is usually the same as .query, except in the bulk_persistence
180 # routines where a separate FromStatement is manufactured in the
181 # compile stage; this allows differentiation in that case.
182 self.user_passed_query = user_passed_query
183
184 self.session = session
185 self.loaders_require_buffering = False
186 self.loaders_require_uniquing = False
187 self.params = params
188 self.top_level_context = load_options._sa_top_level_orm_context
189
190 cached_options = compile_state.select_statement._with_options
191 uncached_options = user_passed_query._with_options
192
193 # see issue #7447 , #8399 for some background
194 # propagated loader options will be present on loaded InstanceState
195 # objects under state.load_options and are typically used by
196 # LazyLoader to apply options to the SELECT statement it emits.
197 # For compile state options (i.e. loader strategy options), these
198 # need to line up with the ".load_path" attribute which in
199 # loader.py is pulled from context.compile_state.current_path.
200 # so, this means these options have to be the ones from the
201 # *cached* statement that's travelling with compile_state, not the
202 # *current* statement which won't match up for an ad-hoc
203 # AliasedClass
204 self.propagated_loader_options = tuple(
205 opt._adapt_cached_option_to_uncached_option(self, uncached_opt)
206 for opt, uncached_opt in zip(cached_options, uncached_options)
207 if opt.propagate_to_loaders
208 )
209
210 self.attributes = dict(compile_state.attributes)
211
212 self.autoflush = load_options._autoflush
213 self.populate_existing = load_options._populate_existing
214 self.invoke_all_eagers = load_options._invoke_all_eagers
215 self.version_check = load_options._version_check
216 self.refresh_state = load_options._refresh_state
217 self.yield_per = load_options._yield_per
218 self.identity_token = load_options._identity_token
219
220 def _get_top_level_context(self) -> QueryContext:
221 return self.top_level_context or self
222
223
224_orm_load_exec_options = util.immutabledict(
225 {"_result_disable_adapt_to_context": True}
226)
227
228
229class _AbstractORMCompileState(CompileState):
230 is_dml_returning = False
231
232 def _init_global_attributes(
233 self, statement, compiler, *, toplevel, process_criteria_for_toplevel
234 ):
235 self.attributes = {}
236
237 if compiler is None:
238 # this is the legacy / testing only ORM _compile_state() use case.
239 # there is no need to apply criteria options for this.
240 self.global_attributes = {}
241 assert toplevel
242 return
243 else:
244 self.global_attributes = ga = compiler._global_attributes
245
246 if toplevel:
247 ga["toplevel_orm"] = True
248
249 if process_criteria_for_toplevel:
250 for opt in statement._with_options:
251 if opt._is_criteria_option:
252 opt.process_compile_state(self)
253
254 return
255 elif ga.get("toplevel_orm", False):
256 return
257
258 stack_0 = compiler.stack[0]
259
260 try:
261 toplevel_stmt = stack_0["selectable"]
262 except KeyError:
263 pass
264 else:
265 for opt in toplevel_stmt._with_options:
266 if opt._is_compile_state and opt._is_criteria_option:
267 opt.process_compile_state(self)
268
269 ga["toplevel_orm"] = True
270
271 @classmethod
272 def create_for_statement(
273 cls,
274 statement: Executable,
275 compiler: SQLCompiler,
276 **kw: Any,
277 ) -> CompileState:
278 """Create a context for a statement given a :class:`.Compiler`.
279
280 This method is always invoked in the context of SQLCompiler.process().
281
282 For a Select object, this would be invoked from
283 SQLCompiler.visit_select(). For the special FromStatement object used
284 by Query to indicate "Query.from_statement()", this is called by
285 FromStatement._compiler_dispatch() that would be called by
286 SQLCompiler.process().
287 """
288 return super().create_for_statement(statement, compiler, **kw)
289
290 @classmethod
291 def orm_pre_session_exec(
292 cls,
293 session,
294 statement,
295 params,
296 execution_options,
297 bind_arguments,
298 is_pre_event,
299 ):
300 raise NotImplementedError()
301
302 @classmethod
303 def orm_execute_statement(
304 cls,
305 session,
306 statement,
307 params,
308 execution_options,
309 bind_arguments,
310 conn,
311 ) -> Result:
312 result = conn.execute(
313 statement, params or {}, execution_options=execution_options
314 )
315 return cls.orm_setup_cursor_result(
316 session,
317 statement,
318 params,
319 execution_options,
320 bind_arguments,
321 result,
322 )
323
324 @classmethod
325 def orm_setup_cursor_result(
326 cls,
327 session,
328 statement,
329 params,
330 execution_options,
331 bind_arguments,
332 result,
333 ):
334 raise NotImplementedError()
335
336
337class _AutoflushOnlyORMCompileState(_AbstractORMCompileState):
338 """ORM compile state that is a passthrough, except for autoflush."""
339
340 @classmethod
341 def orm_pre_session_exec(
342 cls,
343 session,
344 statement,
345 params,
346 execution_options,
347 bind_arguments,
348 is_pre_event,
349 ):
350 # consume result-level load_options. These may have been set up
351 # in an ORMExecuteState hook
352 (
353 load_options,
354 execution_options,
355 ) = QueryContext.default_load_options.from_execution_options(
356 "_sa_orm_load_options",
357 {
358 "autoflush",
359 },
360 execution_options,
361 statement._execution_options,
362 )
363
364 if not is_pre_event and load_options._autoflush:
365 session._autoflush()
366
367 return statement, execution_options, params
368
369 @classmethod
370 def orm_setup_cursor_result(
371 cls,
372 session,
373 statement,
374 params,
375 execution_options,
376 bind_arguments,
377 result,
378 ):
379 return result
380
381
382class _ORMCompileState(_AbstractORMCompileState):
383 class default_compile_options(CacheableOptions):
384 _cache_key_traversal = [
385 ("_use_legacy_query_style", InternalTraversal.dp_boolean),
386 ("_for_statement", InternalTraversal.dp_boolean),
387 ("_bake_ok", InternalTraversal.dp_boolean),
388 ("_current_path", InternalTraversal.dp_has_cache_key),
389 ("_enable_single_crit", InternalTraversal.dp_boolean),
390 ("_enable_eagerloads", InternalTraversal.dp_boolean),
391 ("_only_load_props", InternalTraversal.dp_plain_obj),
392 ("_set_base_alias", InternalTraversal.dp_boolean),
393 ("_for_refresh_state", InternalTraversal.dp_boolean),
394 ("_render_for_subquery", InternalTraversal.dp_boolean),
395 ("_is_star", InternalTraversal.dp_boolean),
396 ]
397
398 # set to True by default from Query._statement_20(), to indicate
399 # the rendered query should look like a legacy ORM query. right
400 # now this basically indicates we should use tablename_columnname
401 # style labels. Generally indicates the statement originated
402 # from a Query object.
403 _use_legacy_query_style = False
404
405 # set *only* when we are coming from the Query.statement
406 # accessor, or a Query-level equivalent such as
407 # query.subquery(). this supersedes "toplevel".
408 _for_statement = False
409
410 _bake_ok = True
411 _current_path = _path_registry
412 _enable_single_crit = True
413 _enable_eagerloads = True
414 _only_load_props = None
415 _set_base_alias = False
416 _for_refresh_state = False
417 _render_for_subquery = False
418 _is_star = False
419
420 attributes: Dict[Any, Any]
421 global_attributes: Dict[Any, Any]
422
423 statement: Union[
424 Select[Unpack[TupleAny]], FromStatement[Unpack[TupleAny]], UpdateBase
425 ]
426 select_statement: Union[
427 Select[Unpack[TupleAny]], FromStatement[Unpack[TupleAny]]
428 ]
429 _entities: List[_QueryEntity]
430 _polymorphic_adapters: Dict[_InternalEntityType, ORMAdapter]
431 compile_options: Union[
432 Type[default_compile_options], default_compile_options
433 ]
434 _primary_entity: Optional[_QueryEntity]
435 use_legacy_query_style: bool
436 _label_convention: _LabelConventionCallable
437 primary_columns: List[ColumnElement[Any]]
438 secondary_columns: List[ColumnElement[Any]]
439 dedupe_columns: Set[ColumnElement[Any]]
440 create_eager_joins: List[
441 # TODO: this structure is set up by JoinedLoader
442 TupleAny
443 ]
444 current_path: PathRegistry = _path_registry
445 _has_mapper_entities = False
446
447 def __init__(self, *arg, **kw):
448 raise NotImplementedError()
449
450 @classmethod
451 def create_for_statement(
452 cls,
453 statement: Executable,
454 compiler: SQLCompiler,
455 **kw: Any,
456 ) -> _ORMCompileState:
457 return cls._create_orm_context(
458 cast("Union[Select, FromStatement]", statement),
459 toplevel=not compiler.stack,
460 compiler=compiler,
461 **kw,
462 )
463
464 @classmethod
465 def _create_orm_context(
466 cls,
467 statement: Union[Select, FromStatement],
468 *,
469 toplevel: bool,
470 compiler: Optional[SQLCompiler],
471 **kw: Any,
472 ) -> _ORMCompileState:
473 raise NotImplementedError()
474
475 def _append_dedupe_col_collection(self, obj, col_collection):
476 dedupe = self.dedupe_columns
477 if obj not in dedupe:
478 dedupe.add(obj)
479 col_collection.append(obj)
480
481 @classmethod
482 def _column_naming_convention(
483 cls, label_style: SelectLabelStyle, legacy: bool
484 ) -> _LabelConventionCallable:
485 if legacy:
486
487 def name(col, col_name=None):
488 if col_name:
489 return col_name
490 else:
491 return getattr(col, "key")
492
493 return name
494 else:
495 return SelectState._column_naming_convention(label_style)
496
497 @classmethod
498 def get_column_descriptions(cls, statement):
499 return _column_descriptions(statement)
500
501 @classmethod
502 def orm_pre_session_exec(
503 cls,
504 session,
505 statement,
506 params,
507 execution_options,
508 bind_arguments,
509 is_pre_event,
510 ):
511 # consume result-level load_options. These may have been set up
512 # in an ORMExecuteState hook
513 (
514 load_options,
515 execution_options,
516 ) = QueryContext.default_load_options.from_execution_options(
517 "_sa_orm_load_options",
518 {
519 "populate_existing",
520 "autoflush",
521 "yield_per",
522 "identity_token",
523 "sa_top_level_orm_context",
524 },
525 execution_options,
526 statement._execution_options,
527 )
528
529 # default execution options for ORM results:
530 # 1. _result_disable_adapt_to_context=True
531 # this will disable the ResultSetMetadata._adapt_to_context()
532 # step which we don't need, as we have result processors cached
533 # against the original SELECT statement before caching.
534
535 if "sa_top_level_orm_context" in execution_options:
536 ctx = execution_options["sa_top_level_orm_context"]
537 execution_options = ctx.query._execution_options.merge_with(
538 ctx.execution_options, execution_options
539 )
540
541 if not execution_options:
542 execution_options = _orm_load_exec_options
543 else:
544 execution_options = execution_options.union(_orm_load_exec_options)
545
546 # would have been placed here by legacy Query only
547 if load_options._yield_per:
548 execution_options = execution_options.union(
549 {"yield_per": load_options._yield_per}
550 )
551
552 if (
553 getattr(statement._compile_options, "_current_path", None)
554 and len(statement._compile_options._current_path) > 10
555 and execution_options.get("compiled_cache", True) is not None
556 ):
557 execution_options: util.immutabledict[str, Any] = (
558 execution_options.union(
559 {
560 "compiled_cache": None,
561 "_cache_disable_reason": "excess depth for "
562 "ORM loader options",
563 }
564 )
565 )
566
567 bind_arguments["clause"] = statement
568
569 # new in 1.4 - the coercions system is leveraged to allow the
570 # "subject" mapper of a statement be propagated to the top
571 # as the statement is built. "subject" mapper is the generally
572 # standard object used as an identifier for multi-database schemes.
573
574 # we are here based on the fact that _propagate_attrs contains
575 # "compile_state_plugin": "orm". The "plugin_subject"
576 # needs to be present as well.
577
578 try:
579 plugin_subject = statement._propagate_attrs["plugin_subject"]
580 except KeyError:
581 assert False, "statement had 'orm' plugin but no plugin_subject"
582 else:
583 if plugin_subject:
584 bind_arguments["mapper"] = plugin_subject.mapper
585
586 if not is_pre_event and load_options._autoflush:
587 session._autoflush()
588
589 return statement, execution_options, params
590
591 @classmethod
592 def orm_setup_cursor_result(
593 cls,
594 session,
595 statement,
596 params,
597 execution_options,
598 bind_arguments,
599 result,
600 ):
601 execution_context = result.context
602 compile_state = execution_context.compiled.compile_state
603
604 # cover edge case where ORM entities used in legacy select
605 # were passed to session.execute:
606 # session.execute(legacy_select([User.id, User.name]))
607 # see test_query->test_legacy_tuple_old_select
608
609 load_options = execution_options.get(
610 "_sa_orm_load_options", QueryContext.default_load_options
611 )
612
613 if compile_state.compile_options._is_star:
614 return result
615
616 querycontext = QueryContext(
617 compile_state,
618 statement,
619 statement,
620 params,
621 session,
622 load_options,
623 execution_options,
624 bind_arguments,
625 )
626 return loading.instances(result, querycontext)
627
628 @property
629 def _lead_mapper_entities(self):
630 """return all _MapperEntity objects in the lead entities collection.
631
632 Does **not** include entities that have been replaced by
633 with_entities(), with_only_columns()
634
635 """
636 return [
637 ent for ent in self._entities if isinstance(ent, _MapperEntity)
638 ]
639
640 def _create_with_polymorphic_adapter(self, ext_info, selectable):
641 """given MapperEntity or ORMColumnEntity, setup polymorphic loading
642 if called for by the Mapper.
643
644 As of #8168 in 2.0.0rc1, polymorphic adapters, which greatly increase
645 the complexity of the query creation process, are not used at all
646 except in the quasi-legacy cases of with_polymorphic referring to an
647 alias and/or subquery. This would apply to concrete polymorphic
648 loading, and joined inheritance where a subquery is
649 passed to with_polymorphic (which is completely unnecessary in modern
650 use).
651
652 TODO: What is a "quasi-legacy" case? Do we need this method with
653 2.0 style select() queries or not? Why is with_polymorphic referring
654 to an alias or subquery "legacy" ?
655
656 """
657 if (
658 not ext_info.is_aliased_class
659 and ext_info.mapper.persist_selectable
660 not in self._polymorphic_adapters
661 ):
662 for mp in ext_info.mapper.iterate_to_root():
663 self._mapper_loads_polymorphically_with(
664 mp,
665 ORMAdapter(
666 _TraceAdaptRole.WITH_POLYMORPHIC_ADAPTER,
667 mp,
668 equivalents=mp._equivalent_columns,
669 selectable=selectable,
670 ),
671 )
672
673 def _mapper_loads_polymorphically_with(self, mapper, adapter):
674 for m2 in mapper._with_polymorphic_mappers or [mapper]:
675 self._polymorphic_adapters[m2] = adapter
676
677 for m in m2.iterate_to_root():
678 self._polymorphic_adapters[m.local_table] = adapter
679
680 @classmethod
681 def _create_entities_collection(cls, query, legacy):
682 raise NotImplementedError(
683 "this method only works for ORMSelectCompileState"
684 )
685
686
687class _DMLReturningColFilter:
688 """a base for an adapter used for the DML RETURNING cases
689
690 Has a subset of the interface used by
691 :class:`.ORMAdapter` and is used for :class:`._QueryEntity`
692 instances to set up their columns as used in RETURNING for a
693 DML statement.
694
695 """
696
697 __slots__ = ("mapper", "columns", "__weakref__")
698
699 def __init__(self, target_mapper, immediate_dml_mapper):
700 if (
701 immediate_dml_mapper is not None
702 and target_mapper.local_table
703 is not immediate_dml_mapper.local_table
704 ):
705 # joined inh, or in theory other kinds of multi-table mappings
706 self.mapper = immediate_dml_mapper
707 else:
708 # single inh, normal mappings, etc.
709 self.mapper = target_mapper
710 self.columns = self.columns = util.WeakPopulateDict(
711 self.adapt_check_present # type: ignore
712 )
713
714 def __call__(self, col, as_filter):
715 for cc in sql_util._find_columns(col):
716 c2 = self.adapt_check_present(cc)
717 if c2 is not None:
718 return col
719 else:
720 return None
721
722 def adapt_check_present(self, col):
723 raise NotImplementedError()
724
725
726class _DMLBulkInsertReturningColFilter(_DMLReturningColFilter):
727 """an adapter used for the DML RETURNING case specifically
728 for ORM bulk insert (or any hypothetical DML that is splitting out a class
729 hierarchy among multiple DML statements....ORM bulk insert is the only
730 example right now)
731
732 its main job is to limit the columns in a RETURNING to only a specific
733 mapped table in a hierarchy.
734
735 """
736
737 def adapt_check_present(self, col):
738 mapper = self.mapper
739 prop = mapper._columntoproperty.get(col, None)
740 if prop is None:
741 return None
742 return mapper.local_table.c.corresponding_column(col)
743
744
745class _DMLUpdateDeleteReturningColFilter(_DMLReturningColFilter):
746 """an adapter used for the DML RETURNING case specifically
747 for ORM enabled UPDATE/DELETE
748
749 its main job is to limit the columns in a RETURNING to include
750 only direct persisted columns from the immediate selectable, not
751 expressions like column_property(), or to also allow columns from other
752 mappers for the UPDATE..FROM use case.
753
754 """
755
756 def adapt_check_present(self, col):
757 mapper = self.mapper
758 prop = mapper._columntoproperty.get(col, None)
759 if prop is not None:
760 # if the col is from the immediate mapper, only return a persisted
761 # column, not any kind of column_property expression
762 return mapper.persist_selectable.c.corresponding_column(col)
763
764 # if the col is from some other mapper, just return it, assume the
765 # user knows what they are doing
766 return col
767
768
769@sql.base.CompileState.plugin_for("orm", "orm_from_statement")
770class _ORMFromStatementCompileState(_ORMCompileState):
771 _from_obj_alias = None
772 _has_mapper_entities = False
773
774 statement_container: FromStatement
775 requested_statement: Union[SelectBase, TextClause, UpdateBase]
776 dml_table: Optional[_DMLTableElement] = None
777
778 _has_orm_entities = False
779 multi_row_eager_loaders = False
780 eager_adding_joins = False
781 compound_eager_adapter = None
782
783 extra_criteria_entities = util.EMPTY_DICT
784 eager_joins = util.EMPTY_DICT
785
786 @classmethod
787 def _create_orm_context(
788 cls,
789 statement: Union[Select, FromStatement],
790 *,
791 toplevel: bool,
792 compiler: Optional[SQLCompiler],
793 **kw: Any,
794 ) -> _ORMFromStatementCompileState:
795 statement_container = statement
796
797 assert isinstance(statement_container, FromStatement)
798
799 if compiler is not None and compiler.stack:
800 raise sa_exc.CompileError(
801 "The ORM FromStatement construct only supports being "
802 "invoked as the topmost statement, as it is only intended to "
803 "define how result rows should be returned."
804 )
805
806 self = cls.__new__(cls)
807 self._primary_entity = None
808
809 self.use_legacy_query_style = (
810 statement_container._compile_options._use_legacy_query_style
811 )
812 self.statement_container = self.select_statement = statement_container
813 self.requested_statement = statement = statement_container.element
814
815 if statement.is_dml:
816 self.dml_table = statement.table
817 self.is_dml_returning = True
818
819 self._entities = []
820 self._polymorphic_adapters = {}
821
822 self.compile_options = statement_container._compile_options
823
824 if (
825 self.use_legacy_query_style
826 and isinstance(statement, expression.SelectBase)
827 and not statement._is_textual
828 and not statement.is_dml
829 and statement._label_style is LABEL_STYLE_NONE
830 ):
831 self.statement = statement.set_label_style(
832 LABEL_STYLE_TABLENAME_PLUS_COL
833 )
834 else:
835 self.statement = statement
836
837 self._label_convention = self._column_naming_convention(
838 (
839 statement._label_style
840 if not statement._is_textual and not statement.is_dml
841 else LABEL_STYLE_NONE
842 ),
843 self.use_legacy_query_style,
844 )
845
846 _QueryEntity.to_compile_state(
847 self,
848 statement_container._raw_columns,
849 self._entities,
850 is_current_entities=True,
851 )
852
853 self.current_path = statement_container._compile_options._current_path
854
855 self._init_global_attributes(
856 statement_container,
857 compiler,
858 process_criteria_for_toplevel=False,
859 toplevel=True,
860 )
861
862 if statement_container._with_options:
863 for opt in statement_container._with_options:
864 if opt._is_compile_state:
865 opt.process_compile_state(self)
866
867 if statement_container._compile_state_funcs:
868 for fn, key in statement_container._compile_state_funcs:
869 fn(self)
870
871 self.primary_columns = []
872 self.secondary_columns = []
873 self.dedupe_columns = set()
874 self.create_eager_joins = []
875 self._fallback_from_clauses = []
876
877 self.order_by = None
878
879 if self.statement._is_text_clause:
880 # AbstractTextClause (TextClause, TString) has no "column"
881 # objects at all. for this case, we generate columns from our
882 # _QueryEntity objects, then flip on all the
883 # "please match no matter what" parameters.
884 self.extra_criteria_entities = {}
885
886 for entity in self._entities:
887 entity.setup_compile_state(self)
888
889 compiler._ordered_columns = compiler._textual_ordered_columns = (
890 False
891 )
892
893 # enable looser result column matching. this is shown to be
894 # needed by test_query.py::TextTest
895 compiler._loose_column_name_matching = True
896
897 for c in self.primary_columns:
898 compiler.process(
899 c,
900 within_columns_clause=True,
901 add_to_result_map=compiler._add_to_result_map,
902 )
903 else:
904 # for everyone else, Select, Insert, Update, TextualSelect, they
905 # have column objects already. After much
906 # experimentation here, the best approach seems to be, use
907 # those columns completely, don't interfere with the compiler
908 # at all; just in ORM land, use an adapter to convert from
909 # our ORM columns to whatever columns are in the statement,
910 # before we look in the result row. Adapt on names
911 # to accept cases such as issue #9217, however also allow
912 # this to be overridden for cases such as #9273.
913 self._from_obj_alias = ORMStatementAdapter(
914 _TraceAdaptRole.ADAPT_FROM_STATEMENT,
915 self.statement,
916 adapt_on_names=statement_container._adapt_on_names,
917 )
918
919 return self
920
921 def _adapt_col_list(self, cols, current_adapter):
922 return cols
923
924 def _get_current_adapter(self):
925 return None
926
927 def setup_dml_returning_compile_state(self, dml_mapper):
928 """used by BulkORMInsert, Update, Delete to set up a handler
929 for RETURNING to return ORM objects and expressions
930
931 """
932 target_mapper = self.statement._propagate_attrs.get(
933 "plugin_subject", None
934 )
935
936 if self.statement.is_insert:
937 adapter = _DMLBulkInsertReturningColFilter(
938 target_mapper, dml_mapper
939 )
940 elif self.statement.is_update or self.statement.is_delete:
941 adapter = _DMLUpdateDeleteReturningColFilter(
942 target_mapper, dml_mapper
943 )
944 else:
945 adapter = None
946
947 if self.compile_options._is_star and (len(self._entities) != 1):
948 raise sa_exc.CompileError(
949 "Can't generate ORM query that includes multiple expressions "
950 "at the same time as '*'; query for '*' alone if present"
951 )
952
953 for entity in self._entities:
954 entity.setup_dml_returning_compile_state(self, adapter)
955
956
957class FromStatement(GroupedElement, Generative, TypedReturnsRows[Unpack[_Ts]]):
958 """Core construct that represents a load of ORM objects from various
959 :class:`.ReturnsRows` and other classes including:
960
961 :class:`.Select`, :class:`.TextClause`, :class:`.TextualSelect`,
962 :class:`.CompoundSelect`, :class`.Insert`, :class:`.Update`,
963 and in theory, :class:`.Delete`.
964
965 """
966
967 __visit_name__ = "orm_from_statement"
968
969 _compile_options = _ORMFromStatementCompileState.default_compile_options
970
971 _compile_state_factory = _ORMFromStatementCompileState.create_for_statement
972
973 _for_update_arg = None
974
975 element: Union[ExecutableReturnsRows, TextClause]
976
977 _adapt_on_names: bool
978
979 _traverse_internals = [
980 ("_raw_columns", InternalTraversal.dp_clauseelement_list),
981 ("element", InternalTraversal.dp_clauseelement),
982 ] + ExecutableStatement._executable_traverse_internals
983
984 _cache_key_traversal = _traverse_internals + [
985 ("_compile_options", InternalTraversal.dp_has_cache_key)
986 ]
987
988 is_from_statement = True
989
990 def __init__(
991 self,
992 entities: Iterable[_ColumnsClauseArgument[Any]],
993 element: Union[ExecutableReturnsRows, TextClause],
994 _adapt_on_names: bool = True,
995 ):
996 self._raw_columns = [
997 coercions.expect(
998 roles.ColumnsClauseRole,
999 ent,
1000 apply_propagate_attrs=self,
1001 post_inspect=True,
1002 )
1003 for ent in util.to_list(entities)
1004 ]
1005 self.element = element
1006 self.is_dml = element.is_dml
1007 self.is_select = element.is_select
1008 self.is_delete = element.is_delete
1009 self.is_insert = element.is_insert
1010 self.is_update = element.is_update
1011 self._label_style = (
1012 element._label_style if is_select_base(element) else None
1013 )
1014 self._adapt_on_names = _adapt_on_names
1015
1016 def _compiler_dispatch(self, compiler, **kw):
1017 """provide a fixed _compiler_dispatch method.
1018
1019 This is roughly similar to using the sqlalchemy.ext.compiler
1020 ``@compiles`` extension.
1021
1022 """
1023
1024 compile_state = self._compile_state_factory(self, compiler, **kw)
1025
1026 toplevel = not compiler.stack
1027
1028 if toplevel:
1029 compiler.compile_state = compile_state
1030
1031 return compiler.process(compile_state.statement, **kw)
1032
1033 @property
1034 def column_descriptions(self):
1035 """Return a :term:`plugin-enabled` 'column descriptions' structure
1036 referring to the columns which are SELECTed by this statement.
1037
1038 See the section :ref:`queryguide_inspection` for an overview
1039 of this feature.
1040
1041 .. seealso::
1042
1043 :ref:`queryguide_inspection` - ORM background
1044
1045 """
1046 meth = cast(
1047 _ORMSelectCompileState, SelectState.get_plugin_class(self)
1048 ).get_column_descriptions
1049 return meth(self)
1050
1051 def _ensure_disambiguated_names(self):
1052 return self
1053
1054 def get_children(self, **kw):
1055 yield from itertools.chain.from_iterable(
1056 element._from_objects for element in self._raw_columns
1057 )
1058 yield from super().get_children(**kw)
1059
1060 @property
1061 def _all_selected_columns(self):
1062 return self.element._all_selected_columns
1063
1064 @property
1065 def _return_defaults(self):
1066 return self.element._return_defaults if is_dml(self.element) else None
1067
1068 @property
1069 def _returning(self):
1070 return self.element._returning if is_dml(self.element) else None
1071
1072 @property
1073 def _inline(self):
1074 return self.element._inline if is_insert_update(self.element) else None
1075
1076
1077@sql.base.CompileState.plugin_for("orm", "compound_select")
1078class _CompoundSelectCompileState(
1079 _AutoflushOnlyORMCompileState, CompoundSelectState
1080):
1081 pass
1082
1083
1084@sql.base.CompileState.plugin_for("orm", "select")
1085class _ORMSelectCompileState(_ORMCompileState, SelectState):
1086 _already_joined_edges = ()
1087
1088 _memoized_entities = util.EMPTY_DICT
1089
1090 _from_obj_alias = None
1091 _has_mapper_entities = False
1092
1093 _has_orm_entities = False
1094 multi_row_eager_loaders = False
1095 eager_adding_joins = False
1096 compound_eager_adapter = None
1097
1098 correlate = None
1099 correlate_except = None
1100 _where_criteria = ()
1101 _having_criteria = ()
1102
1103 @classmethod
1104 def _create_orm_context(
1105 cls,
1106 statement: Union[Select, FromStatement],
1107 *,
1108 toplevel: bool,
1109 compiler: Optional[SQLCompiler],
1110 **kw: Any,
1111 ) -> _ORMSelectCompileState:
1112
1113 self = cls.__new__(cls)
1114
1115 select_statement = statement
1116
1117 # if we are a select() that was never a legacy Query, we won't
1118 # have ORM level compile options.
1119 statement._compile_options = cls.default_compile_options.safe_merge(
1120 statement._compile_options
1121 )
1122
1123 if select_statement._execution_options:
1124 # execution options should not impact the compilation of a
1125 # query, and at the moment subqueryloader is putting some things
1126 # in here that we explicitly don't want stuck in a cache.
1127 self.select_statement = select_statement._clone()
1128 self.select_statement._execution_options = util.EMPTY_DICT
1129 else:
1130 self.select_statement = select_statement
1131
1132 # indicates this select() came from Query.statement
1133 self.for_statement = select_statement._compile_options._for_statement
1134
1135 # generally if we are from Query or directly from a select()
1136 self.use_legacy_query_style = (
1137 select_statement._compile_options._use_legacy_query_style
1138 )
1139
1140 self._entities = []
1141 self._primary_entity = None
1142 self._polymorphic_adapters = {}
1143
1144 self.compile_options = select_statement._compile_options
1145
1146 if not toplevel:
1147 # for subqueries, turn off eagerloads and set
1148 # "render_for_subquery".
1149 self.compile_options += {
1150 "_enable_eagerloads": False,
1151 "_render_for_subquery": True,
1152 }
1153
1154 # determine label style. we can make different decisions here.
1155 # at the moment, trying to see if we can always use DISAMBIGUATE_ONLY
1156 # rather than LABEL_STYLE_NONE, and if we can use disambiguate style
1157 # for new style ORM selects too.
1158 if (
1159 self.use_legacy_query_style
1160 and self.select_statement._label_style is LABEL_STYLE_LEGACY_ORM
1161 ):
1162 if not self.for_statement:
1163 self.label_style = LABEL_STYLE_TABLENAME_PLUS_COL
1164 else:
1165 self.label_style = LABEL_STYLE_DISAMBIGUATE_ONLY
1166 else:
1167 self.label_style = self.select_statement._label_style
1168
1169 if select_statement._memoized_select_entities:
1170 self._memoized_entities = {
1171 memoized_entities: _QueryEntity.to_compile_state(
1172 self,
1173 memoized_entities._raw_columns,
1174 [],
1175 is_current_entities=False,
1176 )
1177 for memoized_entities in (
1178 select_statement._memoized_select_entities
1179 )
1180 }
1181
1182 # label_convention is stateful and will yield deduping keys if it
1183 # sees the same key twice. therefore it's important that it is not
1184 # invoked for the above "memoized" entities that aren't actually
1185 # in the columns clause
1186 self._label_convention = self._column_naming_convention(
1187 statement._label_style, self.use_legacy_query_style
1188 )
1189
1190 _QueryEntity.to_compile_state(
1191 self,
1192 select_statement._raw_columns,
1193 self._entities,
1194 is_current_entities=True,
1195 )
1196
1197 self.current_path = select_statement._compile_options._current_path
1198
1199 self.eager_order_by = ()
1200
1201 self._init_global_attributes(
1202 select_statement,
1203 compiler,
1204 toplevel=toplevel,
1205 process_criteria_for_toplevel=False,
1206 )
1207
1208 if toplevel and (
1209 select_statement._with_options
1210 or select_statement._memoized_select_entities
1211 ):
1212 for (
1213 memoized_entities
1214 ) in select_statement._memoized_select_entities:
1215 for opt in memoized_entities._with_options:
1216 if opt._is_compile_state:
1217 opt.process_compile_state_replaced_entities(
1218 self,
1219 [
1220 ent
1221 for ent in self._memoized_entities[
1222 memoized_entities
1223 ]
1224 if isinstance(ent, _MapperEntity)
1225 ],
1226 )
1227
1228 for opt in self.select_statement._with_options:
1229 if opt._is_compile_state:
1230 opt.process_compile_state(self)
1231
1232 # uncomment to print out the context.attributes structure
1233 # after it's been set up above
1234 # self._dump_option_struct()
1235
1236 if select_statement._compile_state_funcs:
1237 for fn, key in select_statement._compile_state_funcs:
1238 fn(self)
1239
1240 self.primary_columns = []
1241 self.secondary_columns = []
1242 self.dedupe_columns = set()
1243 self.eager_joins = {}
1244 self.extra_criteria_entities = {}
1245 self.create_eager_joins = []
1246 self._fallback_from_clauses = []
1247
1248 # normalize the FROM clauses early by themselves, as this makes
1249 # it an easier job when we need to assemble a JOIN onto these,
1250 # for select.join() as well as joinedload(). As of 1.4 there are now
1251 # potentially more complex sets of FROM objects here as the use
1252 # of lambda statements for lazyload, load_on_pk etc. uses more
1253 # cloning of the select() construct. See #6495
1254 self.from_clauses = self._normalize_froms(
1255 info.selectable for info in select_statement._from_obj
1256 )
1257
1258 # this is a fairly arbitrary break into a second method,
1259 # so it might be nicer to break up create_for_statement()
1260 # and _setup_for_generate into three or four logical sections
1261 self._setup_for_generate()
1262
1263 SelectState.__init__(self, self.statement, compiler, **kw)
1264 return self
1265
1266 def _dump_option_struct(self):
1267 print("\n---------------------------------------------------\n")
1268 print(f"current path: {self.current_path}")
1269 for key in self.attributes:
1270 if isinstance(key, tuple) and key[0] == "loader":
1271 print(f"\nLoader: {PathRegistry.coerce(key[1])}")
1272 print(f" {self.attributes[key]}")
1273 print(f" {self.attributes[key].__dict__}")
1274 elif isinstance(key, tuple) and key[0] == "path_with_polymorphic":
1275 print(f"\nWith Polymorphic: {PathRegistry.coerce(key[1])}")
1276 print(f" {self.attributes[key]}")
1277
1278 def _setup_for_generate(self):
1279 query = self.select_statement
1280
1281 self.statement = None
1282 self._join_entities = ()
1283
1284 if self.compile_options._set_base_alias:
1285 # legacy Query only
1286 self._set_select_from_alias()
1287
1288 for memoized_entities in query._memoized_select_entities:
1289 if memoized_entities._setup_joins:
1290 self._join(
1291 memoized_entities._setup_joins,
1292 self._memoized_entities[memoized_entities],
1293 )
1294
1295 if query._setup_joins:
1296 self._join(query._setup_joins, self._entities)
1297
1298 current_adapter = self._get_current_adapter()
1299
1300 if query._where_criteria:
1301 self._where_criteria = query._where_criteria
1302
1303 if current_adapter:
1304 self._where_criteria = tuple(
1305 current_adapter(crit, True)
1306 for crit in self._where_criteria
1307 )
1308
1309 # TODO: some complexity with order_by here was due to mapper.order_by.
1310 # now that this is removed we can hopefully make order_by /
1311 # group_by act identically to how they are in Core select.
1312 self.order_by = (
1313 self._adapt_col_list(query._order_by_clauses, current_adapter)
1314 if current_adapter and query._order_by_clauses not in (None, False)
1315 else query._order_by_clauses
1316 )
1317
1318 if query._having_criteria:
1319 self._having_criteria = tuple(
1320 current_adapter(crit, True) if current_adapter else crit
1321 for crit in query._having_criteria
1322 )
1323
1324 self.group_by = (
1325 self._adapt_col_list(
1326 util.flatten_iterator(query._group_by_clauses), current_adapter
1327 )
1328 if current_adapter and query._group_by_clauses not in (None, False)
1329 else query._group_by_clauses or None
1330 )
1331
1332 if self.eager_order_by:
1333 adapter = self.from_clauses[0]._target_adapter
1334 self.eager_order_by = adapter.copy_and_process(self.eager_order_by)
1335
1336 if query._distinct_on:
1337 self.distinct_on = self._adapt_col_list(
1338 query._distinct_on, current_adapter
1339 )
1340 else:
1341 self.distinct_on = ()
1342
1343 self.distinct = query._distinct
1344
1345 self.syntax_extensions = {
1346 key: current_adapter(value, True) if current_adapter else value
1347 for key, value in query._get_syntax_extensions_as_dict().items()
1348 }
1349
1350 if query._correlate:
1351 # ORM mapped entities that are mapped to joins can be passed
1352 # to .correlate, so here they are broken into their component
1353 # tables.
1354 self.correlate = tuple(
1355 util.flatten_iterator(
1356 sql_util.surface_selectables(s) if s is not None else None
1357 for s in query._correlate
1358 )
1359 )
1360 elif query._correlate_except is not None:
1361 self.correlate_except = tuple(
1362 util.flatten_iterator(
1363 sql_util.surface_selectables(s) if s is not None else None
1364 for s in query._correlate_except
1365 )
1366 )
1367 elif not query._auto_correlate:
1368 self.correlate = (None,)
1369
1370 # PART II
1371
1372 self._for_update_arg = query._for_update_arg
1373
1374 if self.compile_options._is_star and (len(self._entities) != 1):
1375 raise sa_exc.CompileError(
1376 "Can't generate ORM query that includes multiple expressions "
1377 "at the same time as '*'; query for '*' alone if present"
1378 )
1379 for entity in self._entities:
1380 entity.setup_compile_state(self)
1381
1382 for rec in self.create_eager_joins:
1383 strategy = rec[0]
1384 strategy(self, *rec[1:])
1385
1386 # else "load from discrete FROMs" mode,
1387 # i.e. when each _MappedEntity has its own FROM
1388
1389 if self.compile_options._enable_single_crit:
1390 self._adjust_for_extra_criteria()
1391
1392 if not self.primary_columns:
1393 if self.compile_options._only_load_props:
1394 assert False, "no columns were included in _only_load_props"
1395
1396 raise sa_exc.InvalidRequestError(
1397 "Query contains no columns with which to SELECT from."
1398 )
1399
1400 if not self.from_clauses:
1401 self.from_clauses = list(self._fallback_from_clauses)
1402
1403 if self.order_by is False:
1404 self.order_by = None
1405
1406 if self._should_nest_selectable:
1407 self.statement = self._compound_eager_statement()
1408 else:
1409 self.statement = self._simple_statement()
1410
1411 if self.for_statement:
1412 ezero = self._mapper_zero()
1413 if ezero is not None:
1414 # TODO: this goes away once we get rid of the deep entity
1415 # thing
1416 self.statement = self.statement._annotate(
1417 {"deepentity": ezero}
1418 )
1419
1420 @classmethod
1421 def _create_entities_collection(cls, query, legacy):
1422 """Creates a partial ORMSelectCompileState that includes
1423 the full collection of _MapperEntity and other _QueryEntity objects.
1424
1425 Supports a few remaining use cases that are pre-compilation
1426 but still need to gather some of the column / adaption information.
1427
1428 """
1429 self = cls.__new__(cls)
1430
1431 self._entities = []
1432 self._primary_entity = None
1433 self._polymorphic_adapters = {}
1434
1435 self._label_convention = self._column_naming_convention(
1436 query._label_style, legacy
1437 )
1438
1439 # entities will also set up polymorphic adapters for mappers
1440 # that have with_polymorphic configured
1441 _QueryEntity.to_compile_state(
1442 self, query._raw_columns, self._entities, is_current_entities=True
1443 )
1444 return self
1445
1446 @classmethod
1447 def _get_filter_by_entities(cls, statement):
1448 """Return all ORM entities for filter_by() searches.
1449
1450 the ORM version for Select is special vs. update/delete since it needs
1451 to navigate along select.join() paths which have ORM specific
1452 directives.
1453
1454 beyond that, it delivers other entities as the Mapper or Aliased
1455 object rather than the Table or Alias, which mostly affects
1456 how error messages regarding ambiguous entities or entity not
1457 found are rendered; class-specific attributes like hybrid,
1458 column_property() etc. work either way since
1459 _entity_namespace_key_search_all() uses _entity_namespace().
1460
1461 DML Update and Delete objects, even though they also have filter_by()
1462 and also accept ORM objects, don't use this routine since they
1463 typically just have a single table, and if they have multiple tables
1464 it's only via WHERE clause, which interestingly do not maintain ORM
1465 annotations when used (that is, (User.name ==
1466 'foo').left.table._annotations is empty; the ORMness of User.name is
1467 lost in the expression construction process, since we don't annotate
1468 (copy) Column objects with ORM entities the way we do for Table.
1469
1470 .. versionadded:: 2.1
1471 """
1472
1473 def _setup_join_targets(collection):
1474 for (target, *_) in collection:
1475 if isinstance(target, attributes.QueryableAttribute):
1476 yield target.entity
1477 elif "_no_filter_by" not in target._annotations:
1478 yield target
1479
1480 entities = set(_setup_join_targets(statement._setup_joins))
1481
1482 for memoized in statement._memoized_select_entities:
1483 entities.update(_setup_join_targets(memoized._setup_joins))
1484
1485 entities.update(
1486 (
1487 from_obj._annotations["parententity"]
1488 if "parententity" in from_obj._annotations
1489 else from_obj
1490 )
1491 for from_obj in statement._from_obj
1492 if "_no_filter_by" not in from_obj._annotations
1493 )
1494
1495 for element in statement._raw_columns:
1496 if "entity_namespace" in element._annotations:
1497 ens = element._annotations["entity_namespace"]
1498 entities.add(ens)
1499 elif "_no_filter_by" not in element._annotations:
1500 entities.update(element._from_objects)
1501
1502 return entities
1503
1504 @classmethod
1505 def all_selected_columns(cls, statement):
1506 for element in statement._raw_columns:
1507 if (
1508 element.is_selectable
1509 and "entity_namespace" in element._annotations
1510 ):
1511 ens = element._annotations["entity_namespace"]
1512 if not ens.is_mapper and not ens.is_aliased_class:
1513 yield from _select_iterables([element])
1514 else:
1515 yield from _select_iterables(ens._all_column_expressions)
1516 else:
1517 yield from _select_iterables([element])
1518
1519 @classmethod
1520 def get_columns_clause_froms(cls, statement):
1521 return cls._normalize_froms(
1522 itertools.chain.from_iterable(
1523 (
1524 element._from_objects
1525 if "parententity" not in element._annotations
1526 else [
1527 element._annotations[
1528 "parententity"
1529 ].__clause_element__()
1530 ]
1531 )
1532 for element in statement._raw_columns
1533 )
1534 )
1535
1536 @classmethod
1537 def from_statement(cls, statement, from_statement):
1538 from_statement = coercions.expect(
1539 roles.ReturnsRowsRole,
1540 from_statement,
1541 apply_propagate_attrs=statement,
1542 )
1543
1544 stmt = FromStatement(statement._raw_columns, from_statement)
1545
1546 stmt.__dict__.update(
1547 _with_options=statement._with_options,
1548 _compile_state_funcs=statement._compile_state_funcs,
1549 _execution_options=statement._execution_options,
1550 _propagate_attrs=statement._propagate_attrs,
1551 )
1552 return stmt
1553
1554 def _set_select_from_alias(self):
1555 """used only for legacy Query cases"""
1556
1557 query = self.select_statement # query
1558
1559 assert self.compile_options._set_base_alias
1560 assert len(query._from_obj) == 1
1561
1562 adapter = self._get_select_from_alias_from_obj(query._from_obj[0])
1563 if adapter:
1564 self.compile_options += {"_enable_single_crit": False}
1565 self._from_obj_alias = adapter
1566
1567 def _get_select_from_alias_from_obj(self, from_obj):
1568 """used only for legacy Query cases"""
1569
1570 info = from_obj
1571
1572 if "parententity" in info._annotations:
1573 info = info._annotations["parententity"]
1574
1575 if hasattr(info, "mapper"):
1576 if not info.is_aliased_class:
1577 raise sa_exc.ArgumentError(
1578 "A selectable (FromClause) instance is "
1579 "expected when the base alias is being set."
1580 )
1581 else:
1582 return info._adapter
1583
1584 elif isinstance(info.selectable, sql.selectable.AliasedReturnsRows):
1585 equivs = self._all_equivs()
1586 assert info is info.selectable
1587 return ORMStatementAdapter(
1588 _TraceAdaptRole.LEGACY_SELECT_FROM_ALIAS,
1589 info.selectable,
1590 equivalents=equivs,
1591 )
1592 else:
1593 return None
1594
1595 def _mapper_zero(self):
1596 """return the Mapper associated with the first QueryEntity."""
1597 return self._entities[0].mapper
1598
1599 def _entity_zero(self):
1600 """Return the 'entity' (mapper or AliasedClass) associated
1601 with the first QueryEntity, or alternatively the 'select from'
1602 entity if specified."""
1603
1604 for ent in self.from_clauses:
1605 if "parententity" in ent._annotations:
1606 return ent._annotations["parententity"]
1607 for qent in self._entities:
1608 if qent.entity_zero:
1609 return qent.entity_zero
1610
1611 return None
1612
1613 def _only_full_mapper_zero(self, methname):
1614 if self._entities != [self._primary_entity]:
1615 raise sa_exc.InvalidRequestError(
1616 "%s() can only be used against "
1617 "a single mapped class." % methname
1618 )
1619 return self._primary_entity.entity_zero
1620
1621 def _only_entity_zero(self, rationale=None):
1622 if len(self._entities) > 1:
1623 raise sa_exc.InvalidRequestError(
1624 rationale
1625 or "This operation requires a Query "
1626 "against a single mapper."
1627 )
1628 return self._entity_zero()
1629
1630 def _all_equivs(self):
1631 equivs = {}
1632
1633 for memoized_entities in self._memoized_entities.values():
1634 for ent in [
1635 ent
1636 for ent in memoized_entities
1637 if isinstance(ent, _MapperEntity)
1638 ]:
1639 equivs.update(ent.mapper._equivalent_columns)
1640
1641 for ent in [
1642 ent for ent in self._entities if isinstance(ent, _MapperEntity)
1643 ]:
1644 equivs.update(ent.mapper._equivalent_columns)
1645 return equivs
1646
1647 def _compound_eager_statement(self):
1648 # for eager joins present and LIMIT/OFFSET/DISTINCT,
1649 # wrap the query inside a select,
1650 # then append eager joins onto that
1651
1652 if self.order_by:
1653 # the default coercion for ORDER BY is now the OrderByRole,
1654 # which adds an additional post coercion to ByOfRole in that
1655 # elements are converted into label references. For the
1656 # eager load / subquery wrapping case, we need to un-coerce
1657 # the original expressions outside of the label references
1658 # in order to have them render.
1659 unwrapped_order_by = [
1660 (
1661 elem.element
1662 if isinstance(elem, sql.elements._label_reference)
1663 else elem
1664 )
1665 for elem in self.order_by
1666 ]
1667
1668 order_by_col_expr = sql_util.expand_column_list_from_order_by(
1669 self.primary_columns, unwrapped_order_by
1670 )
1671 else:
1672 order_by_col_expr = []
1673 unwrapped_order_by = None
1674
1675 # put FOR UPDATE on the inner query, where MySQL will honor it,
1676 # as well as if it has an OF so PostgreSQL can use it.
1677 inner = self._select_statement(
1678 self.primary_columns
1679 + [c for c in order_by_col_expr if c not in self.dedupe_columns],
1680 self.from_clauses,
1681 self._where_criteria,
1682 self._having_criteria,
1683 self.label_style,
1684 self.order_by,
1685 for_update=self._for_update_arg,
1686 hints=self.select_statement._hints,
1687 statement_hints=self.select_statement._statement_hints,
1688 correlate=self.correlate,
1689 correlate_except=self.correlate_except,
1690 **self._select_args,
1691 )
1692
1693 inner = inner.alias()
1694
1695 equivs = self._all_equivs()
1696
1697 self.compound_eager_adapter = ORMStatementAdapter(
1698 _TraceAdaptRole.COMPOUND_EAGER_STATEMENT, inner, equivalents=equivs
1699 )
1700
1701 statement = future.select(
1702 *([inner] + self.secondary_columns) # use_labels=self.labels
1703 )
1704 statement._label_style = self.label_style
1705
1706 # Oracle Database however does not allow FOR UPDATE on the subquery,
1707 # and the Oracle Database dialects ignore it, plus for PostgreSQL,
1708 # MySQL we expect that all elements of the row are locked, so also put
1709 # it on the outside (except in the case of PG when OF is used)
1710 if (
1711 self._for_update_arg is not None
1712 and self._for_update_arg.of is None
1713 ):
1714 statement._for_update_arg = self._for_update_arg
1715
1716 from_clause = inner
1717 for eager_join in self.eager_joins.values():
1718 # EagerLoader places a 'stop_on' attribute on the join,
1719 # giving us a marker as to where the "splice point" of
1720 # the join should be
1721 from_clause = sql_util.splice_joins(
1722 from_clause, eager_join, eager_join.stop_on
1723 )
1724
1725 statement.select_from.non_generative(statement, from_clause)
1726
1727 if unwrapped_order_by:
1728 statement.order_by.non_generative(
1729 statement,
1730 *self.compound_eager_adapter.copy_and_process(
1731 unwrapped_order_by
1732 ),
1733 )
1734
1735 statement.order_by.non_generative(statement, *self.eager_order_by)
1736 return statement
1737
1738 def _simple_statement(self):
1739 statement = self._select_statement(
1740 self.primary_columns + self.secondary_columns,
1741 tuple(self.from_clauses) + tuple(self.eager_joins.values()),
1742 self._where_criteria,
1743 self._having_criteria,
1744 self.label_style,
1745 self.order_by,
1746 for_update=self._for_update_arg,
1747 hints=self.select_statement._hints,
1748 statement_hints=self.select_statement._statement_hints,
1749 correlate=self.correlate,
1750 correlate_except=self.correlate_except,
1751 **self._select_args,
1752 )
1753
1754 if self.eager_order_by:
1755 statement.order_by.non_generative(statement, *self.eager_order_by)
1756 return statement
1757
1758 def _select_statement(
1759 self,
1760 raw_columns,
1761 from_obj,
1762 where_criteria,
1763 having_criteria,
1764 label_style,
1765 order_by,
1766 for_update,
1767 hints,
1768 statement_hints,
1769 correlate,
1770 correlate_except,
1771 limit_clause,
1772 offset_clause,
1773 fetch_clause,
1774 fetch_clause_options,
1775 distinct,
1776 distinct_on,
1777 prefixes,
1778 suffixes,
1779 group_by,
1780 independent_ctes,
1781 independent_ctes_opts,
1782 syntax_extensions,
1783 ):
1784 statement = Select._create_raw_select(
1785 _raw_columns=raw_columns,
1786 _from_obj=from_obj,
1787 _label_style=label_style,
1788 )
1789
1790 if where_criteria:
1791 statement._where_criteria = where_criteria
1792 if having_criteria:
1793 statement._having_criteria = having_criteria
1794
1795 if order_by:
1796 statement._order_by_clauses += tuple(order_by)
1797
1798 if distinct_on:
1799 statement._distinct = True
1800 statement._distinct_on = distinct_on
1801 elif distinct:
1802 statement._distinct = True
1803
1804 if group_by:
1805 statement._group_by_clauses += tuple(group_by)
1806
1807 statement._limit_clause = limit_clause
1808 statement._offset_clause = offset_clause
1809 statement._fetch_clause = fetch_clause
1810 statement._fetch_clause_options = fetch_clause_options
1811 statement._independent_ctes = independent_ctes
1812 statement._independent_ctes_opts = independent_ctes_opts
1813 if syntax_extensions:
1814 statement._set_syntax_extensions(**syntax_extensions)
1815
1816 if prefixes:
1817 statement._prefixes = prefixes
1818
1819 if suffixes:
1820 statement._suffixes = suffixes
1821
1822 statement._for_update_arg = for_update
1823
1824 if hints:
1825 statement._hints = hints
1826 if statement_hints:
1827 statement._statement_hints = statement_hints
1828
1829 if correlate:
1830 statement.correlate.non_generative(statement, *correlate)
1831
1832 if correlate_except is not None:
1833 statement.correlate_except.non_generative(
1834 statement, *correlate_except
1835 )
1836
1837 return statement
1838
1839 def _adapt_polymorphic_element(self, element):
1840 if "parententity" in element._annotations:
1841 search = element._annotations["parententity"]
1842 alias = self._polymorphic_adapters.get(search, None)
1843 if alias:
1844 return alias.adapt_clause(element)
1845
1846 if isinstance(element, expression.FromClause):
1847 search = element
1848 elif hasattr(element, "table"):
1849 search = element.table
1850 else:
1851 return None
1852
1853 alias = self._polymorphic_adapters.get(search, None)
1854 if alias:
1855 return alias.adapt_clause(element)
1856
1857 def _adapt_col_list(self, cols, current_adapter):
1858 if current_adapter:
1859 return [current_adapter(o, True) for o in cols]
1860 else:
1861 return cols
1862
1863 def _get_current_adapter(self):
1864 adapters = []
1865
1866 if self._from_obj_alias:
1867 # used for legacy going forward for query set_ops, e.g.
1868 # union(), union_all(), etc.
1869 # 1.4 and previously, also used for from_self(),
1870 # select_entity_from()
1871 #
1872 # for the "from obj" alias, apply extra rule to the
1873 # 'ORM only' check, if this query were generated from a
1874 # subquery of itself, i.e. _from_selectable(), apply adaption
1875 # to all SQL constructs.
1876 adapters.append(
1877 self._from_obj_alias.replace,
1878 )
1879
1880 # this was *hopefully* the only adapter we were going to need
1881 # going forward...however, we unfortunately need _from_obj_alias
1882 # for query.union(), which we can't drop
1883 if self._polymorphic_adapters:
1884 adapters.append(self._adapt_polymorphic_element)
1885
1886 if not adapters:
1887 return None
1888
1889 def _adapt_clause(clause, as_filter):
1890 # do we adapt all expression elements or only those
1891 # tagged as 'ORM' constructs ?
1892
1893 def replace(elem):
1894 for adapter in adapters:
1895 e = adapter(elem)
1896 if e is not None:
1897 return e
1898
1899 return visitors.replacement_traverse(clause, {}, replace)
1900
1901 return _adapt_clause
1902
1903 def _join(self, args, entities_collection):
1904 for right, onclause, from_, flags in args:
1905 isouter = flags["isouter"]
1906 full = flags["full"]
1907
1908 right = inspect(right)
1909 if onclause is not None:
1910 onclause = inspect(onclause)
1911
1912 if isinstance(right, interfaces.PropComparator):
1913 if onclause is not None:
1914 raise sa_exc.InvalidRequestError(
1915 "No 'on clause' argument may be passed when joining "
1916 "to a relationship path as a target"
1917 )
1918
1919 onclause = right
1920 right = None
1921 elif "parententity" in right._annotations:
1922 right = right._annotations["parententity"]
1923
1924 if onclause is None:
1925 if not right.is_selectable and not hasattr(right, "mapper"):
1926 raise sa_exc.ArgumentError(
1927 "Expected mapped entity or "
1928 "selectable/table as join target"
1929 )
1930
1931 if isinstance(onclause, interfaces.PropComparator):
1932 # descriptor/property given (or determined); this tells us
1933 # explicitly what the expected "left" side of the join is.
1934
1935 of_type = getattr(onclause, "_of_type", None)
1936
1937 if right is None:
1938 if of_type:
1939 right = of_type
1940 else:
1941 right = onclause.property
1942
1943 try:
1944 right = right.entity
1945 except AttributeError as err:
1946 raise sa_exc.ArgumentError(
1947 "Join target %s does not refer to a "
1948 "mapped entity" % right
1949 ) from err
1950
1951 left = onclause._parententity
1952
1953 prop = onclause.property
1954 if not isinstance(onclause, attributes.QueryableAttribute):
1955 onclause = prop
1956
1957 # check for this path already present. don't render in that
1958 # case.
1959 if (left, right, prop.key) in self._already_joined_edges:
1960 continue
1961
1962 if from_ is not None:
1963 if (
1964 from_ is not left
1965 and from_._annotations.get("parententity", None)
1966 is not left
1967 ):
1968 raise sa_exc.InvalidRequestError(
1969 "explicit from clause %s does not match left side "
1970 "of relationship attribute %s"
1971 % (
1972 from_._annotations.get("parententity", from_),
1973 onclause,
1974 )
1975 )
1976 elif from_ is not None:
1977 prop = None
1978 left = from_
1979 else:
1980 # no descriptor/property given; we will need to figure out
1981 # what the effective "left" side is
1982 prop = left = None
1983
1984 # figure out the final "left" and "right" sides and create an
1985 # ORMJoin to add to our _from_obj tuple
1986 self._join_left_to_right(
1987 entities_collection,
1988 left,
1989 right,
1990 onclause,
1991 prop,
1992 isouter,
1993 full,
1994 )
1995
1996 def _join_left_to_right(
1997 self,
1998 entities_collection,
1999 left,
2000 right,
2001 onclause,
2002 prop,
2003 outerjoin,
2004 full,
2005 ):
2006 """given raw "left", "right", "onclause" parameters consumed from
2007 a particular key within _join(), add a real ORMJoin object to
2008 our _from_obj list (or augment an existing one)
2009
2010 """
2011
2012 explicit_left = left
2013 if left is None:
2014 # left not given (e.g. no relationship object/name specified)
2015 # figure out the best "left" side based on our existing froms /
2016 # entities
2017 assert prop is None
2018 (
2019 left,
2020 replace_from_obj_index,
2021 use_entity_index,
2022 ) = self._join_determine_implicit_left_side(
2023 entities_collection, left, right, onclause
2024 )
2025 else:
2026 # left is given via a relationship/name, or as explicit left side.
2027 # Determine where in our
2028 # "froms" list it should be spliced/appended as well as what
2029 # existing entity it corresponds to.
2030 (
2031 replace_from_obj_index,
2032 use_entity_index,
2033 ) = self._join_place_explicit_left_side(entities_collection, left)
2034
2035 if left is right:
2036 raise sa_exc.InvalidRequestError(
2037 "Can't construct a join from %s to %s, they "
2038 "are the same entity" % (left, right)
2039 )
2040
2041 # the right side as given often needs to be adapted. additionally
2042 # a lot of things can be wrong with it. handle all that and
2043 # get back the new effective "right" side
2044 r_info, right, onclause = self._join_check_and_adapt_right_side(
2045 left, right, onclause, prop
2046 )
2047
2048 if not r_info.is_selectable:
2049 extra_criteria = self._get_extra_criteria(r_info)
2050 else:
2051 extra_criteria = ()
2052
2053 if replace_from_obj_index is not None:
2054 # splice into an existing element in the
2055 # self._from_obj list
2056 left_clause = self.from_clauses[replace_from_obj_index]
2057
2058 if explicit_left is not None and onclause is None:
2059 onclause = _ORMJoin._join_condition(explicit_left, right)
2060
2061 self.from_clauses = (
2062 self.from_clauses[:replace_from_obj_index]
2063 + [
2064 _ORMJoin(
2065 left_clause,
2066 right,
2067 onclause,
2068 isouter=outerjoin,
2069 full=full,
2070 _extra_criteria=extra_criteria,
2071 )
2072 ]
2073 + self.from_clauses[replace_from_obj_index + 1 :]
2074 )
2075 else:
2076 # add a new element to the self._from_obj list
2077 if use_entity_index is not None:
2078 # make use of _MapperEntity selectable, which is usually
2079 # entity_zero.selectable, but if with_polymorphic() were used
2080 # might be distinct
2081 assert isinstance(
2082 entities_collection[use_entity_index], _MapperEntity
2083 )
2084 left_clause = entities_collection[use_entity_index].selectable
2085 else:
2086 left_clause = left
2087
2088 self.from_clauses = self.from_clauses + [
2089 _ORMJoin(
2090 left_clause,
2091 r_info,
2092 onclause,
2093 isouter=outerjoin,
2094 full=full,
2095 _extra_criteria=extra_criteria,
2096 )
2097 ]
2098
2099 def _join_determine_implicit_left_side(
2100 self, entities_collection, left, right, onclause
2101 ):
2102 """When join conditions don't express the left side explicitly,
2103 determine if an existing FROM or entity in this query
2104 can serve as the left hand side.
2105
2106 """
2107
2108 # when we are here, it means join() was called without an ORM-
2109 # specific way of telling us what the "left" side is, e.g.:
2110 #
2111 # join(RightEntity)
2112 #
2113 # or
2114 #
2115 # join(RightEntity, RightEntity.foo == LeftEntity.bar)
2116 #
2117
2118 r_info = inspect(right)
2119
2120 replace_from_obj_index = use_entity_index = None
2121
2122 if self.from_clauses:
2123 # we have a list of FROMs already. So by definition this
2124 # join has to connect to one of those FROMs.
2125
2126 indexes = sql_util.find_left_clause_to_join_from(
2127 self.from_clauses, r_info.selectable, onclause
2128 )
2129
2130 if len(indexes) == 1:
2131 replace_from_obj_index = indexes[0]
2132 left = self.from_clauses[replace_from_obj_index]
2133 elif len(indexes) > 1:
2134 raise sa_exc.InvalidRequestError(
2135 "Can't determine which FROM clause to join "
2136 "from, there are multiple FROMS which can "
2137 "join to this entity. Please use the .select_from() "
2138 "method to establish an explicit left side, as well as "
2139 "providing an explicit ON clause if not present already "
2140 "to help resolve the ambiguity."
2141 )
2142 else:
2143 raise sa_exc.InvalidRequestError(
2144 "Don't know how to join to %r. "
2145 "Please use the .select_from() "
2146 "method to establish an explicit left side, as well as "
2147 "providing an explicit ON clause if not present already "
2148 "to help resolve the ambiguity." % (right,)
2149 )
2150
2151 elif entities_collection:
2152 # we have no explicit FROMs, so the implicit left has to
2153 # come from our list of entities.
2154
2155 potential = {}
2156 for entity_index, ent in enumerate(entities_collection):
2157 entity = ent.entity_zero_or_selectable
2158 if entity is None:
2159 continue
2160 ent_info = inspect(entity)
2161 if ent_info is r_info: # left and right are the same, skip
2162 continue
2163
2164 # by using a dictionary with the selectables as keys this
2165 # de-duplicates those selectables as occurs when the query is
2166 # against a series of columns from the same selectable
2167 if isinstance(ent, _MapperEntity):
2168 potential[ent.selectable] = (entity_index, entity)
2169 else:
2170 potential[ent_info.selectable] = (None, entity)
2171
2172 all_clauses = list(potential.keys())
2173 indexes = sql_util.find_left_clause_to_join_from(
2174 all_clauses, r_info.selectable, onclause
2175 )
2176
2177 if len(indexes) == 1:
2178 use_entity_index, left = potential[all_clauses[indexes[0]]]
2179 elif len(indexes) > 1:
2180 raise sa_exc.InvalidRequestError(
2181 "Can't determine which FROM clause to join "
2182 "from, there are multiple FROMS which can "
2183 "join to this entity. Please use the .select_from() "
2184 "method to establish an explicit left side, as well as "
2185 "providing an explicit ON clause if not present already "
2186 "to help resolve the ambiguity."
2187 )
2188 else:
2189 raise sa_exc.InvalidRequestError(
2190 "Don't know how to join to %r. "
2191 "Please use the .select_from() "
2192 "method to establish an explicit left side, as well as "
2193 "providing an explicit ON clause if not present already "
2194 "to help resolve the ambiguity." % (right,)
2195 )
2196 else:
2197 raise sa_exc.InvalidRequestError(
2198 "No entities to join from; please use "
2199 "select_from() to establish the left "
2200 "entity/selectable of this join"
2201 )
2202
2203 return left, replace_from_obj_index, use_entity_index
2204
2205 def _join_place_explicit_left_side(self, entities_collection, left):
2206 """When join conditions express a left side explicitly, determine
2207 where in our existing list of FROM clauses we should join towards,
2208 or if we need to make a new join, and if so is it from one of our
2209 existing entities.
2210
2211 """
2212
2213 # when we are here, it means join() was called with an indicator
2214 # as to an exact left side, which means a path to a
2215 # Relationship was given, e.g.:
2216 #
2217 # join(RightEntity, LeftEntity.right)
2218 #
2219 # or
2220 #
2221 # join(LeftEntity.right)
2222 #
2223 # as well as string forms:
2224 #
2225 # join(RightEntity, "right")
2226 #
2227 # etc.
2228 #
2229
2230 replace_from_obj_index = use_entity_index = None
2231
2232 l_info = inspect(left)
2233 if self.from_clauses:
2234 indexes = sql_util.find_left_clause_that_matches_given(
2235 self.from_clauses, l_info.selectable
2236 )
2237
2238 if len(indexes) > 1:
2239 raise sa_exc.InvalidRequestError(
2240 "Can't identify which entity in which to assign the "
2241 "left side of this join. Please use a more specific "
2242 "ON clause."
2243 )
2244
2245 # have an index, means the left side is already present in
2246 # an existing FROM in the self._from_obj tuple
2247 if indexes:
2248 replace_from_obj_index = indexes[0]
2249
2250 # no index, means we need to add a new element to the
2251 # self._from_obj tuple
2252
2253 # no from element present, so we will have to add to the
2254 # self._from_obj tuple. Determine if this left side matches up
2255 # with existing mapper entities, in which case we want to apply the
2256 # aliasing / adaptation rules present on that entity if any
2257 if (
2258 replace_from_obj_index is None
2259 and entities_collection
2260 and hasattr(l_info, "mapper")
2261 ):
2262 for idx, ent in enumerate(entities_collection):
2263 # TODO: should we be checking for multiple mapper entities
2264 # matching?
2265 if isinstance(ent, _MapperEntity) and ent.corresponds_to(left):
2266 use_entity_index = idx
2267 break
2268
2269 return replace_from_obj_index, use_entity_index
2270
2271 def _join_check_and_adapt_right_side(self, left, right, onclause, prop):
2272 """transform the "right" side of the join as well as the onclause
2273 according to polymorphic mapping translations, aliasing on the query
2274 or on the join, special cases where the right and left side have
2275 overlapping tables.
2276
2277 """
2278
2279 l_info = inspect(left)
2280 r_info = inspect(right)
2281
2282 overlap = False
2283
2284 right_mapper = getattr(r_info, "mapper", None)
2285 # if the target is a joined inheritance mapping,
2286 # be more liberal about auto-aliasing.
2287 if right_mapper and (
2288 right_mapper.with_polymorphic
2289 or isinstance(right_mapper.persist_selectable, expression.Join)
2290 ):
2291 for from_obj in self.from_clauses or [l_info.selectable]:
2292 if sql_util.selectables_overlap(
2293 l_info.selectable, from_obj
2294 ) and sql_util.selectables_overlap(
2295 from_obj, r_info.selectable
2296 ):
2297 overlap = True
2298 break
2299
2300 if overlap and l_info.selectable is r_info.selectable:
2301 raise sa_exc.InvalidRequestError(
2302 "Can't join table/selectable '%s' to itself"
2303 % l_info.selectable
2304 )
2305
2306 right_mapper, right_selectable, right_is_aliased = (
2307 getattr(r_info, "mapper", None),
2308 r_info.selectable,
2309 getattr(r_info, "is_aliased_class", False),
2310 )
2311
2312 if (
2313 right_mapper
2314 and prop
2315 and not right_mapper.common_parent(prop.mapper)
2316 ):
2317 raise sa_exc.InvalidRequestError(
2318 "Join target %s does not correspond to "
2319 "the right side of join condition %s" % (right, onclause)
2320 )
2321
2322 # _join_entities is used as a hint for single-table inheritance
2323 # purposes at the moment
2324 if hasattr(r_info, "mapper"):
2325 self._join_entities += (r_info,)
2326
2327 need_adapter = False
2328
2329 # test for joining to an unmapped selectable as the target
2330 if r_info.is_clause_element:
2331 if prop:
2332 right_mapper = prop.mapper
2333
2334 if right_selectable._is_lateral:
2335 # orm_only is disabled to suit the case where we have to
2336 # adapt an explicit correlate(Entity) - the select() loses
2337 # the ORM-ness in this case right now, ideally it would not
2338 current_adapter = self._get_current_adapter()
2339 if current_adapter is not None:
2340 # TODO: we had orm_only=False here before, removing
2341 # it didn't break things. if we identify the rationale,
2342 # may need to apply "_orm_only" annotation here.
2343 right = current_adapter(right, True)
2344
2345 elif prop:
2346 # joining to selectable with a mapper property given
2347 # as the ON clause
2348
2349 if not right_selectable.is_derived_from(
2350 right_mapper.persist_selectable
2351 ):
2352 raise sa_exc.InvalidRequestError(
2353 "Selectable '%s' is not derived from '%s'"
2354 % (
2355 right_selectable.description,
2356 right_mapper.persist_selectable.description,
2357 )
2358 )
2359
2360 # if the destination selectable is a plain select(),
2361 # turn it into an alias().
2362 if isinstance(right_selectable, expression.SelectBase):
2363 right_selectable = coercions.expect(
2364 roles.FromClauseRole, right_selectable
2365 )
2366 need_adapter = True
2367
2368 # make the right hand side target into an ORM entity
2369 right = AliasedClass(right_mapper, right_selectable)
2370
2371 util.warn_deprecated(
2372 "An alias is being generated automatically against "
2373 "joined entity %s for raw clauseelement, which is "
2374 "deprecated and will be removed in a later release. "
2375 "Use the aliased() "
2376 "construct explicitly, see the linked example."
2377 % right_mapper,
2378 "1.4",
2379 code="xaj1",
2380 )
2381
2382 # test for overlap:
2383 # orm/inheritance/relationships.py
2384 # SelfReferentialM2MTest
2385 aliased_entity = right_mapper and not right_is_aliased and overlap
2386
2387 if not need_adapter and aliased_entity:
2388 # there are a few places in the ORM that automatic aliasing
2389 # is still desirable, and can't be automatic with a Core
2390 # only approach. For illustrations of "overlaps" see
2391 # test/orm/inheritance/test_relationships.py. There are also
2392 # general overlap cases with many-to-many tables where automatic
2393 # aliasing is desirable.
2394 right = AliasedClass(right, flat=True)
2395 need_adapter = True
2396
2397 util.warn(
2398 "An alias is being generated automatically against "
2399 "joined entity %s due to overlapping tables. This is a "
2400 "legacy pattern which may be "
2401 "deprecated in a later release. Use the "
2402 "aliased(<entity>, flat=True) "
2403 "construct explicitly, see the linked example." % right_mapper,
2404 code="xaj2",
2405 )
2406
2407 if need_adapter:
2408 # if need_adapter is True, we are in a deprecated case and
2409 # a warning has been emitted.
2410 assert right_mapper
2411
2412 adapter = ORMAdapter(
2413 _TraceAdaptRole.DEPRECATED_JOIN_ADAPT_RIGHT_SIDE,
2414 inspect(right),
2415 equivalents=right_mapper._equivalent_columns,
2416 )
2417
2418 # if an alias() on the right side was generated,
2419 # which is intended to wrap a the right side in a subquery,
2420 # ensure that columns retrieved from this target in the result
2421 # set are also adapted.
2422 self._mapper_loads_polymorphically_with(right_mapper, adapter)
2423 elif (
2424 not r_info.is_clause_element
2425 and not right_is_aliased
2426 and right_mapper._has_aliased_polymorphic_fromclause
2427 ):
2428 # for the case where the target mapper has a with_polymorphic
2429 # set up, ensure an adapter is set up for criteria that works
2430 # against this mapper. Previously, this logic used to
2431 # use the "create_aliases or aliased_entity" case to generate
2432 # an aliased() object, but this creates an alias that isn't
2433 # strictly necessary.
2434 # see test/orm/test_core_compilation.py
2435 # ::RelNaturalAliasedJoinsTest::test_straight
2436 # and similar
2437 self._mapper_loads_polymorphically_with(
2438 right_mapper,
2439 ORMAdapter(
2440 _TraceAdaptRole.WITH_POLYMORPHIC_ADAPTER_RIGHT_JOIN,
2441 right_mapper,
2442 selectable=right_mapper.selectable,
2443 equivalents=right_mapper._equivalent_columns,
2444 ),
2445 )
2446 # if the onclause is a ClauseElement, adapt it with any
2447 # adapters that are in place right now
2448 if isinstance(onclause, expression.ClauseElement):
2449 current_adapter = self._get_current_adapter()
2450 if current_adapter:
2451 onclause = current_adapter(onclause, True)
2452
2453 # if joining on a MapperProperty path,
2454 # track the path to prevent redundant joins
2455 if prop:
2456 self._already_joined_edges += ((left, right, prop.key),)
2457
2458 return inspect(right), right, onclause
2459
2460 @property
2461 def _select_args(self):
2462 return {
2463 "limit_clause": self.select_statement._limit_clause,
2464 "offset_clause": self.select_statement._offset_clause,
2465 "distinct": self.distinct,
2466 "distinct_on": self.distinct_on,
2467 "prefixes": self.select_statement._prefixes,
2468 "suffixes": self.select_statement._suffixes,
2469 "group_by": self.group_by or None,
2470 "fetch_clause": self.select_statement._fetch_clause,
2471 "fetch_clause_options": (
2472 self.select_statement._fetch_clause_options
2473 ),
2474 "independent_ctes": self.select_statement._independent_ctes,
2475 "independent_ctes_opts": (
2476 self.select_statement._independent_ctes_opts
2477 ),
2478 "syntax_extensions": self.syntax_extensions,
2479 }
2480
2481 @property
2482 def _should_nest_selectable(self):
2483 kwargs = self._select_args
2484
2485 if not self.eager_adding_joins:
2486 return False
2487
2488 return (
2489 (
2490 kwargs.get("limit_clause") is not None
2491 and self.multi_row_eager_loaders
2492 )
2493 or (
2494 kwargs.get("offset_clause") is not None
2495 and self.multi_row_eager_loaders
2496 )
2497 or kwargs.get("distinct", False)
2498 or kwargs.get("distinct_on", ())
2499 or kwargs.get("group_by", False)
2500 )
2501
2502 def _get_extra_criteria(self, ext_info):
2503 if (
2504 "additional_entity_criteria",
2505 ext_info.mapper,
2506 ) in self.global_attributes:
2507 return tuple(
2508 ae._resolve_where_criteria(ext_info)
2509 for ae in self.global_attributes[
2510 ("additional_entity_criteria", ext_info.mapper)
2511 ]
2512 if (ae.include_aliases or ae.entity is ext_info)
2513 and ae._should_include(self)
2514 )
2515 else:
2516 return ()
2517
2518 def _adjust_for_extra_criteria(self):
2519 """Apply extra criteria filtering.
2520
2521 For all distinct single-table-inheritance mappers represented in
2522 the columns clause of this query, as well as the "select from entity",
2523 add criterion to the WHERE
2524 clause of the given QueryContext such that only the appropriate
2525 subtypes are selected from the total results.
2526
2527 Additionally, add WHERE criteria originating from LoaderCriteriaOptions
2528 associated with the global context.
2529
2530 """
2531 ext_infos = [
2532 fromclause._annotations.get("parententity", None)
2533 for fromclause in self.from_clauses
2534 ] + [
2535 elem._annotations.get("parententity", None)
2536 for where_crit in self.select_statement._where_criteria
2537 for elem in sql_util.surface_expressions(where_crit)
2538 ]
2539
2540 for ext_info in ext_infos:
2541
2542 if (
2543 ext_info
2544 and (
2545 ext_info.mapper._single_table_criterion is not None
2546 or ("additional_entity_criteria", ext_info.mapper)
2547 in self.global_attributes
2548 )
2549 and ext_info not in self.extra_criteria_entities
2550 ):
2551 self.extra_criteria_entities[ext_info] = (
2552 ext_info,
2553 ext_info._adapter if ext_info.is_aliased_class else None,
2554 )
2555
2556 _where_criteria_to_add = ()
2557
2558 merged_single_crit = collections.defaultdict(
2559 lambda: (util.OrderedSet(), set())
2560 )
2561
2562 for ext_info, adapter in util.OrderedSet(
2563 self.extra_criteria_entities.values()
2564 ):
2565 if ext_info in self._join_entities:
2566 continue
2567
2568 # assemble single table inheritance criteria.
2569 if (
2570 ext_info.is_aliased_class
2571 and ext_info._base_alias()._is_with_polymorphic
2572 ):
2573 # for a with_polymorphic(), we always include the full
2574 # hierarchy from what's given as the base class for the wpoly.
2575 # this is new in 2.1 for #12395 so that it matches the behavior
2576 # of joined inheritance.
2577 hierarchy_root = ext_info._base_alias()
2578 else:
2579 hierarchy_root = ext_info
2580
2581 single_crit_component = (
2582 hierarchy_root.mapper._single_table_criteria_component
2583 )
2584
2585 if single_crit_component is not None:
2586 polymorphic_on, criteria = single_crit_component
2587
2588 polymorphic_on = polymorphic_on._annotate(
2589 {
2590 "parententity": hierarchy_root,
2591 "parentmapper": hierarchy_root.mapper,
2592 }
2593 )
2594
2595 list_of_single_crits, adapters = merged_single_crit[
2596 (hierarchy_root, polymorphic_on)
2597 ]
2598 list_of_single_crits.update(criteria)
2599 if adapter:
2600 adapters.add(adapter)
2601
2602 # assemble "additional entity criteria", which come from
2603 # with_loader_criteria() options
2604 if not self.compile_options._for_refresh_state:
2605 additional_entity_criteria = self._get_extra_criteria(ext_info)
2606 _where_criteria_to_add += tuple(
2607 adapter.traverse(crit) if adapter else crit
2608 for crit in additional_entity_criteria
2609 )
2610
2611 # merge together single table inheritance criteria keyed to
2612 # top-level mapper / aliasedinsp (which may be a with_polymorphic())
2613 for (ext_info, polymorphic_on), (
2614 merged_crit,
2615 adapters,
2616 ) in merged_single_crit.items():
2617 new_crit = polymorphic_on.in_(merged_crit)
2618 for adapter in adapters:
2619 new_crit = adapter.traverse(new_crit)
2620 _where_criteria_to_add += (new_crit,)
2621
2622 current_adapter = self._get_current_adapter()
2623 if current_adapter:
2624 # finally run all the criteria through the "main" adapter, if we
2625 # have one, and concatenate to final WHERE criteria
2626 for crit in _where_criteria_to_add:
2627 crit = current_adapter(crit, False)
2628 self._where_criteria += (crit,)
2629 else:
2630 # else just concatenate our criteria to the final WHERE criteria
2631 self._where_criteria += _where_criteria_to_add
2632
2633
2634def _column_descriptions(
2635 query_or_select_stmt: Union[Query, Select, FromStatement],
2636 compile_state: Optional[_ORMSelectCompileState] = None,
2637 legacy: bool = False,
2638) -> List[ORMColumnDescription]:
2639 if compile_state is None:
2640 compile_state = _ORMSelectCompileState._create_entities_collection(
2641 query_or_select_stmt, legacy=legacy
2642 )
2643 ctx = compile_state
2644 d = [
2645 {
2646 "name": ent._label_name,
2647 "type": ent.type,
2648 "aliased": getattr(insp_ent, "is_aliased_class", False),
2649 "expr": ent.expr,
2650 "entity": (
2651 getattr(insp_ent, "entity", None)
2652 if ent.entity_zero is not None
2653 and not insp_ent.is_clause_element
2654 else None
2655 ),
2656 }
2657 for ent, insp_ent in [
2658 (_ent, _ent.entity_zero) for _ent in ctx._entities
2659 ]
2660 ]
2661 return d
2662
2663
2664def _legacy_filter_by_entity_zero(
2665 query_or_augmented_select: Union[Query[Any], Select[Unpack[TupleAny]]],
2666) -> Optional[_InternalEntityType[Any]]:
2667 self = query_or_augmented_select
2668 if self._setup_joins:
2669 _last_joined_entity = self._last_joined_entity
2670 if _last_joined_entity is not None:
2671 return _last_joined_entity
2672
2673 if self._from_obj and "parententity" in self._from_obj[0]._annotations:
2674 return self._from_obj[0]._annotations["parententity"]
2675
2676 return _entity_from_pre_ent_zero(self)
2677
2678
2679def _entity_from_pre_ent_zero(
2680 query_or_augmented_select: Union[Query[Any], Select[Unpack[TupleAny]]],
2681) -> Optional[_InternalEntityType[Any]]:
2682 self = query_or_augmented_select
2683 if not self._raw_columns:
2684 return None
2685
2686 ent = self._raw_columns[0]
2687
2688 if "parententity" in ent._annotations:
2689 return ent._annotations["parententity"]
2690 elif isinstance(ent, ORMColumnsClauseRole):
2691 return ent.entity
2692 elif "bundle" in ent._annotations:
2693 return ent._annotations["bundle"]
2694 else:
2695 return ent
2696
2697
2698def _determine_last_joined_entity(
2699 setup_joins: Tuple[_SetupJoinsElement, ...],
2700 entity_zero: Optional[_InternalEntityType[Any]] = None,
2701) -> Optional[Union[_InternalEntityType[Any], _JoinTargetElement]]:
2702 if not setup_joins:
2703 return None
2704
2705 (target, onclause, from_, flags) = setup_joins[-1]
2706
2707 if isinstance(
2708 target,
2709 attributes.QueryableAttribute,
2710 ):
2711 return target.entity
2712 else:
2713 return target
2714
2715
2716class _QueryEntity:
2717 """represent an entity column returned within a Query result."""
2718
2719 __slots__ = ()
2720
2721 supports_single_entity: bool
2722
2723 _non_hashable_value = False
2724 _null_column_type = False
2725 use_id_for_hash = False
2726
2727 _label_name: Optional[str]
2728 type: Union[Type[Any], TypeEngine[Any]]
2729 expr: Union[_InternalEntityType, ColumnElement[Any]]
2730 entity_zero: Optional[_InternalEntityType]
2731
2732 def setup_compile_state(self, compile_state: _ORMCompileState) -> None:
2733 raise NotImplementedError()
2734
2735 def setup_dml_returning_compile_state(
2736 self,
2737 compile_state: _ORMCompileState,
2738 adapter: Optional[_DMLReturningColFilter],
2739 ) -> None:
2740 raise NotImplementedError()
2741
2742 def row_processor(self, context, result):
2743 raise NotImplementedError()
2744
2745 @classmethod
2746 def to_compile_state(
2747 cls, compile_state, entities, entities_collection, is_current_entities
2748 ):
2749 for idx, entity in enumerate(entities):
2750 if entity._is_lambda_element:
2751 if entity._is_sequence:
2752 cls.to_compile_state(
2753 compile_state,
2754 entity._resolved,
2755 entities_collection,
2756 is_current_entities,
2757 )
2758 continue
2759 else:
2760 entity = entity._resolved
2761
2762 if entity.is_clause_element:
2763 if entity.is_selectable:
2764 if "parententity" in entity._annotations:
2765 _MapperEntity(
2766 compile_state,
2767 entity,
2768 entities_collection,
2769 is_current_entities,
2770 )
2771 else:
2772 _ColumnEntity._for_columns(
2773 compile_state,
2774 entity._select_iterable,
2775 entities_collection,
2776 idx,
2777 is_current_entities,
2778 )
2779 else:
2780 if entity._annotations.get("bundle", False):
2781 _BundleEntity(
2782 compile_state,
2783 entity,
2784 entities_collection,
2785 is_current_entities,
2786 )
2787 elif entity._is_clause_list:
2788 # this is legacy only - test_composites.py
2789 # test_query_cols_legacy
2790 _ColumnEntity._for_columns(
2791 compile_state,
2792 entity._select_iterable,
2793 entities_collection,
2794 idx,
2795 is_current_entities,
2796 )
2797 else:
2798 _ColumnEntity._for_columns(
2799 compile_state,
2800 [entity],
2801 entities_collection,
2802 idx,
2803 is_current_entities,
2804 )
2805 elif entity.is_bundle:
2806 _BundleEntity(compile_state, entity, entities_collection)
2807
2808 return entities_collection
2809
2810
2811class _MapperEntity(_QueryEntity):
2812 """mapper/class/AliasedClass entity"""
2813
2814 __slots__ = (
2815 "expr",
2816 "mapper",
2817 "entity_zero",
2818 "is_aliased_class",
2819 "path",
2820 "_extra_entities",
2821 "_label_name",
2822 "_with_polymorphic_mappers",
2823 "selectable",
2824 "_polymorphic_discriminator",
2825 )
2826
2827 expr: _InternalEntityType
2828 mapper: Mapper[Any]
2829 entity_zero: _InternalEntityType
2830 is_aliased_class: bool
2831 path: PathRegistry
2832 _label_name: str
2833
2834 def __init__(
2835 self, compile_state, entity, entities_collection, is_current_entities
2836 ):
2837 entities_collection.append(self)
2838 if is_current_entities:
2839 if compile_state._primary_entity is None:
2840 compile_state._primary_entity = self
2841 compile_state._has_mapper_entities = True
2842 compile_state._has_orm_entities = True
2843
2844 entity = entity._annotations["parententity"]
2845 entity._post_inspect
2846 ext_info = self.entity_zero = entity
2847 entity = ext_info.entity
2848
2849 self.expr = entity
2850 self.mapper = mapper = ext_info.mapper
2851
2852 self._extra_entities = (self.expr,)
2853
2854 if ext_info.is_aliased_class:
2855 self._label_name = ext_info.name
2856 else:
2857 self._label_name = mapper.class_.__name__
2858
2859 self.is_aliased_class = ext_info.is_aliased_class
2860 self.path = ext_info._path_registry
2861
2862 self.selectable = ext_info.selectable
2863 self._with_polymorphic_mappers = ext_info.with_polymorphic_mappers
2864 self._polymorphic_discriminator = ext_info.polymorphic_on
2865
2866 if mapper._should_select_with_poly_adapter:
2867 compile_state._create_with_polymorphic_adapter(
2868 ext_info, self.selectable
2869 )
2870
2871 supports_single_entity = True
2872
2873 _non_hashable_value = True
2874 use_id_for_hash = True
2875
2876 @property
2877 def type(self):
2878 return self.mapper.class_
2879
2880 @property
2881 def entity_zero_or_selectable(self):
2882 return self.entity_zero
2883
2884 def corresponds_to(self, entity):
2885 return _entity_corresponds_to(self.entity_zero, entity)
2886
2887 def _get_entity_clauses(self, compile_state):
2888 adapter = None
2889
2890 if not self.is_aliased_class:
2891 if compile_state._polymorphic_adapters:
2892 adapter = compile_state._polymorphic_adapters.get(
2893 self.mapper, None
2894 )
2895 else:
2896 adapter = self.entity_zero._adapter
2897
2898 if adapter:
2899 if compile_state._from_obj_alias:
2900 ret = adapter.wrap(compile_state._from_obj_alias)
2901 else:
2902 ret = adapter
2903 else:
2904 ret = compile_state._from_obj_alias
2905
2906 return ret
2907
2908 def row_processor(self, context, result):
2909 compile_state = context.compile_state
2910 adapter = self._get_entity_clauses(compile_state)
2911
2912 if compile_state.compound_eager_adapter and adapter:
2913 adapter = adapter.wrap(compile_state.compound_eager_adapter)
2914 elif not adapter:
2915 adapter = compile_state.compound_eager_adapter
2916
2917 if compile_state._primary_entity is self:
2918 only_load_props = compile_state.compile_options._only_load_props
2919 refresh_state = context.refresh_state
2920 else:
2921 only_load_props = refresh_state = None
2922
2923 _instance = loading._instance_processor(
2924 self,
2925 self.mapper,
2926 context,
2927 result,
2928 self.path,
2929 adapter,
2930 only_load_props=only_load_props,
2931 refresh_state=refresh_state,
2932 polymorphic_discriminator=self._polymorphic_discriminator,
2933 )
2934
2935 return _instance, self._label_name, self._extra_entities
2936
2937 def setup_dml_returning_compile_state(
2938 self,
2939 compile_state: _ORMCompileState,
2940 adapter: Optional[_DMLReturningColFilter],
2941 ) -> None:
2942 loading._setup_entity_query(
2943 compile_state,
2944 self.mapper,
2945 self,
2946 self.path,
2947 adapter,
2948 compile_state.primary_columns,
2949 with_polymorphic=self._with_polymorphic_mappers,
2950 only_load_props=compile_state.compile_options._only_load_props,
2951 polymorphic_discriminator=self._polymorphic_discriminator,
2952 )
2953
2954 def setup_compile_state(self, compile_state):
2955 adapter = self._get_entity_clauses(compile_state)
2956
2957 single_table_crit = self.mapper._single_table_criterion
2958 if (
2959 single_table_crit is not None
2960 or ("additional_entity_criteria", self.mapper)
2961 in compile_state.global_attributes
2962 ):
2963 ext_info = self.entity_zero
2964 compile_state.extra_criteria_entities[ext_info] = (
2965 ext_info,
2966 ext_info._adapter if ext_info.is_aliased_class else None,
2967 )
2968
2969 loading._setup_entity_query(
2970 compile_state,
2971 self.mapper,
2972 self,
2973 self.path,
2974 adapter,
2975 compile_state.primary_columns,
2976 with_polymorphic=self._with_polymorphic_mappers,
2977 only_load_props=compile_state.compile_options._only_load_props,
2978 polymorphic_discriminator=self._polymorphic_discriminator,
2979 )
2980 compile_state._fallback_from_clauses.append(self.selectable)
2981
2982
2983class _BundleEntity(_QueryEntity):
2984 _extra_entities = ()
2985
2986 __slots__ = (
2987 "bundle",
2988 "expr",
2989 "type",
2990 "_label_name",
2991 "_entities",
2992 "supports_single_entity",
2993 )
2994
2995 _entities: List[_QueryEntity]
2996 bundle: Bundle
2997 type: Type[Any]
2998 _label_name: str
2999 supports_single_entity: bool
3000 expr: Bundle
3001
3002 def __init__(
3003 self,
3004 compile_state,
3005 expr,
3006 entities_collection,
3007 is_current_entities,
3008 setup_entities=True,
3009 parent_bundle=None,
3010 ):
3011 compile_state._has_orm_entities = True
3012
3013 expr = expr._annotations["bundle"]
3014 if parent_bundle:
3015 parent_bundle._entities.append(self)
3016 else:
3017 entities_collection.append(self)
3018
3019 if isinstance(
3020 expr, (attributes.QueryableAttribute, interfaces.PropComparator)
3021 ):
3022 bundle = expr.__clause_element__()
3023 else:
3024 bundle = expr
3025
3026 self.bundle = self.expr = bundle
3027 self.type = type(bundle)
3028 self._label_name = bundle.name
3029 self._entities = []
3030
3031 if setup_entities:
3032 for expr in bundle.exprs:
3033 if "bundle" in expr._annotations:
3034 _BundleEntity(
3035 compile_state,
3036 expr,
3037 entities_collection,
3038 is_current_entities,
3039 parent_bundle=self,
3040 )
3041 elif isinstance(expr, Bundle):
3042 _BundleEntity(
3043 compile_state,
3044 expr,
3045 entities_collection,
3046 is_current_entities,
3047 parent_bundle=self,
3048 )
3049 else:
3050 _ORMColumnEntity._for_columns(
3051 compile_state,
3052 [expr],
3053 entities_collection,
3054 None,
3055 is_current_entities,
3056 parent_bundle=self,
3057 )
3058
3059 self.supports_single_entity = self.bundle.single_entity
3060
3061 @property
3062 def mapper(self):
3063 ezero = self.entity_zero
3064 if ezero is not None:
3065 return ezero.mapper
3066 else:
3067 return None
3068
3069 @property
3070 def entity_zero(self):
3071 for ent in self._entities:
3072 ezero = ent.entity_zero
3073 if ezero is not None:
3074 return ezero
3075 else:
3076 return None
3077
3078 def corresponds_to(self, entity):
3079 # TODO: we might be able to implement this but for now
3080 # we are working around it
3081 return False
3082
3083 @property
3084 def entity_zero_or_selectable(self):
3085 for ent in self._entities:
3086 ezero = ent.entity_zero_or_selectable
3087 if ezero is not None:
3088 return ezero
3089 else:
3090 return None
3091
3092 def setup_compile_state(self, compile_state):
3093 for ent in self._entities:
3094 ent.setup_compile_state(compile_state)
3095
3096 def setup_dml_returning_compile_state(
3097 self,
3098 compile_state: _ORMCompileState,
3099 adapter: Optional[_DMLReturningColFilter],
3100 ) -> None:
3101 return self.setup_compile_state(compile_state)
3102
3103 def row_processor(self, context, result):
3104 procs, labels, extra = zip(
3105 *[ent.row_processor(context, result) for ent in self._entities]
3106 )
3107
3108 proc = self.bundle.create_row_processor(context.query, procs, labels)
3109
3110 return proc, self._label_name, self._extra_entities
3111
3112
3113class _ColumnEntity(_QueryEntity):
3114 __slots__ = (
3115 "_fetch_column",
3116 "_row_processor",
3117 "raw_column_index",
3118 "translate_raw_column",
3119 )
3120
3121 @classmethod
3122 def _for_columns(
3123 cls,
3124 compile_state,
3125 columns,
3126 entities_collection,
3127 raw_column_index,
3128 is_current_entities,
3129 parent_bundle=None,
3130 ):
3131 for column in columns:
3132 annotations = column._annotations
3133 if "parententity" in annotations:
3134 _entity = annotations["parententity"]
3135 else:
3136 _entity = sql_util.extract_first_column_annotation(
3137 column, "parententity"
3138 )
3139
3140 if _entity:
3141 if "identity_token" in column._annotations:
3142 _IdentityTokenEntity(
3143 compile_state,
3144 column,
3145 entities_collection,
3146 _entity,
3147 raw_column_index,
3148 is_current_entities,
3149 parent_bundle=parent_bundle,
3150 )
3151 else:
3152 _ORMColumnEntity(
3153 compile_state,
3154 column,
3155 entities_collection,
3156 _entity,
3157 raw_column_index,
3158 is_current_entities,
3159 parent_bundle=parent_bundle,
3160 )
3161 else:
3162 _RawColumnEntity(
3163 compile_state,
3164 column,
3165 entities_collection,
3166 raw_column_index,
3167 is_current_entities,
3168 parent_bundle=parent_bundle,
3169 )
3170
3171 @property
3172 def type(self):
3173 return self.column.type
3174
3175 @property
3176 def _non_hashable_value(self):
3177 return not self.column.type.hashable
3178
3179 @property
3180 def _null_column_type(self):
3181 return self.column.type._isnull
3182
3183 def row_processor(self, context, result):
3184 compile_state = context.compile_state
3185
3186 # the resulting callable is entirely cacheable so just return
3187 # it if we already made one
3188 if self._row_processor is not None:
3189 getter, label_name, extra_entities = self._row_processor
3190 if self.translate_raw_column:
3191 extra_entities += (
3192 context.query._raw_columns[self.raw_column_index],
3193 )
3194
3195 return getter, label_name, extra_entities
3196
3197 # retrieve the column that would have been set up in
3198 # setup_compile_state, to avoid doing redundant work
3199 if self._fetch_column is not None:
3200 column = self._fetch_column
3201 else:
3202 # fetch_column will be None when we are doing a from_statement
3203 # and setup_compile_state may not have been called.
3204 column = self.column
3205
3206 # previously, the RawColumnEntity didn't look for from_obj_alias
3207 # however I can't think of a case where we would be here and
3208 # we'd want to ignore it if this is the from_statement use case.
3209 # it's not really a use case to have raw columns + from_statement
3210 if compile_state._from_obj_alias:
3211 column = compile_state._from_obj_alias.columns[column]
3212
3213 if column._annotations:
3214 # annotated columns perform more slowly in compiler and
3215 # result due to the __eq__() method, so use deannotated
3216 column = column._deannotate()
3217
3218 if compile_state.compound_eager_adapter:
3219 column = compile_state.compound_eager_adapter.columns[column]
3220
3221 getter = result._getter(column)
3222 ret = getter, self._label_name, self._extra_entities
3223 self._row_processor = ret
3224
3225 if self.translate_raw_column:
3226 extra_entities = self._extra_entities + (
3227 context.query._raw_columns[self.raw_column_index],
3228 )
3229 return getter, self._label_name, extra_entities
3230 else:
3231 return ret
3232
3233
3234class _RawColumnEntity(_ColumnEntity):
3235 entity_zero = None
3236 mapper = None
3237 supports_single_entity = False
3238
3239 __slots__ = (
3240 "expr",
3241 "column",
3242 "_label_name",
3243 "entity_zero_or_selectable",
3244 "_extra_entities",
3245 )
3246
3247 def __init__(
3248 self,
3249 compile_state,
3250 column,
3251 entities_collection,
3252 raw_column_index,
3253 is_current_entities,
3254 parent_bundle=None,
3255 ):
3256 self.expr = column
3257 self.raw_column_index = raw_column_index
3258 self.translate_raw_column = raw_column_index is not None
3259
3260 if column._is_star:
3261 compile_state.compile_options += {"_is_star": True}
3262
3263 if not is_current_entities or column._is_text_clause:
3264 self._label_name = None
3265 else:
3266 if parent_bundle:
3267 self._label_name = column._proxy_key
3268 else:
3269 self._label_name = compile_state._label_convention(column)
3270
3271 if parent_bundle:
3272 parent_bundle._entities.append(self)
3273 else:
3274 entities_collection.append(self)
3275
3276 self.column = column
3277 self.entity_zero_or_selectable = (
3278 self.column._from_objects[0] if self.column._from_objects else None
3279 )
3280 self._extra_entities = (self.expr, self.column)
3281 self._fetch_column = self._row_processor = None
3282
3283 def corresponds_to(self, entity):
3284 return False
3285
3286 def setup_dml_returning_compile_state(
3287 self,
3288 compile_state: _ORMCompileState,
3289 adapter: Optional[_DMLReturningColFilter],
3290 ) -> None:
3291 return self.setup_compile_state(compile_state)
3292
3293 def setup_compile_state(self, compile_state):
3294 current_adapter = compile_state._get_current_adapter()
3295 if current_adapter:
3296 column = current_adapter(self.column, False)
3297 if column is None:
3298 return
3299 else:
3300 column = self.column
3301
3302 if column._annotations:
3303 # annotated columns perform more slowly in compiler and
3304 # result due to the __eq__() method, so use deannotated
3305 column = column._deannotate()
3306
3307 compile_state.dedupe_columns.add(column)
3308 compile_state.primary_columns.append(column)
3309 self._fetch_column = column
3310
3311
3312class _ORMColumnEntity(_ColumnEntity):
3313 """Column/expression based entity."""
3314
3315 supports_single_entity = False
3316
3317 __slots__ = (
3318 "expr",
3319 "mapper",
3320 "column",
3321 "_label_name",
3322 "entity_zero_or_selectable",
3323 "entity_zero",
3324 "_extra_entities",
3325 )
3326
3327 def __init__(
3328 self,
3329 compile_state,
3330 column,
3331 entities_collection,
3332 parententity,
3333 raw_column_index,
3334 is_current_entities,
3335 parent_bundle=None,
3336 ):
3337 annotations = column._annotations
3338
3339 _entity = parententity
3340
3341 # an AliasedClass won't have proxy_key in the annotations for
3342 # a column if it was acquired using the class' adapter directly,
3343 # such as using AliasedInsp._adapt_element(). this occurs
3344 # within internal loaders.
3345
3346 orm_key = annotations.get("proxy_key", None)
3347 proxy_owner = annotations.get("proxy_owner", _entity)
3348 if orm_key:
3349 self.expr = getattr(proxy_owner.entity, orm_key)
3350 self.translate_raw_column = False
3351 else:
3352 # if orm_key is not present, that means this is an ad-hoc
3353 # SQL ColumnElement, like a CASE() or other expression.
3354 # include this column position from the invoked statement
3355 # in the ORM-level ResultSetMetaData on each execute, so that
3356 # it can be targeted by identity after caching
3357 self.expr = column
3358 self.translate_raw_column = raw_column_index is not None
3359
3360 self.raw_column_index = raw_column_index
3361
3362 if is_current_entities:
3363 if parent_bundle:
3364 self._label_name = orm_key if orm_key else column._proxy_key
3365 else:
3366 self._label_name = compile_state._label_convention(
3367 column, col_name=orm_key
3368 )
3369 else:
3370 self._label_name = None
3371
3372 _entity._post_inspect
3373 self.entity_zero = self.entity_zero_or_selectable = ezero = _entity
3374 self.mapper = mapper = _entity.mapper
3375
3376 if parent_bundle:
3377 parent_bundle._entities.append(self)
3378 else:
3379 entities_collection.append(self)
3380
3381 compile_state._has_orm_entities = True
3382
3383 self.column = column
3384
3385 self._fetch_column = self._row_processor = None
3386
3387 self._extra_entities = (self.expr, self.column)
3388
3389 if mapper._should_select_with_poly_adapter:
3390 compile_state._create_with_polymorphic_adapter(
3391 ezero, ezero.selectable
3392 )
3393
3394 def corresponds_to(self, entity):
3395 if _is_aliased_class(entity):
3396 # TODO: polymorphic subclasses ?
3397 return entity is self.entity_zero
3398 else:
3399 return not _is_aliased_class(
3400 self.entity_zero
3401 ) and entity.common_parent(self.entity_zero)
3402
3403 def setup_dml_returning_compile_state(
3404 self,
3405 compile_state: _ORMCompileState,
3406 adapter: Optional[_DMLReturningColFilter],
3407 ) -> None:
3408
3409 self._fetch_column = column = self.column
3410 if adapter:
3411 column = adapter(column, False)
3412
3413 if column is not None:
3414 compile_state.dedupe_columns.add(column)
3415 compile_state.primary_columns.append(column)
3416
3417 def setup_compile_state(self, compile_state):
3418 current_adapter = compile_state._get_current_adapter()
3419 if current_adapter:
3420 column = current_adapter(self.column, False)
3421 if column is None:
3422 assert compile_state.is_dml_returning
3423 self._fetch_column = self.column
3424 return
3425 else:
3426 column = self.column
3427
3428 ezero = self.entity_zero
3429
3430 single_table_crit = self.mapper._single_table_criterion
3431 if (
3432 single_table_crit is not None
3433 or ("additional_entity_criteria", self.mapper)
3434 in compile_state.global_attributes
3435 ):
3436 compile_state.extra_criteria_entities[ezero] = (
3437 ezero,
3438 ezero._adapter if ezero.is_aliased_class else None,
3439 )
3440
3441 if column._annotations and not column._expression_label:
3442 # annotated columns perform more slowly in compiler and
3443 # result due to the __eq__() method, so use deannotated
3444 column = column._deannotate()
3445
3446 # use entity_zero as the from if we have it. this is necessary
3447 # for polymorphic scenarios where our FROM is based on ORM entity,
3448 # not the FROM of the column. but also, don't use it if our column
3449 # doesn't actually have any FROMs that line up, such as when its
3450 # a scalar subquery.
3451 if set(self.column._from_objects).intersection(
3452 ezero.selectable._from_objects
3453 ):
3454 compile_state._fallback_from_clauses.append(ezero.selectable)
3455
3456 compile_state.dedupe_columns.add(column)
3457 compile_state.primary_columns.append(column)
3458 self._fetch_column = column
3459
3460
3461class _IdentityTokenEntity(_ORMColumnEntity):
3462 translate_raw_column = False
3463
3464 def setup_compile_state(self, compile_state):
3465 pass
3466
3467 def row_processor(self, context, result):
3468 def getter(row):
3469 return context.load_options._identity_token
3470
3471 return getter, self._label_name, self._extra_entities