diff options
Diffstat (limited to 'lib/sqlalchemy/sql/selectable.py')
-rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 304 |
1 files changed, 157 insertions, 147 deletions
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 5167182fe..41be9fc5a 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -15,8 +15,9 @@ import itertools import operator from operator import attrgetter -from sqlalchemy.sql.visitors import Visitable +from . import coercions from . import operators +from . import roles from . import type_api from .annotation import Annotated from .base import _from_objects @@ -26,18 +27,12 @@ from .base import ColumnSet from .base import Executable from .base import Generative from .base import Immutable +from .coercions import _document_text_coercion from .elements import _anonymous_label -from .elements import _clause_element_as_expr from .elements import _clone from .elements import _cloned_difference from .elements import _cloned_intersection -from .elements import _document_text_coercion from .elements import _expand_cloned -from .elements import _interpret_as_column_or_from -from .elements import _literal_and_labels_as_label_reference -from .elements import _literal_as_label_reference -from .elements import _literal_as_text -from .elements import _no_text_coercion from .elements import _select_iterables from .elements import and_ from .elements import BindParameter @@ -48,75 +43,15 @@ from .elements import literal_column from .elements import True_ from .elements import UnaryExpression from .. import exc -from .. import inspection from .. import util -def _interpret_as_from(element): - insp = inspection.inspect(element, raiseerr=False) - if insp is None: - if isinstance(element, util.string_types): - _no_text_coercion(element) - try: - return insp.selectable - except AttributeError: - raise exc.ArgumentError("FROM expression expected") - - -def _interpret_as_select(element): - element = _interpret_as_from(element) - if isinstance(element, Alias): - element = element.original - if not isinstance(element, SelectBase): - element = element.select() - return element - - class _OffsetLimitParam(BindParameter): @property def _limit_offset_value(self): return self.effective_value -def _offset_or_limit_clause(element, name=None, type_=None): - """Convert the given value to an "offset or limit" clause. - - This handles incoming integers and converts to an expression; if - an expression is already given, it is passed through. - - """ - if element is None: - return None - elif hasattr(element, "__clause_element__"): - return element.__clause_element__() - elif isinstance(element, Visitable): - return element - else: - value = util.asint(element) - return _OffsetLimitParam(name, value, type_=type_, unique=True) - - -def _offset_or_limit_clause_asint(clause, attrname): - """Convert the "offset or limit" clause of a select construct to an - integer. - - This is only possible if the value is stored as a simple bound parameter. - Otherwise, a compilation error is raised. - - """ - if clause is None: - return None - try: - value = clause._limit_offset_value - except AttributeError: - raise exc.CompileError( - "This SELECT structure does not use a simple " - "integer value for %s" % attrname - ) - else: - return util.asint(value) - - def subquery(alias, *args, **kwargs): r"""Return an :class:`.Alias` object derived from a :class:`.Select`. @@ -133,8 +68,42 @@ def subquery(alias, *args, **kwargs): return Select(*args, **kwargs).alias(alias) -class Selectable(ClauseElement): - """mark a class as being selectable""" +class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): + """The basemost class for Core contructs that have some concept of + columns that can represent rows. + + While the SELECT statement and TABLE are the primary things we think + of in this category, DML like INSERT, UPDATE and DELETE can also specify + RETURNING which means they can be used in CTEs and other forms, and + PostgreSQL has functions that return rows also. + + .. versionadded:: 1.4 + + """ + + _is_returns_rows = True + + # sub-elements of returns_rows + _is_from_clause = False + _is_select_statement = False + _is_lateral = False + + @property + def selectable(self): + raise NotImplementedError( + "This object is a base ReturnsRows object, but is not a " + "FromClause so has no .c. collection." + ) + + +class Selectable(ReturnsRows): + """mark a class as being selectable. + + This class is legacy as of 1.4 as the concept of a SQL construct which + "returns rows" is more generalized than one which can be the subject + of a SELECT. + + """ __visit_name__ = "selectable" @@ -190,7 +159,7 @@ class HasPrefixes(object): def _setup_prefixes(self, prefixes, dialect=None): self._prefixes = self._prefixes + tuple( [ - (_literal_as_text(p, allow_coercion_to_text=True), dialect) + (coercions.expect(roles.StatementOptionRole, p), dialect) for p in prefixes ] ) @@ -236,13 +205,13 @@ class HasSuffixes(object): def _setup_suffixes(self, suffixes, dialect=None): self._suffixes = self._suffixes + tuple( [ - (_literal_as_text(p, allow_coercion_to_text=True), dialect) + (coercions.expect(roles.StatementOptionRole, p), dialect) for p in suffixes ] ) -class FromClause(Selectable): +class FromClause(roles.FromClauseRole, Selectable): """Represent an element that can be used within the ``FROM`` clause of a ``SELECT`` statement. @@ -265,16 +234,6 @@ class FromClause(Selectable): named_with_column = False _hide_froms = [] - _is_join = False - _is_select = False - _is_from_container = False - - _is_lateral = False - - _textual = False - """a marker that allows us to easily distinguish a :class:`.TextAsFrom` - or similar object from other kinds of :class:`.FromClause` objects.""" - schema = None """Define the 'schema' attribute for this :class:`.FromClause`. @@ -284,6 +243,11 @@ class FromClause(Selectable): """ + is_selectable = has_selectable = True + _is_from_clause = True + _is_text_as_from = False + _is_join = False + def _translate_schema(self, effective_schema, map_): return effective_schema @@ -726,8 +690,8 @@ class Join(FromClause): :class:`.FromClause` object. """ - self.left = _interpret_as_from(left) - self.right = _interpret_as_from(right).self_group() + self.left = coercions.expect(roles.FromClauseRole, left) + self.right = coercions.expect(roles.FromClauseRole, right).self_group() if onclause is None: self.onclause = self._match_primaries(self.left, self.right) @@ -1292,7 +1256,9 @@ class Alias(FromClause): .. versionadded:: 0.9.0 """ - return _interpret_as_from(selectable).alias(name=name, flat=flat) + return coercions.expect(roles.FromClauseRole, selectable).alias( + name=name, flat=flat + ) def _init(self, selectable, name=None): baseselectable = selectable @@ -1327,14 +1293,6 @@ class Alias(FromClause): else: return self.name.encode("ascii", "backslashreplace") - def as_scalar(self): - try: - return self.element.as_scalar() - except AttributeError: - raise AttributeError( - "Element %s does not support " "'as_scalar()'" % self.element - ) - def is_derived_from(self, fromclause): if fromclause in self._cloned_set: return True @@ -1426,7 +1384,9 @@ class Lateral(Alias): :ref:`lateral_selects` - overview of usage. """ - return _interpret_as_from(selectable).lateral(name=name) + return coercions.expect(roles.FromClauseRole, selectable).lateral( + name=name + ) class TableSample(Alias): @@ -1488,7 +1448,7 @@ class TableSample(Alias): REPEATABLE sub-clause is also rendered. """ - return _interpret_as_from(selectable).tablesample( + return coercions.expect(roles.FromClauseRole, selectable).tablesample( sampling, name=name, seed=seed ) @@ -1523,7 +1483,7 @@ class CTE(Generative, HasSuffixes, Alias): Please see :meth:`.HasCte.cte` for detail on CTE usage. """ - return _interpret_as_from(selectable).cte( + return coercions.expect(roles.HasCTERole, selectable).cte( name=name, recursive=recursive ) @@ -1588,7 +1548,7 @@ class CTE(Generative, HasSuffixes, Alias): ) -class HasCTE(object): +class HasCTE(roles.HasCTERole): """Mixin that declares a class to include CTE support. .. versionadded:: 1.1 @@ -2059,13 +2019,22 @@ class ForUpdateArg(ClauseElement): self.key_share = key_share if of is not None: self.of = [ - _interpret_as_column_or_from(elem) for elem in util.to_list(of) + coercions.expect(roles.ColumnsClauseRole, elem) + for elem in util.to_list(of) ] else: self.of = None -class SelectBase(HasCTE, Executable, FromClause): +class SelectBase( + roles.SelectStatementRole, + roles.DMLSelectRole, + roles.CompoundElementRole, + roles.InElementRole, + HasCTE, + Executable, + FromClause, +): """Base class for SELECT statements. @@ -2075,15 +2044,32 @@ class SelectBase(HasCTE, Executable, FromClause): """ + _is_select_statement = True + + @util.deprecated( + "1.4", + "The :meth:`.SelectBase.as_scalar` method is deprecated and will be " + "removed in a future release. Please refer to " + ":meth:`.SelectBase.scalar_subquery`.", + ) def as_scalar(self): + return self.scalar_subquery() + + def scalar_subquery(self): """return a 'scalar' representation of this selectable, which can be used as a column expression. Typically, a select statement which has only one column in its columns - clause is eligible to be used as a scalar expression. + clause is eligible to be used as a scalar expression. The scalar + subquery can then be used in the WHERE clause or columns clause of + an enclosing SELECT. - The returned object is an instance of - :class:`ScalarSelect`. + Note that the scalar subquery differentiates from the FROM-level + subquery that can be produced using the :meth:`.SelectBase.subquery` + method. + + .. versionchanged: 1.4 - the ``.as_scalar()`` method was renamed to + :meth:`.SelectBase.scalar_subquery`. """ return ScalarSelect(self) @@ -2097,7 +2083,7 @@ class SelectBase(HasCTE, Executable, FromClause): :meth:`~.SelectBase.as_scalar`. """ - return self.as_scalar().label(name) + return self.scalar_subquery().label(name) @_generative @util.deprecated( @@ -2181,20 +2167,19 @@ class GenerativeSelect(SelectBase): {"autocommit": autocommit} ) if limit is not None: - self._limit_clause = _offset_or_limit_clause(limit) + self._limit_clause = self._offset_or_limit_clause(limit) if offset is not None: - self._offset_clause = _offset_or_limit_clause(offset) + self._offset_clause = self._offset_or_limit_clause(offset) self._bind = bind if order_by is not None: self._order_by_clause = ClauseList( *util.to_list(order_by), - _literal_as_text=_literal_and_labels_as_label_reference + _literal_as_text_role=roles.OrderByRole ) if group_by is not None: self._group_by_clause = ClauseList( - *util.to_list(group_by), - _literal_as_text=_literal_as_label_reference + *util.to_list(group_by), _literal_as_text_role=roles.ByOfRole ) @property @@ -2287,6 +2272,37 @@ class GenerativeSelect(SelectBase): """ self.use_labels = True + def _offset_or_limit_clause(self, element, name=None, type_=None): + """Convert the given value to an "offset or limit" clause. + + This handles incoming integers and converts to an expression; if + an expression is already given, it is passed through. + + """ + return coercions.expect( + roles.LimitOffsetRole, element, name=name, type_=type_ + ) + + def _offset_or_limit_clause_asint(self, clause, attrname): + """Convert the "offset or limit" clause of a select construct to an + integer. + + This is only possible if the value is stored as a simple bound + parameter. Otherwise, a compilation error is raised. + + """ + if clause is None: + return None + try: + value = clause._limit_offset_value + except AttributeError: + raise exc.CompileError( + "This SELECT structure does not use a simple " + "integer value for %s" % attrname + ) + else: + return util.asint(value) + @property def _limit(self): """Get an integer value for the limit. This should only be used @@ -2295,7 +2311,7 @@ class GenerativeSelect(SelectBase): isn't currently set to an integer. """ - return _offset_or_limit_clause_asint(self._limit_clause, "limit") + return self._offset_or_limit_clause_asint(self._limit_clause, "limit") @property def _simple_int_limit(self): @@ -2319,7 +2335,9 @@ class GenerativeSelect(SelectBase): offset isn't currently set to an integer. """ - return _offset_or_limit_clause_asint(self._offset_clause, "offset") + return self._offset_or_limit_clause_asint( + self._offset_clause, "offset" + ) @_generative def limit(self, limit): @@ -2339,7 +2357,7 @@ class GenerativeSelect(SelectBase): """ - self._limit_clause = _offset_or_limit_clause(limit) + self._limit_clause = self._offset_or_limit_clause(limit) @_generative def offset(self, offset): @@ -2361,7 +2379,7 @@ class GenerativeSelect(SelectBase): """ - self._offset_clause = _offset_or_limit_clause(offset) + self._offset_clause = self._offset_or_limit_clause(offset) @_generative def order_by(self, *clauses): @@ -2403,8 +2421,7 @@ class GenerativeSelect(SelectBase): if getattr(self, "_order_by_clause", None) is not None: clauses = list(self._order_by_clause) + list(clauses) self._order_by_clause = ClauseList( - *clauses, - _literal_as_text=_literal_and_labels_as_label_reference + *clauses, _literal_as_text_role=roles.OrderByRole ) def append_group_by(self, *clauses): @@ -2423,7 +2440,7 @@ class GenerativeSelect(SelectBase): if getattr(self, "_group_by_clause", None) is not None: clauses = list(self._group_by_clause) + list(clauses) self._group_by_clause = ClauseList( - *clauses, _literal_as_text=_literal_as_label_reference + *clauses, _literal_as_text_role=roles.ByOfRole ) @property @@ -2478,7 +2495,7 @@ class CompoundSelect(GenerativeSelect): # some DBs do not like ORDER BY in the inner queries of a UNION, etc. for n, s in enumerate(selects): - s = _clause_element_as_expr(s) + s = coercions.expect(roles.CompoundElementRole, s) if not numcols: numcols = len(s.c._all_columns) @@ -2741,7 +2758,6 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): _correlate = () _correlate_except = None _memoized_property = SelectBase._memoized_property - _is_select = True @util.deprecated_params( autocommit=( @@ -2965,12 +2981,14 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): self._distinct = True else: self._distinct = [ - _literal_as_text(e) for e in util.to_list(distinct) + coercions.expect(roles.WhereHavingRole, e) + for e in util.to_list(distinct) ] if from_obj is not None: self._from_obj = util.OrderedSet( - _interpret_as_from(f) for f in util.to_list(from_obj) + coercions.expect(roles.FromClauseRole, f) + for f in util.to_list(from_obj) ) else: self._from_obj = util.OrderedSet() @@ -2986,7 +3004,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): if cols_present: self._raw_columns = [] for c in columns: - c = _interpret_as_column_or_from(c) + c = coercions.expect(roles.ColumnsClauseRole, c) if isinstance(c, ScalarSelect): c = c.self_group(against=operators.comma_op) self._raw_columns.append(c) @@ -2994,16 +3012,16 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): self._raw_columns = [] if whereclause is not None: - self._whereclause = _literal_as_text(whereclause).self_group( - against=operators._asbool - ) + self._whereclause = coercions.expect( + roles.WhereHavingRole, whereclause + ).self_group(against=operators._asbool) else: self._whereclause = None if having is not None: - self._having = _literal_as_text(having).self_group( - against=operators._asbool - ) + self._having = coercions.expect( + roles.WhereHavingRole, having + ).self_group(against=operators._asbool) else: self._having = None @@ -3202,15 +3220,6 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): else: self._hints = self._hints.union({(selectable, dialect_name): text}) - @property - def type(self): - raise exc.InvalidRequestError( - "Select objects don't have a type. " - "Call as_scalar() on this Select " - "object to return a 'scalar' version " - "of this Select." - ) - @_memoized_property.method def locate_all_froms(self): """return a Set of all FromClause elements referenced by this Select. @@ -3496,7 +3505,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): self._reset_exported() rc = [] for c in columns: - c = _interpret_as_column_or_from(c) + c = coercions.expect(roles.ColumnsClauseRole, c) if isinstance(c, ScalarSelect): c = c.self_group(against=operators.comma_op) rc.append(c) @@ -3530,7 +3539,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): """ if expr: - expr = [_literal_as_label_reference(e) for e in expr] + expr = [coercions.expect(roles.ByOfRole, e) for e in expr] if isinstance(self._distinct, list): self._distinct = self._distinct + expr else: @@ -3618,7 +3627,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): self._correlate = () else: self._correlate = set(self._correlate).union( - _interpret_as_from(f) for f in fromclauses + coercions.expect(roles.FromClauseRole, f) for f in fromclauses ) @_generative @@ -3653,7 +3662,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): self._correlate_except = () else: self._correlate_except = set(self._correlate_except or ()).union( - _interpret_as_from(f) for f in fromclauses + coercions.expect(roles.FromClauseRole, f) for f in fromclauses ) def append_correlation(self, fromclause): @@ -3668,7 +3677,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): self._auto_correlate = False self._correlate = set(self._correlate).union( - _interpret_as_from(f) for f in fromclause + coercions.expect(roles.FromClauseRole, f) for f in fromclause ) def append_column(self, column): @@ -3689,7 +3698,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): """ self._reset_exported() - column = _interpret_as_column_or_from(column) + column = coercions.expect(roles.ColumnsClauseRole, column) if isinstance(column, ScalarSelect): column = column.self_group(against=operators.comma_op) @@ -3705,7 +3714,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): standard :term:`method chaining`. """ - clause = _literal_as_text(clause) + clause = coercions.expect(roles.WhereHavingRole, clause) self._prefixes = self._prefixes + (clause,) def append_whereclause(self, whereclause): @@ -3747,7 +3756,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): """ self._reset_exported() - fromclause = _interpret_as_from(fromclause) + fromclause = coercions.expect(roles.FromClauseRole, fromclause) self._from_obj = self._from_obj.union([fromclause]) @_memoized_property @@ -3894,7 +3903,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): bind = property(bind, _set_bind) -class ScalarSelect(Generative, Grouping): +class ScalarSelect(roles.InElementRole, Generative, Grouping): _from_objects = [] _is_from_container = True _is_implicitly_boolean = False @@ -3956,7 +3965,7 @@ class Exists(UnaryExpression): else: if not args: args = ([literal_column("*")],) - s = Select(*args, **kwargs).as_scalar().self_group() + s = Select(*args, **kwargs).scalar_subquery().self_group() UnaryExpression.__init__( self, @@ -3999,6 +4008,7 @@ class Exists(UnaryExpression): return e +# TODO: rename to TextualSelect, this is not a FROM clause class TextAsFrom(SelectBase): """Wrap a :class:`.TextClause` construct within a :class:`.SelectBase` interface. @@ -4022,7 +4032,7 @@ class TextAsFrom(SelectBase): __visit_name__ = "text_as_from" - _textual = True + _is_textual = True def __init__(self, text, columns, positional=False): self.element = text |