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