summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/orm/mapper.py39
-rw-r--r--lib/sqlalchemy/orm/query.py15
-rw-r--r--lib/sqlalchemy/orm/relationships.py12
-rw-r--r--lib/sqlalchemy/orm/util.py19
-rw-r--r--lib/sqlalchemy/sql/coercions.py42
-rw-r--r--lib/sqlalchemy/sql/roles.py24
-rw-r--r--lib/sqlalchemy/sql/selectable.py42
7 files changed, 142 insertions, 51 deletions
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 3c26f7247..33a474576 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -638,7 +638,13 @@ class Mapper(InspectionAttr):
self.concrete = concrete
self.single = False
self.inherits = inherits
- self.local_table = local_table
+ if local_table is not None:
+ self.local_table = coercions.expect(
+ roles.StrictFromClauseRole, local_table
+ )
+ else:
+ self.local_table = None
+
self.inherit_condition = inherit_condition
self.inherit_foreign_keys = inherit_foreign_keys
self._init_properties = properties or {}
@@ -674,14 +680,6 @@ class Mapper(InspectionAttr):
else:
self.confirm_deleted_rows = confirm_deleted_rows
- if isinstance(self.local_table, expression.SelectBase):
- raise sa_exc.InvalidRequestError(
- "When mapping against a select() construct, map against "
- "an alias() of the construct instead."
- "This because several databases don't allow a "
- "SELECT from a subquery that does not have an alias."
- )
-
self._set_with_polymorphic(with_polymorphic)
self.polymorphic_load = polymorphic_load
@@ -1154,20 +1152,14 @@ class Mapper(InspectionAttr):
else:
self.with_polymorphic = None
- if isinstance(self.local_table, expression.SelectBase):
- raise sa_exc.InvalidRequestError(
- "When mapping against a select() construct, map against "
- "an alias() of the construct instead."
- "This because several databases don't allow a "
- "SELECT from a subquery that does not have an alias."
- )
-
- if self.with_polymorphic and isinstance(
- self.with_polymorphic[1], expression.SelectBase
- ):
+ if self.with_polymorphic and self.with_polymorphic[1] is not None:
self.with_polymorphic = (
self.with_polymorphic[0],
- self.with_polymorphic[1].alias(),
+ coercions.expect(
+ roles.StrictFromClauseRole,
+ self.with_polymorphic[1],
+ allow_select=True,
+ ),
)
if self.configured:
@@ -2274,6 +2266,11 @@ class Mapper(InspectionAttr):
def _with_polymorphic_args(
self, spec=None, selectable=False, innerjoin=False
):
+ if selectable not in (None, False):
+ selectable = coercions.expect(
+ roles.StrictFromClauseRole, selectable, allow_select=True
+ )
+
if self.with_polymorphic:
if not spec:
spec = self.with_polymorphic[0]
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 8ef1663a1..01a96d7b5 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -259,8 +259,9 @@ class Query(object):
"aliased(), or FromClause instance."
)
else:
- if isinstance(from_obj, expression.SelectBase):
- from_obj = from_obj.alias()
+ from_obj = coercions.expect(
+ roles.StrictFromClauseRole, from_obj, allow_select=True
+ )
if set_base_alias:
select_from_alias = from_obj
fa.append(from_obj)
@@ -1401,7 +1402,9 @@ class Query(object):
fromclause = (
self.with_labels()
.enable_eagerloads(False)
- .statement.correlate(None)
+ .correlate(None)
+ .subquery()
+ ._anonymous_fromclause()
)
q = self._from_selectable(fromclause)
q._enable_single_crit = False
@@ -1897,7 +1900,7 @@ class Query(object):
def _set_op(self, expr_fn, *q):
return self._from_selectable(
- expr_fn(*([self] + list(q)))
+ expr_fn(*([self] + list(q))).subquery()
)._set_enable_single_crit(False)
def union(self, *q):
@@ -2918,7 +2921,7 @@ class Query(object):
used at the start of the query to adapt the existing ``User`` entity::
q = session.query(User).\
- select_entity_from(select_stmt).\
+ select_entity_from(select_stmt.subquery()).\
filter(User.name == 'ed')
Above, the generated SQL will show that the ``User`` entity is
@@ -2955,7 +2958,7 @@ class Query(object):
:meth:`.TextClause.columns` method::
text_stmt = text("select id, name from user").columns(
- User.id, User.name)
+ User.id, User.name).subquery()
q = session.query(User).select_entity_from(text_stmt)
:meth:`.Query.select_entity_from` itself accepts an :func:`.aliased`
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 0d7ce8bbf..6b3107f59 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -1105,7 +1105,7 @@ class RelationshipProperty(StrategizedProperty):
info.is_aliased_class,
)
if self.property._is_self_referential and not is_aliased_class:
- to_selectable = to_selectable.alias()
+ to_selectable = to_selectable._anonymous_fromclause()
single_crit = target_mapper._single_table_criterion
if single_crit is not None:
@@ -1502,9 +1502,9 @@ class RelationshipProperty(StrategizedProperty):
)
if self.secondary is not None and alias_secondary:
- criterion = ClauseAdapter(self.secondary.alias()).traverse(
- criterion
- )
+ criterion = ClauseAdapter(
+ self.secondary._anonymous_fromclause()
+ ).traverse(criterion)
criterion = visitors.cloned_traverse(
criterion, {}, {"bindparam": visit_bindparam}
@@ -2173,7 +2173,7 @@ class RelationshipProperty(StrategizedProperty):
aliased = True
if self._is_self_referential and source_selectable is None:
- dest_selectable = dest_selectable.alias()
+ dest_selectable = dest_selectable._anonymous_fromclause()
aliased = True
else:
aliased = True
@@ -3143,7 +3143,7 @@ class JoinCondition(object):
if aliased:
if secondary is not None:
- secondary = secondary.alias(flat=True)
+ secondary = secondary._anonymous_fromclause(flat=True)
primary_aliasizer = ClauseAdapter(secondary)
secondary_aliasizer = ClauseAdapter(
dest_selectable, equivalents=self.child_equivalents
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index e57418106..c7e338848 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -29,7 +29,9 @@ from .. import exc as sa_exc
from .. import inspection
from .. import sql
from .. import util
+from ..sql import coercions
from ..sql import expression
+from ..sql import roles
from ..sql import util as sql_util
@@ -204,11 +206,10 @@ def polymorphic_union(
for key in table_map:
table = table_map[key]
- # mysql doesn't like selecting from a select;
- # make it an alias of the select
- if isinstance(table, sql.Select):
- table = table.alias()
- table_map[key] = table
+ table = coercions.expect(
+ roles.StrictFromClauseRole, table, allow_select=True
+ )
+ table_map[key] = table
m = {}
for c in table.c:
@@ -466,7 +467,7 @@ class AliasedClass(object):
):
mapper = _class_to_mapper(cls)
if alias is None:
- alias = mapper._with_polymorphic_selectable.alias(
+ alias = mapper._with_polymorphic_selectable._anonymous_fromclause(
name=name, flat=flat
)
@@ -829,7 +830,7 @@ def aliased(element, alias=None, name=None, flat=False, adapt_on_names=False):
raise sa_exc.ArgumentError(
"adapt_on_names only applies to ORM elements"
)
- return element.alias(name, flat=flat)
+ return element._anonymous_fromclause(name=name, flat=flat)
else:
return AliasedClass(
element,
@@ -896,7 +897,7 @@ def with_polymorphic(
.. seealso:: :meth:`.Join.alias`
- :param selectable: a table or select() statement that will
+ :param selectable: a table or subquery that will
be used in place of the generated FROM clause. This argument is
required if any of the desired classes use concrete table
inheritance, since SQLAlchemy currently cannot generate UNIONs
@@ -930,7 +931,7 @@ def with_polymorphic(
classes, selectable, innerjoin=innerjoin
)
if aliased or flat:
- selectable = selectable.alias(flat=flat)
+ selectable = selectable._anonymous_fromclause(flat=flat)
return AliasedClass(
base,
selectable,
diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py
index 7c7222f9f..d4551eb60 100644
--- a/lib/sqlalchemy/sql/coercions.py
+++ b/lib/sqlalchemy/sql/coercions.py
@@ -199,7 +199,7 @@ def _no_text_coercion(
class _NoTextCoercion(object):
- def _literal_coercion(self, element, argname=None):
+ def _literal_coercion(self, element, argname=None, **kw):
if isinstance(element, util.string_types) and issubclass(
elements.TextClause, self._role_class
):
@@ -216,7 +216,7 @@ class _CoerceLiterals(object):
def _text_coercion(self, element, argname=None):
return _no_text_coercion(element, argname)
- def _literal_coercion(self, element, argname=None):
+ def _literal_coercion(self, element, argname=None, **kw):
if isinstance(element, util.string_types):
if self._coerce_star and element == "*":
return elements.ColumnClause("*", is_literal=True)
@@ -240,7 +240,9 @@ class _CoerceLiterals(object):
class ExpressionElementImpl(
_ColumnCoercions, RoleImpl, roles.ExpressionElementRole
):
- def _literal_coercion(self, element, name=None, type_=None, argname=None):
+ def _literal_coercion(
+ self, element, name=None, type_=None, argname=None, **kw
+ ):
if element is None:
return elements.Null()
else:
@@ -256,7 +258,7 @@ class BinaryElementImpl(
ExpressionElementImpl, RoleImpl, roles.BinaryElementRole
):
def _literal_coercion(
- self, element, expr, operator, bindparam_type=None, argname=None
+ self, element, expr, operator, bindparam_type=None, argname=None, **kw
):
try:
return expr._bind_param(operator, element, type_=bindparam_type)
@@ -393,7 +395,7 @@ class DMLColumnImpl(_ReturnsStringKey, RoleImpl, roles.DMLColumnRole):
class ConstExprImpl(RoleImpl, roles.ConstExprRole):
- def _literal_coercion(self, element, argname=None):
+ def _literal_coercion(self, element, argname=None, **kw):
if element is None:
return elements.Null()
elif element is False:
@@ -413,7 +415,7 @@ class TruncatedLabelImpl(_StringOnly, RoleImpl, roles.TruncatedLabelRole):
else:
self._raise_for_expected(original_element, argname)
- def _literal_coercion(self, element, argname=None):
+ def _literal_coercion(self, element, argname=None, **kw):
"""coerce the given value to :class:`._truncated_label`.
Existing :class:`._truncated_label` and
@@ -542,6 +544,34 @@ class FromClauseImpl(_NoTextCoercion, RoleImpl, roles.FromClauseRole):
self._raise_for_expected(original_element, argname)
+class StrictFromClauseImpl(FromClauseImpl, roles.StrictFromClauseRole):
+ def _implicit_coercions(
+ self,
+ original_element,
+ resolved,
+ argname=None,
+ allow_select=False,
+ **kw
+ ):
+ if resolved._is_select_statement and allow_select:
+ util.warn_deprecated(
+ "Implicit coercion of SELECT and textual SELECT constructs "
+ "into FROM clauses is deprecated; please call .subquery() "
+ "on any Core select or ORM Query object in order to produce a "
+ "subquery object."
+ )
+ return resolved.subquery()
+ else:
+ self._raise_for_expected(original_element, argname)
+
+
+class AnonymizedFromClauseImpl(
+ StrictFromClauseImpl, roles.AnonymizedFromClauseRole
+):
+ def _post_coercion(self, element, flat=False, **kw):
+ return element.alias(flat=flat)
+
+
class DMLSelectImpl(_NoTextCoercion, RoleImpl, roles.DMLSelectRole):
def _implicit_coercions(
self, original_element, resolved, argname=None, **kw
diff --git a/lib/sqlalchemy/sql/roles.py b/lib/sqlalchemy/sql/roles.py
index 2d3aaf903..053bd7146 100644
--- a/lib/sqlalchemy/sql/roles.py
+++ b/lib/sqlalchemy/sql/roles.py
@@ -100,6 +100,30 @@ class FromClauseRole(ColumnsClauseRole):
raise NotImplementedError()
+class StrictFromClauseRole(FromClauseRole):
+ # does not allow text() or select() objects
+ pass
+
+
+class AnonymizedFromClauseRole(StrictFromClauseRole):
+ # calls .alias() as a post processor
+
+ def _anonymous_fromclause(self, name=None, flat=False):
+ """A synonym for ``.alias()`` that is only present on objects of this
+ role.
+
+ This is an implicit assurance of the target object being part of the
+ role where anonymous aliasing without any warnings is allowed,
+ as opposed to other kinds of SELECT objects that may or may not have
+ an ``.alias()`` method.
+
+ The method is used by the ORM but is currently semi-private to
+ preserve forwards-compatibility.
+
+ """
+ return self.alias(name=name, flat=flat)
+
+
class CoerceTextStatementRole(SQLRole):
_role_name = "Executable SQL, text() construct, or string statement"
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 41be9fc5a..b0d6002b7 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -662,7 +662,7 @@ class FromClause(roles.FromClauseRole, Selectable):
return None
-class Join(FromClause):
+class Join(roles.AnonymizedFromClauseRole, FromClause):
"""represent a ``JOIN`` construct between two :class:`.FromClause`
elements.
@@ -1178,7 +1178,7 @@ class Join(FromClause):
)
-class Alias(FromClause):
+class Alias(roles.AnonymizedFromClauseRole, FromClause):
"""Represents an table or selectable alias (AS).
Represents an alias, as typically applied to any table or
@@ -1772,7 +1772,7 @@ class FromGrouping(FromClause):
self.element = state["element"]
-class TableClause(Immutable, FromClause):
+class TableClause(roles.AnonymizedFromClauseRole, Immutable, FromClause):
"""Represents a minimal "table" construct.
This is a lightweight table object that has only a name and a
@@ -2116,6 +2116,42 @@ class SelectBase(
def _from_objects(self):
return [self]
+ def subquery(self, name=None):
+ """Return a subquery of this :class:`.SelectBase`.
+
+ A subquery is from a SQL perspective a parentheized, named construct
+ that can be placed in the FROM clause of another SELECT statement.
+
+ Given a SELECT statement such as::
+
+ stmt = select([table.c.id, table.c.name])
+
+ The above statement might look like::
+
+ SELECT table.id, table.name FROM table
+
+ The subquery form by itself renders the same way, however when
+ embedded into the FROM clause of another SELECT statement, it becomes
+ a named sub-element::
+
+ subq = stmt.subquery()
+ new_stmt = select([subq])
+
+ The above renders as::
+
+ SELECT anon_1.id, anon_1.name
+ FROM (SELECT table.id, table.name FROM table) AS anon_1
+
+ Historically, :meth:`.SelectBase.subquery` is equivalent to calling
+ the :meth:`.FromClause.alias` method on a FROM object; however,
+ as a :class:`.SelectBase` object is not directly FROM object,
+ the :meth:`.SelectBase.subquery` method provides clearer semantics.
+
+ .. versionadded:: 1.4
+
+ """
+ return self.alias()
+
class GenerativeSelect(SelectBase):
"""Base class for SELECT statements where additional elements can be