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