diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-06-26 16:15:19 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-07-08 11:05:11 -0400 |
| commit | 91f376692d472a5bf0c4b4033816250ec1ce3ab6 (patch) | |
| tree | 31f7f72cbe981eb73ed0ba11808d4fb5ae6b7d51 /lib/sqlalchemy | |
| parent | 3dc9a4a2392d033f9d1bd79dd6b6ecea6281a61c (diff) | |
| download | sqlalchemy-91f376692d472a5bf0c4b4033816250ec1ce3ab6.tar.gz | |
Add future=True to create_engine/Session; unify select()
Several weeks of using the future_select() construct
has led to the proposal there be just one select() construct
again which features the new join() method, and otherwise accepts
both the 1.x and 2.x argument styles. This would make
migration simpler and reduce confusion.
However, confusion may be increased by the fact that select().join()
is different Current thinking is we may be better off
with a few hard behavioral changes to old and relatively unknown APIs
rather than trying to play both sides within two extremely similar
but subtly different APIs. At the moment, the .join() thing seems
to be the only behavioral change that occurs without the user
taking any explicit steps. Session.execute() will still
behave the old way as we are adding a future flag.
This change also adds the "future" flag to Session() and
session.execute(), so that interpretation of the incoming statement,
as well as that the new style result is returned, does not
occur for existing applications unless they add the use
of this flag.
The change in general is moving the "removed in 2.0" system
further along where we want the test suite to fully pass
even if the SQLALCHEMY_WARN_20 flag is set.
Get many tests to pass when SQLALCHEMY_WARN_20 is set; this
should be ongoing after this patch merges.
Improve the RemovedIn20 warning; these are all deprecated
"since" 1.4, so ensure that's what the messages read.
Make sure the inforamtion link is on all warnings.
Add deprecation warnings for parameters present and
add warnings to all FromClause.select() types of methods.
Fixes: #5379
Fixes: #5284
Change-Id: I765a0b912b3dcd0e995426427d8bb7997cbffd51
References: #5159
Diffstat (limited to 'lib/sqlalchemy')
24 files changed, 661 insertions, 421 deletions
diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c index ed6f57470..f99236e1e 100644 --- a/lib/sqlalchemy/cextension/resultproxy.c +++ b/lib/sqlalchemy/cextension/resultproxy.c @@ -505,7 +505,8 @@ BaseRow_getattro(BaseRow *self, PyObject *name) else return tmp; - tmp = BaseRow_subscript_mapping(self, name); + tmp = BaseRow_subscript_impl(self, name, 1); + if (tmp == NULL && PyErr_ExceptionMatches(PyExc_KeyError)) { #if PY_MAJOR_VERSION >= 3 diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 4b211bde7..06ea80b9e 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -2612,7 +2612,7 @@ class MSDialect(default.DefaultDialect): def has_table(self, connection, tablename, dbname, owner, schema): tables = ischema.tables - s = sql.select([tables.c.table_name]).where( + s = sql.select(tables.c.table_name).where( sql.and_( tables.c.table_type == "BASE TABLE", tables.c.table_name == tablename, @@ -2630,7 +2630,7 @@ class MSDialect(default.DefaultDialect): def has_sequence(self, connection, sequencename, dbname, owner, schema): sequences = ischema.sequences - s = sql.select([sequences.c.sequence_name]).where( + s = sql.select(sequences.c.sequence_name).where( sequences.c.sequence_name == sequencename ) @@ -2646,7 +2646,7 @@ class MSDialect(default.DefaultDialect): def get_sequence_names(self, connection, dbname, owner, schema, **kw): sequences = ischema.sequences - s = sql.select([sequences.c.sequence_name]) + s = sql.select(sequences.c.sequence_name) if owner: s = s.where(sequences.c.sequence_schema == owner) @@ -2668,7 +2668,7 @@ class MSDialect(default.DefaultDialect): def get_table_names(self, connection, dbname, owner, schema, **kw): tables = ischema.tables s = ( - sql.select([tables.c.table_name]) + sql.select(tables.c.table_name) .where( sql.and_( tables.c.table_schema == owner, @@ -2684,12 +2684,15 @@ class MSDialect(default.DefaultDialect): @_db_plus_owner_listing def get_view_names(self, connection, dbname, owner, schema, **kw): tables = ischema.tables - s = sql.select( - [tables.c.table_name], - sql.and_( - tables.c.table_schema == owner, tables.c.table_type == "VIEW" - ), - order_by=[tables.c.table_name], + s = ( + sql.select(tables.c.table_name) + .where( + sql.and_( + tables.c.table_schema == owner, + tables.c.table_type == "VIEW", + ) + ) + .order_by(tables.c.table_name) ) view_names = [r[0] for r in connection.execute(s)] return view_names @@ -2807,11 +2810,13 @@ class MSDialect(default.DefaultDialect): computed_cols.c.definition, NVARCHAR(4000) ) - s = sql.select( - [columns, computed_definition, computed_cols.c.is_persisted], - whereclause, - from_obj=join, - order_by=[columns.c.ordinal_position], + s = ( + sql.select( + columns, computed_definition, computed_cols.c.is_persisted + ) + .where(whereclause) + .select_from(join) + .order_by(columns.c.ordinal_position) ) c = connection.execution_options(future_result=True).execute(s) @@ -2930,7 +2935,8 @@ class MSDialect(default.DefaultDialect): # Primary key constraints s = sql.select( - [C.c.column_name, TC.c.constraint_type, C.c.constraint_name], + C.c.column_name, TC.c.constraint_type, C.c.constraint_name + ).where( sql.and_( TC.c.constraint_name == C.c.constraint_name, TC.c.table_schema == C.c.table_schema, @@ -2957,8 +2963,8 @@ class MSDialect(default.DefaultDialect): R = ischema.key_constraints.alias("R") # Foreign key constraints - s = sql.select( - [ + s = ( + sql.select( C.c.column_name, R.c.table_schema, R.c.table_name, @@ -2967,17 +2973,19 @@ class MSDialect(default.DefaultDialect): RR.c.match_option, RR.c.update_rule, RR.c.delete_rule, - ], - sql.and_( - C.c.table_name == tablename, - C.c.table_schema == owner, - RR.c.constraint_schema == C.c.table_schema, - C.c.constraint_name == RR.c.constraint_name, - R.c.constraint_name == RR.c.unique_constraint_name, - R.c.constraint_schema == RR.c.unique_constraint_schema, - C.c.ordinal_position == R.c.ordinal_position, - ), - order_by=[RR.c.constraint_name, R.c.ordinal_position], + ) + .where( + sql.and_( + C.c.table_name == tablename, + C.c.table_schema == owner, + RR.c.constraint_schema == C.c.table_schema, + C.c.constraint_name == RR.c.constraint_name, + R.c.constraint_name == RR.c.unique_constraint_name, + R.c.constraint_schema == RR.c.unique_constraint_schema, + C.c.ordinal_position == R.c.ordinal_position, + ) + ) + .order_by(RR.c.constraint_name, R.c.ordinal_position) ) # group rows by constraint ID, to handle multi-column FKs diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 9ac61fe12..6bc9588ad 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1511,7 +1511,6 @@ class Connection(Connectable): # legacy stuff. if should_close_with_result and context._soft_closed: assert not self._is_future - assert not context._is_future_result # CursorResult already exhausted rows / has no rows. # close us now diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index c199c21e0..8b0377a58 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -227,6 +227,15 @@ def create_engine(url, **kwargs): be applied to all connections. See :meth:`~sqlalchemy.engine.Connection.execution_options` + :param future: Use the 2.0 style :class:`_future.Engine` and + :class:`_future.Connection` API. + + ..versionadded:: 1.4 + + .. seealso:: + + :ref:`migration_20_toplevel` + :param hide_parameters: Boolean, when set to True, SQL statement parameters will not be displayed in INFO logging nor will they be formatted into the string representation of :class:`.StatementError` objects. @@ -575,7 +584,14 @@ def create_engine(url, **kwargs): pool._dialect = dialect # create engine. - engineclass = kwargs.pop("_future_engine_class", base.Engine) + if kwargs.pop("future", False): + from sqlalchemy import future + + default_engine_class = future.Engine + else: + default_engine_class = base.Engine + + engineclass = kwargs.pop("_future_engine_class", default_engine_class) engine_args = {} for k in util.get_cls_kwargs(engineclass): diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index f1fc505ac..e567e11e7 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -393,9 +393,7 @@ class DefaultDialect(interfaces.Dialect): parameters = {} def check_unicode(test): - statement = cast_to( - expression.select([test]).compile(dialect=self) - ) + statement = cast_to(expression.select(test).compile(dialect=self)) try: cursor = connection.connection.cursor() connection._cursor_execute(cursor, statement, parameters) @@ -453,7 +451,7 @@ class DefaultDialect(interfaces.Dialect): cursor.execute( cast_to( expression.select( - [expression.literal_column("'x'").label("some_label")] + expression.literal_column("'x'").label("some_label") ).compile(dialect=self) ) ) diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index ecbf871e2..fc6623609 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -405,7 +405,7 @@ class Result(object): ) result = self.session.execute( - statement, params, execution_options=execution_options + statement, params, execution_options=execution_options, future=True ) if result._attributes.get("is_single_entity", False): result = result.scalars() diff --git a/lib/sqlalchemy/future/__init__.py b/lib/sqlalchemy/future/__init__.py index 6a3581599..37ce46e47 100644 --- a/lib/sqlalchemy/future/__init__.py +++ b/lib/sqlalchemy/future/__init__.py @@ -11,7 +11,7 @@ from .engine import Connection # noqa from .engine import create_engine # noqa from .engine import Engine # noqa -from .selectable import Select # noqa +from ..sql.selectable import Select # noqa from ..util.langhelpers import public_factory select = public_factory(Select._create_future_select, ".future.select") diff --git a/lib/sqlalchemy/future/selectable.py b/lib/sqlalchemy/future/selectable.py deleted file mode 100644 index 9d0ae7c89..000000000 --- a/lib/sqlalchemy/future/selectable.py +++ /dev/null @@ -1,165 +0,0 @@ -from ..sql import coercions -from ..sql import roles -from ..sql.base import _generative -from ..sql.selectable import GenerativeSelect -from ..sql.selectable import Select as _LegacySelect -from ..sql.selectable import SelectState -from ..sql.util import _entity_namespace_key - - -class Select(_LegacySelect): - _is_future = True - _setup_joins = () - _legacy_setup_joins = () - inherit_cache = True - - @classmethod - def _create_select(cls, *entities): - raise NotImplementedError("use _create_future_select") - - @classmethod - def _create_future_select(cls, *entities): - r"""Construct a new :class:`_expression.Select` using the 2. - x style API. - - .. versionadded:: 2.0 - the :func:`_future.select` construct is - the same construct as the one returned by - :func:`_expression.select`, except that the function only - accepts the "columns clause" entities up front; the rest of the - state of the SELECT should be built up using generative methods. - - Similar functionality is also available via the - :meth:`_expression.FromClause.select` method on any - :class:`_expression.FromClause`. - - .. seealso:: - - :ref:`coretutorial_selecting` - Core Tutorial description of - :func:`_expression.select`. - - :param \*entities: - Entities to SELECT from. For Core usage, this is typically a series - of :class:`_expression.ColumnElement` and / or - :class:`_expression.FromClause` - objects which will form the columns clause of the resulting - statement. For those objects that are instances of - :class:`_expression.FromClause` (typically :class:`_schema.Table` - or :class:`_expression.Alias` - objects), the :attr:`_expression.FromClause.c` - collection is extracted - to form a collection of :class:`_expression.ColumnElement` objects. - - This parameter will also accept :class:`_expression.TextClause` - constructs as - given, as well as ORM-mapped classes. - - """ - - self = cls.__new__(cls) - self._raw_columns = [ - coercions.expect( - roles.ColumnsClauseRole, ent, apply_propagate_attrs=self - ) - for ent in entities - ] - - GenerativeSelect.__init__(self) - - return self - - def filter(self, *criteria): - """A synonym for the :meth:`_future.Select.where` method.""" - - return self.where(*criteria) - - def _exported_columns_iterator(self): - meth = SelectState.get_plugin_class(self).exported_columns_iterator - return meth(self) - - def _filter_by_zero(self): - if self._setup_joins: - meth = SelectState.get_plugin_class( - self - ).determine_last_joined_entity - _last_joined_entity = meth(self) - if _last_joined_entity is not None: - return _last_joined_entity - - if self._from_obj: - return self._from_obj[0] - - return self._raw_columns[0] - - def filter_by(self, **kwargs): - r"""Apply the given filtering criterion as a WHERE clause - to this select. - - """ - from_entity = self._filter_by_zero() - - clauses = [ - _entity_namespace_key(from_entity, key) == value - for key, value in kwargs.items() - ] - return self.filter(*clauses) - - @property - def column_descriptions(self): - """Return a 'column descriptions' structure which may be - plugin-specific. - - """ - meth = SelectState.get_plugin_class(self).get_column_descriptions - return meth(self) - - @_generative - def join(self, target, onclause=None, isouter=False, full=False): - r"""Create a SQL JOIN against this :class:`_expression.Select` - object's criterion - and apply generatively, returning the newly resulting - :class:`_expression.Select`. - - - """ - target = coercions.expect( - roles.JoinTargetRole, target, apply_propagate_attrs=self - ) - if onclause is not None: - onclause = coercions.expect(roles.OnClauseRole, onclause) - self._setup_joins += ( - (target, onclause, None, {"isouter": isouter, "full": full}), - ) - - @_generative - def join_from( - self, from_, target, onclause=None, isouter=False, full=False - ): - r"""Create a SQL JOIN against this :class:`_expression.Select` - object's criterion - and apply generatively, returning the newly resulting - :class:`_expression.Select`. - - - """ - # note the order of parsing from vs. target is important here, as we - # are also deriving the source of the plugin (i.e. the subject mapper - # in an ORM query) which should favor the "from_" over the "target" - - from_ = coercions.expect( - roles.FromClauseRole, from_, apply_propagate_attrs=self - ) - target = coercions.expect( - roles.JoinTargetRole, target, apply_propagate_attrs=self - ) - - self._setup_joins += ( - (target, onclause, from_, {"isouter": isouter, "full": full}), - ) - - def outerjoin(self, target, onclause=None, full=False): - """Create a left outer join. - - - - """ - return self.join(target, onclause=onclause, isouter=True, full=full,) diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 09163d4e9..d5f001db1 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -25,6 +25,7 @@ from ..sql import expression from ..sql import roles from ..sql import util as sql_util from ..sql import visitors +from ..sql.base import _entity_namespace_key from ..sql.base import _select_iterables from ..sql.base import CacheableOptions from ..sql.base import CompileState @@ -241,8 +242,6 @@ class ORMCompileState(CompileState): # were passed to session.execute: # session.execute(legacy_select([User.id, User.name])) # see test_query->test_legacy_tuple_old_select - if not statement._is_future: - return result load_options = execution_options.get( "_sa_orm_load_options", QueryContext.default_load_options @@ -399,6 +398,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): compound_eager_adapter = None correlate = None + correlate_except = None _where_criteria = () _having_criteria = () @@ -406,9 +406,6 @@ class ORMSelectCompileState(ORMCompileState, SelectState): def create_for_statement(cls, statement, compiler, **kw): """compiler hook, we arrive here from compiler.visit_select() only.""" - if not statement._is_future: - return SelectState(statement, compiler, **kw) - if compiler is not None: toplevel = not compiler.stack compiler._rewrites_selected_columns = True @@ -592,6 +589,13 @@ class ORMSelectCompileState(ORMCompileState, SelectState): for s in query._correlate ) ) + elif query._correlate_except: + self.correlate_except = tuple( + util.flatten_iterator( + sql_util.surface_selectables(s) if s is not None else None + for s in query._correlate_except + ) + ) elif not query._auto_correlate: self.correlate = (None,) @@ -827,6 +831,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints=self.select_statement._hints, statement_hints=self.select_statement._statement_hints, correlate=self.correlate, + correlate_except=self.correlate_except, **self._select_args ) @@ -902,6 +907,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints=self.select_statement._hints, statement_hints=self.select_statement._statement_hints, correlate=self.correlate, + correlate_except=self.correlate_except, **self._select_args ) @@ -921,6 +927,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints, statement_hints, correlate, + correlate_except, limit_clause, offset_clause, distinct, @@ -972,6 +979,11 @@ class ORMSelectCompileState(ORMCompileState, SelectState): if correlate: statement.correlate.non_generative(statement, *correlate) + if correlate_except: + statement.correlate_except.non_generative( + statement, *correlate_except + ) + return statement def _adapt_polymorphic_element(self, element): @@ -1222,7 +1234,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # string given, e.g. query(Foo).join("bar"). # we look to the left entity or what we last joined # towards - onclause = sql.util._entity_namespace_key( + onclause = _entity_namespace_key( inspect(self._joinpoint_zero()), onclause ) @@ -1243,9 +1255,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): info = inspect(jp0) if getattr(info, "mapper", None) is onclause._parententity: - onclause = sql.util._entity_namespace_key( - info, onclause.key - ) + onclause = _entity_namespace_key(info, onclause.key) # legacy ^^^^^^^^^^^^^^^^^^^^^^^^^^^ if isinstance(onclause, interfaces.PropComparator): diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index abb8ce32d..55c2b79f5 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -431,6 +431,7 @@ def load_on_pk_identity( params=load_options._params, execution_options={"_sa_orm_load_options": load_options}, bind_arguments=bind_arguments, + future=True, ) .unique() .scalars() diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index cbe7bde33..0cc16b96e 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -29,17 +29,17 @@ from .. import future from .. import sql from .. import util from ..engine import result as _result -from ..future import select as future_select from ..sql import coercions from ..sql import expression from ..sql import operators from ..sql import roles +from ..sql import select +from ..sql.base import _entity_namespace_key from ..sql.base import CompileState from ..sql.base import Options from ..sql.dml import DeleteDMLState from ..sql.dml import UpdateDMLState from ..sql.elements import BooleanClauseList -from ..sql.util import _entity_namespace_key def _bulk_insert( @@ -887,7 +887,7 @@ def _emit_update_statements( ) ) - stmt = table.update(clauses) + stmt = table.update().where(clauses) return stmt cached_stmt = base_mapper._memo(("update", table), update_stmt) @@ -1280,7 +1280,7 @@ def _emit_post_update_statements( ) ) - stmt = table.update(clauses) + stmt = table.update().where(clauses) if mapper.version_id_col is not None: stmt = stmt.return_defaults(mapper.version_id_col) @@ -1394,7 +1394,7 @@ def _emit_delete_statements( ) ) - return table.delete(clauses) + return table.delete().where(clauses) statement = base_mapper._memo(("delete", table), delete_stmt) for connection, recs in groupby(delete, lambda rec: rec[1]): # connection @@ -1940,7 +1940,7 @@ class BulkUDCompileState(CompileState): for k, v in iterator: if mapper: if isinstance(k, util.string_types): - desc = sql.util._entity_namespace_key(mapper, k) + desc = _entity_namespace_key(mapper, k) values.extend(desc._bulk_update_tuples(v)) elif "entity_namespace" in k._annotations: k_anno = k._annotations @@ -1989,7 +1989,7 @@ class BulkUDCompileState(CompileState): ): mapper = update_options._subject_mapper - select_stmt = future_select( + select_stmt = select( *(mapper.primary_key + (mapper.select_identity_token,)) ) select_stmt._where_criteria = statement._where_criteria @@ -2007,6 +2007,7 @@ class BulkUDCompileState(CompileState): execution_options, bind_arguments, _add_event=skip_for_full_returning, + future=True, ) matched_rows = result.fetchall() diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 1ca65c733..acc76094b 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -45,12 +45,13 @@ from .. import inspection from .. import log from .. import sql from .. import util -from ..future.selectable import Select as FutureSelect from ..sql import coercions from ..sql import expression from ..sql import roles +from ..sql import Select from ..sql import util as sql_util from ..sql.annotation import SupportsCloneAnnotations +from ..sql.base import _entity_namespace_key from ..sql.base import _generative from ..sql.base import Executable from ..sql.selectable import _SelectFromElements @@ -61,7 +62,6 @@ from ..sql.selectable import HasSuffixes from ..sql.selectable import LABEL_STYLE_NONE from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..sql.selectable import SelectStatementGrouping -from ..sql.util import _entity_namespace_key from ..sql.visitors import InternalTraversal from ..util import collections_abc @@ -419,7 +419,7 @@ class Query( stmt._propagate_attrs = self._propagate_attrs else: # Query / select() internal attributes are 99% cross-compatible - stmt = FutureSelect.__new__(FutureSelect) + stmt = Select.__new__(Select) stmt.__dict__.update(self.__dict__) stmt.__dict__.update( _label_style=self._label_style, @@ -2836,6 +2836,7 @@ class Query( statement, params, execution_options={"_sa_orm_load_options": self.load_options}, + future=True, ) # legacy: automatically set scalars, unique @@ -3209,6 +3210,7 @@ class Query( delete_, self.load_options._params, execution_options={"synchronize_session": synchronize_session}, + future=True, ) bulk_del.result = result self.session.dispatch.after_bulk_delete(bulk_del) @@ -3363,6 +3365,7 @@ class Query( upd, self.load_options._params, execution_options={"synchronize_session": synchronize_session}, + future=True, ) bulk_ud.result = result self.session.dispatch.after_bulk_update(bulk_ud) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index bedc54153..0be15260e 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -1352,12 +1352,18 @@ class RelationshipProperty(StrategizedProperty): crit = j & sql.True_._ifnone(criterion) if secondary is not None: - ex = sql.exists( - [1], crit, from_obj=[dest, secondary] - ).correlate_except(dest, secondary) + ex = ( + sql.exists(1) + .where(crit) + .select_from(dest, secondary) + .correlate_except(dest, secondary) + ) else: - ex = sql.exists([1], crit, from_obj=dest).correlate_except( - dest + ex = ( + sql.exists(1) + .where(crit) + .select_from(dest) + .correlate_except(dest) ) return ex diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index abc990f7b..f4f7374e4 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -118,6 +118,7 @@ class ORMExecuteState(util.MemoizedSlots): "_compile_state_cls", "_starting_event_idx", "_events_todo", + "_future", ) def __init__( @@ -129,6 +130,7 @@ class ORMExecuteState(util.MemoizedSlots): bind_arguments, compile_state_cls, events_todo, + future, ): self.session = session self.statement = statement @@ -137,6 +139,7 @@ class ORMExecuteState(util.MemoizedSlots): self.bind_arguments = bind_arguments self._compile_state_cls = compile_state_cls self._events_todo = list(events_todo) + self._future = future def _remaining_events(self): return self._events_todo[self._starting_event_idx + 1 :] @@ -212,6 +215,7 @@ class ORMExecuteState(util.MemoizedSlots): _execution_options, _bind_arguments, _parent_execute_state=self, + future=self._future, ) @property @@ -924,6 +928,7 @@ class Session(_SessionClassMethods): self, bind=None, autoflush=True, + future=False, expire_on_commit=True, autocommit=False, twophase=False, @@ -1039,6 +1044,26 @@ class Session(_SessionClassMethods): so that all attribute/object access subsequent to a completed transaction will load from the most recent database state. + :param future: if True, use 2.0 style behavior for the + :meth:`_orm.Session.execute` method. This includes that the + :class:`_engine.Result` object returned will return new-style + tuple rows, as well as that Core constructs such as + :class:`_sql.Select`, + :class:`_sql.Update` and :class:`_sql.Delete` will be interpreted + in an ORM context if they are made against ORM entities rather than + plain :class:`.Table` metadata objects. + + The "future" flag is also available on a per-execution basis + using the :paramref:`_orm.Session.execute.future` flag. + + .. versionadded:: 1.4 + + .. seealso:: + + :ref:`migration_20_toplevel` + + :ref:`migration_20_result_rows` + :param info: optional dictionary of arbitrary data to be associated with this :class:`.Session`. Is available via the :attr:`.Session.info` attribute. Note the dictionary is copied at @@ -1071,6 +1096,7 @@ class Session(_SessionClassMethods): self._flushing = False self._warn_on_events = False self._transaction = None + self.future = future self.hash_key = _new_sessionid() self.autoflush = autoflush self.autocommit = autocommit @@ -1387,6 +1413,7 @@ class Session(_SessionClassMethods): params=None, execution_options=util.immutabledict(), bind_arguments=None, + future=False, _parent_execute_state=None, _add_event=None, **kw @@ -1493,6 +1520,14 @@ class Session(_SessionClassMethods): Contents of this dictionary are passed to the :meth:`.Session.get_bind` method. + :param future: + Use future style execution for this statement. This is + the same effect as the :paramref:`_orm.Session.future` flag, + except at the level of this single statement execution. See + that flag for details. + + .. versionadded:: 1.4 + :param mapper: deprecated; use the bind_arguments dictionary @@ -1518,15 +1553,18 @@ class Session(_SessionClassMethods): """ statement = coercions.expect(roles.CoerceTextStatementRole, statement) + future = future or self.future + if not bind_arguments: bind_arguments = kw elif kw: bind_arguments.update(kw) - if ( + if future and ( statement._propagate_attrs.get("compile_state_plugin", None) == "orm" ): + # note that even without "future" mode, we need compile_state_cls = CompileState._get_plugin_class_for_plugin( statement, "orm" ) @@ -1547,7 +1585,7 @@ class Session(_SessionClassMethods): ) else: bind_arguments.setdefault("clause", statement) - if statement._is_future: + if future: execution_options = util.immutabledict().merge_with( execution_options, {"future_result": True} ) @@ -1568,6 +1606,7 @@ class Session(_SessionClassMethods): bind_arguments, compile_state_cls, events_todo, + future, ) for idx, fn in enumerate(events_todo): orm_exec_state._starting_event_idx = idx diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 6cdab8eac..4bc6d8280 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -1388,3 +1388,25 @@ def _bind_or_error(schemaitem, msg=None): ) raise exc.UnboundExecutionError(msg) return bind + + +def _entity_namespace_key(entity, key): + """Return an entry from an entity_namespace. + + + Raises :class:`_exc.InvalidRequestError` rather than attribute error + on not found. + + """ + + ns = entity.entity_namespace + try: + return getattr(ns, key) + except AttributeError as err: + util.raise_( + exc.InvalidRequestError( + 'Entity namespace for "%s" has no property "%s"' + % (entity, key) + ), + replace_context=err, + ) diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 37441a125..d60c63363 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -185,7 +185,7 @@ tablesample = public_factory( lateral = public_factory(Lateral._factory, ".sql.expression.lateral") or_ = public_factory(BooleanClauseList.or_, ".sql.expression.or_") bindparam = public_factory(BindParameter, ".sql.expression.bindparam") -select = public_factory(Select, ".sql.expression.select") +select = public_factory(Select._create, ".sql.expression.select") text = public_factory(TextClause._create_text, ".sql.expression.text") table = public_factory(TableClause, ".sql.expression.table") column = public_factory(ColumnClause, ".sql.expression.column") diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 832da1a57..12fcc00c3 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -24,6 +24,7 @@ from .annotation import SupportsCloneAnnotations from .base import _clone from .base import _cloned_difference from .base import _cloned_intersection +from .base import _entity_namespace_key from .base import _expand_cloned from .base import _from_objects from .base import _generative @@ -83,7 +84,7 @@ def subquery(alias, *args, **kwargs): :func:`_expression.select` function. """ - return Select(*args, **kwargs).subquery(alias) + return Select.create_legacy_select(*args, **kwargs).subquery(alias) class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): @@ -468,8 +469,38 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): _use_schema_map = False - def select(self, whereclause=None, **params): - """Return a SELECT of this :class:`_expression.FromClause`. + @util.deprecated_params( + whereclause=( + "2.0", + "The :paramref:`_sql.FromClause.select().whereclause` parameter " + "is deprecated and will be removed in version 2.0. " + "Please make use of " + "the :meth:`.Select.where` " + "method to add WHERE criteria to the SELECT statement.", + ), + kwargs=( + "2.0", + "The :meth:`_sql.FromClause.select` method will no longer accept " + "keyword arguments in version 2.0. Please use generative methods " + "from the " + ":class:`_sql.Select` construct in order to apply additional " + "modifications.", + ), + ) + def select(self, whereclause=None, **kwargs): + r"""Return a SELECT of this :class:`_expression.FromClause`. + + + e.g.:: + + stmt = some_table.select().where(some_table.c.id == 5) + + :param whereclause: a WHERE clause, equivalent to calling the + :meth:`_sql.Select.where` method. + + :param \**kwargs: additional keyword arguments are passed to the + legacy constructor for :class:`_sql.Select` described at + :meth:`_sql.Select.create_legacy_select`. .. seealso:: @@ -477,8 +508,9 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): method which allows for arbitrary column lists. """ - - return Select([self], whereclause, **params) + if whereclause is not None: + kwargs["whereclause"] = whereclause + return Select._create_select_from_fromclause(self, [self], **kwargs) def join(self, right, onclause=None, isouter=False, full=False): """Return a :class:`_expression.Join` from this @@ -1138,24 +1170,45 @@ class Join(roles.DMLTableRole, FromClause): "join explicitly." % (a.description, b.description) ) + @util.deprecated_params( + whereclause=( + "2.0", + "The :paramref:`_sql.Join.select().whereclause` parameter " + "is deprecated and will be removed in version 2.0. " + "Please make use of " + "the :meth:`.Select.where` " + "method to add WHERE criteria to the SELECT statement.", + ), + kwargs=( + "2.0", + "The :meth:`_sql.Join.select` method will no longer accept " + "keyword arguments in version 2.0. Please use generative " + "methods from the " + ":class:`_sql.Select` construct in order to apply additional " + "modifications.", + ), + ) def select(self, whereclause=None, **kwargs): r"""Create a :class:`_expression.Select` from this :class:`_expression.Join`. - The equivalent long-hand form, given a :class:`_expression.Join` - object - ``j``, is:: + E.g.:: + + stmt = table_a.join(table_b, table_a.c.id == table_b.c.a_id) - from sqlalchemy import select - j = select([j.left, j.right], **kw).\ - where(whereclause).\ - select_from(j) + stmt = stmt.select() - :param whereclause: the WHERE criterion that will be sent to - the :func:`select()` function + The above will produce a SQL string resembling:: - :param \**kwargs: all other kwargs are sent to the - underlying :func:`select()` function. + SELECT table_a.id, table_a.col, table_b.id, table_b.a_id + FROM table_a JOIN table_b ON table_a.id = table_b.a_id + + :param whereclause: WHERE criteria, same as calling + :meth:`_sql.Select.where` on the resulting statement + + :param \**kwargs: additional keyword arguments are passed to the + legacy constructor for :class:`_sql.Select` described at + :meth:`_sql.Select.create_legacy_select`. """ collist = [self.left, self.right] @@ -2444,30 +2497,6 @@ class SelectBase( def select(self, *arg, **kw): return self._implicit_subquery.select(*arg, **kw) - @util.deprecated( - "1.4", - "The :meth:`_expression.SelectBase.join` method is deprecated " - "and will be removed in a future release; this method implicitly " - "creates a subquery that should be explicit. " - "Please call :meth:`_expression.SelectBase.subquery` " - "first in order to create " - "a subquery, which then can be selected.", - ) - def join(self, *arg, **kw): - return self._implicit_subquery.join(*arg, **kw) - - @util.deprecated( - "1.4", - "The :meth:`_expression.SelectBase.outerjoin` method is deprecated " - "and will be removed in a future release; this method implicitly " - "creates a subquery that should be explicit. " - "Please call :meth:`_expression.SelectBase.subquery` " - "first in order to create " - "a subquery, which then can be selected.", - ) - def outerjoin(self, *arg, **kw): - return self._implicit_subquery.outerjoin(*arg, **kw) - @HasMemoized.memoized_attribute def _implicit_subquery(self): return self.subquery() @@ -3103,6 +3132,16 @@ class CompoundSelect(HasCompileState, GenerativeSelect): for s in selects ] + if kwargs and util.SQLALCHEMY_WARN_20: + util.warn_deprecated_20( + "Set functions such as union(), union_all(), extract(), etc. " + "in SQLAlchemy 2.0 will accept a " + "series of SELECT statements only. " + "Please use generative methods such as order_by() for " + "additional modifications to this CompoundSelect.", + stacklevel=4, + ) + GenerativeSelect.__init__(self, **kwargs) @classmethod @@ -3770,7 +3809,6 @@ class Select( __visit_name__ = "select" - _is_future = False _setup_joins = () _legacy_setup_joins = () @@ -3817,38 +3855,21 @@ class Select( ] @classmethod - def _create_select(cls, *entities): - r"""Construct an old style :class:`_expression.Select` using the - the 2.x style constructor. - - """ - - self = cls.__new__(cls) - self._raw_columns = [ - coercions.expect(roles.ColumnsClauseRole, ent) for ent in entities - ] - - GenerativeSelect.__init__(self) - - return self - - @classmethod def _create_select_from_fromclause(cls, target, entities, *arg, **kw): if arg or kw: - if util.SQLALCHEMY_WARN_20: - util.warn_deprecated_20( - "Passing arguments to %s.select() is deprecated and " - "will be removed in SQLAlchemy 2.0. " - "Please use generative " - "methods such as select().where(), etc." - % (target.__class__.__name__,) - ) - return Select(entities, *arg, **kw) + return Select.create_legacy_select(entities, *arg, **kw) else: return Select._create_select(*entities) - def __init__( - self, + @classmethod + @util.deprecated( + "2.0", + "The legacy calling style of :func:`_sql.select` is deprecated and " + "will be removed in SQLAlchemy 2.0. Please use the new calling " + "style described at :func:`_sql.select`.", + ) + def create_legacy_select( + cls, columns=None, whereclause=None, from_obj=None, @@ -3859,18 +3880,25 @@ class Select( suffixes=None, **kwargs ): - """Construct a new :class:`_expression.Select` using the 1.x style - API. + """Construct a new :class:`_expression.Select` using the 1.x style API. + + This method is called implicitly when the :func:`_expression.select` + construct is used and the first argument is a Python list or other + plain sequence object, which is taken to refer to the columns + collection. + + .. versionchanged:: 1.4 Added the :meth:`.Select.create_legacy_select` + constructor which documents the calling style in use when the + :func:`.select` construct is invoked using 1.x-style arguments. Similar functionality is also available via the :meth:`_expression.FromClause.select` method on any :class:`_expression.FromClause`. - All arguments which accept :class:`_expression.ClauseElement` - arguments also - accept string arguments, which will be converted as appropriate into - either :func:`_expression.text` or - :func:`_expression.literal_column` constructs. + All arguments which accept :class:`_expression.ClauseElement` arguments + also accept string arguments, which will be converted as appropriate + into either :func:`_expression.text()` or + :func:`_expression.literal_column()` constructs. .. seealso:: @@ -4054,14 +4082,7 @@ class Select( :meth:`_expression.Select.apply_labels` """ - if util.SQLALCHEMY_WARN_20: - util.warn_deprecated_20( - "The select() function in SQLAlchemy 2.0 will accept a " - "series of columns / tables and other entities only, " - "passed positionally. For forwards compatibility, use the " - "sqlalchemy.future.select() construct.", - stacklevel=4, - ) + self = cls.__new__(cls) self._auto_correlate = correlate @@ -4079,8 +4100,10 @@ class Select( except TypeError as err: util.raise_( exc.ArgumentError( - "columns argument to select() must " - "be a Python list or other iterable" + "select() construct created in legacy mode, i.e. with " + "keyword arguments, must provide the columns argument as " + "a Python list or other iterable.", + code="c9ae", ), from_=err, ) @@ -4108,12 +4131,247 @@ class Select( self._setup_suffixes(suffixes) GenerativeSelect.__init__(self, **kwargs) + return self + + @classmethod + def _create_future_select(cls, *entities): + r"""Construct a new :class:`_expression.Select` using the 2. + x style API. + + .. versionadded:: 1.4 - The :func:`_sql.select` function now accepts + column arguments positionally. The top-level :func:`_sql.select` + function will automatically use the 1.x or 2.x style API based on + the incoming argumnents; using :func:`_future.select` from the + ``sqlalchemy.future`` module will enforce that only the 2.x style + constructor is used. + + Similar functionality is also available via the + :meth:`_expression.FromClause.select` method on any + :class:`_expression.FromClause`. + + .. seealso:: + + :ref:`coretutorial_selecting` - Core Tutorial description of + :func:`_expression.select`. + + :param \*entities: + Entities to SELECT from. For Core usage, this is typically a series + of :class:`_expression.ColumnElement` and / or + :class:`_expression.FromClause` + objects which will form the columns clause of the resulting + statement. For those objects that are instances of + :class:`_expression.FromClause` (typically :class:`_schema.Table` + or :class:`_expression.Alias` + objects), the :attr:`_expression.FromClause.c` + collection is extracted + to form a collection of :class:`_expression.ColumnElement` objects. + + This parameter will also accept :class:`_expression.TextClause` + constructs as + given, as well as ORM-mapped classes. + + """ + + self = cls.__new__(cls) + self._raw_columns = [ + coercions.expect( + roles.ColumnsClauseRole, ent, apply_propagate_attrs=self + ) + for ent in entities + ] + + GenerativeSelect.__init__(self) + + return self + + _create_select = _create_future_select + + @classmethod + def _create(cls, *args, **kw): + r"""Create a :class:`.Select` using either the 1.x or 2.0 constructor + style. + + For the legacy calling style, see :meth:`.Select.create_legacy_select`. + If the first argument passed is a Python sequence or if keyword + arguments are present, this style is used. + + .. versionadded:: 2.0 - the :func:`_future.select` construct is + the same construct as the one returned by + :func:`_expression.select`, except that the function only + accepts the "columns clause" entities up front; the rest of the + state of the SELECT should be built up using generative methods. + + Similar functionality is also available via the + :meth:`_expression.FromClause.select` method on any + :class:`_expression.FromClause`. + + .. seealso:: + + :ref:`coretutorial_selecting` - Core Tutorial description of + :func:`_expression.select`. + + :param \*entities: + Entities to SELECT from. For Core usage, this is typically a series + of :class:`_expression.ColumnElement` and / or + :class:`_expression.FromClause` + objects which will form the columns clause of the resulting + statement. For those objects that are instances of + :class:`_expression.FromClause` (typically :class:`_schema.Table` + or :class:`_expression.Alias` + objects), the :attr:`_expression.FromClause.c` + collection is extracted + to form a collection of :class:`_expression.ColumnElement` objects. + + This parameter will also accept :class:`_expression.TextClause` + constructs as given, as well as ORM-mapped classes. + + """ + if (args and isinstance(args[0], list)) or kw: + return cls.create_legacy_select(*args, **kw) + else: + return cls._create_future_select(*args) + + def __init__(self,): + raise NotImplementedError() def _scalar_type(self): elem = self._raw_columns[0] cols = list(elem._select_iterable) return cols[0].type + def filter(self, *criteria): + """A synonym for the :meth:`_future.Select.where` method.""" + + return self.where(*criteria) + + def _filter_by_zero(self): + if self._setup_joins: + meth = SelectState.get_plugin_class( + self + ).determine_last_joined_entity + _last_joined_entity = meth(self) + if _last_joined_entity is not None: + return _last_joined_entity + + if self._from_obj: + return self._from_obj[0] + + return self._raw_columns[0] + + def filter_by(self, **kwargs): + r"""apply the given filtering criterion as a WHERE clause + to this select. + + """ + from_entity = self._filter_by_zero() + + clauses = [ + _entity_namespace_key(from_entity, key) == value + for key, value in kwargs.items() + ] + return self.filter(*clauses) + + @property + def column_descriptions(self): + """Return a 'column descriptions' structure which may be + plugin-specific. + + """ + meth = SelectState.get_plugin_class(self).get_column_descriptions + return meth(self) + + @_generative + def join(self, target, onclause=None, isouter=False, full=False): + r"""Create a SQL JOIN against this :class:`_expresson.Select` + object's criterion + and apply generatively, returning the newly resulting + :class:`_expression.Select`. + + .. versionchanged:: 1.4 :meth:`_expression.Select.join` now modifies + the FROM list of the :class:`.Select` object in place, rather than + implicitly producing a subquery. + + :param target: target table to join towards + + :param onclause: ON clause of the join. + + :param isouter: if True, generate LEFT OUTER join. Same as + :meth:`_expression.Select.outerjoin`. + + :param full: if True, generate FULL OUTER join. + + .. seealso:: + + :meth:`_expression.Select.join_from` + + """ + target = coercions.expect( + roles.JoinTargetRole, target, apply_propagate_attrs=self + ) + if onclause is not None: + onclause = coercions.expect(roles.OnClauseRole, onclause) + self._setup_joins += ( + (target, onclause, None, {"isouter": isouter, "full": full}), + ) + + @_generative + def join_from( + self, from_, target, onclause=None, isouter=False, full=False + ): + r"""Create a SQL JOIN against this :class:`_expresson.Select` + object's criterion + and apply generatively, returning the newly resulting + :class:`_expression.Select`. + + .. versionadded:: 1.4 + + :param from\_: the left side of the join, will be rendered in the + FROM clause and is roughly equivalent to using the + :meth:`.Select.select_from` method. + + :param target: target table to join towards + + :param onclause: ON clause of the join. + + :param isouter: if True, generate LEFT OUTER join. Same as + :meth:`_expression.Select.outerjoin`. + + :param full: if True, generate FULL OUTER join. + + .. seealso:: + + :meth:`_expression.Select.join` + + """ + # note the order of parsing from vs. target is important here, as we + # are also deriving the source of the plugin (i.e. the subject mapper + # in an ORM query) which should favor the "from_" over the "target" + + from_ = coercions.expect( + roles.FromClauseRole, from_, apply_propagate_attrs=self + ) + target = coercions.expect( + roles.JoinTargetRole, target, apply_propagate_attrs=self + ) + if onclause is not None: + onclause = coercions.expect(roles.OnClauseRole, onclause) + + self._setup_joins += ( + (target, onclause, from_, {"isouter": isouter, "full": full}), + ) + + def outerjoin(self, target, onclause=None, full=False): + """Create a left outer join. + + Parameters are the same as that of :meth:`_expression.Select.join`. + + .. versionchanged:: 1.4 :meth:`_expression.Select.outerjoin` now + modifies the FROM list of the :class:`.Select` object in place, + rather than implicitly producing a subquery. + + """ + return self.join(target, onclause=onclause, isouter=True, full=full,) + @property def froms(self): """Return the displayed list of :class:`_expression.FromClause` @@ -4642,8 +4900,12 @@ class Select( return ColumnCollection(collection).as_immutable() + # def _exported_columns_iterator(self): + # return _select_iterables(self._raw_columns) + def _exported_columns_iterator(self): - return _select_iterables(self._raw_columns) + meth = SelectState.get_plugin_class(self).exported_columns_iterator + return meth(self) def _ensure_disambiguated_names(self): if self._label_style is LABEL_STYLE_NONE: @@ -4922,37 +5184,30 @@ class Exists(UnaryExpression): inherit_cache = True def __init__(self, *args, **kwargs): - """Construct a new :class:`_expression.Exists` against an existing - :class:`_expression.Select` object. + """Construct a new :class:`_expression.Exists` construct. - Calling styles are of the following forms:: + The modern form of :func:`.exists` is to invoke with no arguments, + which will produce an ``"EXISTS *"`` construct. A WHERE clause + is then added using the :meth:`.Exists.where` method:: - # use on an existing select() - s = select([table.c.col1]).where(table.c.col2==5) - s_e = exists(s) + exists_criteria = exists().where(table1.c.col1 == table2.c.col2) - # an exists is usually used in a where of another select - # to produce a WHERE EXISTS (SELECT ... ) - select([table.c.col1]).where(s_e) + The EXISTS criteria is then used inside of an enclosing SELECT:: - # but can also be used in a select to produce a - # SELECT EXISTS (SELECT ... ) query - select([s_e]) + stmt = select(table1.c.col1).where(exists_criteria) - # construct a select() at once - exists(['*'], **select_arguments).where(criterion) + The above statement will then be of the form:: - # columns argument is optional, generates "EXISTS (SELECT *)" - # by default. - exists().where(table.c.col2==5) + SELECT col1 FROM table1 WHERE EXISTS + (SELECT * FROM table2 WHERE table2.col2 = table1.col1) """ if args and isinstance(args[0], (SelectBase, ScalarSelect)): s = args[0] else: if not args: - args = ([literal_column("*")],) - s = Select(*args, **kwargs).scalar_subquery() + args = (literal_column("*"),) + s = Select._create(*args, **kwargs).scalar_subquery() UnaryExpression.__init__( self, @@ -4967,10 +5222,52 @@ class Exists(UnaryExpression): element = fn(element) return element.self_group(against=operators.exists) - def select(self, whereclause=None, **params): + @util.deprecated_params( + whereclause=( + "2.0", + "The :paramref:`_sql.Exists.select().whereclause` parameter " + "is deprecated and will be removed in version 2.0. " + "Please make use " + "of the :meth:`.Select.where` " + "method to add WHERE criteria to the SELECT statement.", + ), + kwargs=( + "2.0", + "The :meth:`_sql.Exists.select` method will no longer accept " + "keyword arguments in version 2.0. " + "Please use generative methods from the " + ":class:`_sql.Select` construct in order to apply additional " + "modifications.", + ), + ) + def select(self, whereclause=None, **kwargs): + r"""Return a SELECT of this :class:`_expression.Exists`. + + e.g.:: + + stmt = exists(some_table.c.id).where(some_table.c.id == 5).select() + + This will produce a statement resembling:: + + SELECT EXISTS (SELECT id FROM some_table WHERE some_table = :param) AS anon_1 + + :param whereclause: a WHERE clause, equivalent to calling the + :meth:`_sql.Select.where` method. + + :param **kwargs: additional keyword arguments are passed to the + legacy constructor for :class:`_sql.Select` described at + :meth:`_sql.Select.create_legacy_select`. + + .. seealso:: + + :func:`_expression.select` - general purpose + method which allows for arbitrary column lists. + + """ # noqa + if whereclause is not None: - params["whereclause"] = whereclause - return Select._create_select_from_fromclause(self, [self], **params) + kwargs["whereclause"] = whereclause + return Select._create_select_from_fromclause(self, [self], **kwargs) def correlate(self, *fromclause): e = self._clone() @@ -4986,7 +5283,7 @@ class Exists(UnaryExpression): ) return e - def select_from(self, clause): + def select_from(self, *froms): """Return a new :class:`_expression.Exists` construct, applying the given expression to the :meth:`_expression.Select.select_from` @@ -4995,7 +5292,7 @@ class Exists(UnaryExpression): """ e = self._clone() - e.element = self._regroup(lambda element: element.select_from(clause)) + e.element = self._regroup(lambda element: element.select_from(*froms)) return e def where(self, clause): diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index e8726000b..f4aa878ab 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -959,25 +959,3 @@ class ColumnAdapter(ClauseAdapter): def __setstate__(self, state): self.__dict__.update(state) self.columns = util.WeakPopulateDict(self._locate_col) - - -def _entity_namespace_key(entity, key): - """Return an entry from an entity_namespace. - - - Raises :class:`_exc.InvalidRequestError` rather than attribute error - on not found. - - """ - - ns = entity.entity_namespace - try: - return getattr(ns, key) - except AttributeError as err: - util.raise_( - exc.InvalidRequestError( - 'Entity namespace for "%s" has no property "%s"' - % (entity, key) - ), - replace_context=err, - ) diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 998dde66b..1ce59431e 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -170,7 +170,11 @@ def _expect_warnings( with mock.patch("warnings.warn", our_warn), mock.patch( "sqlalchemy.util.SQLALCHEMY_WARN_20", True - ), mock.patch("sqlalchemy.engine.row.LegacyRow._default_key_style", 2): + ), mock.patch( + "sqlalchemy.util.deprecations.SQLALCHEMY_WARN_20", True + ), mock.patch( + "sqlalchemy.engine.row.LegacyRow._default_key_style", 2 + ): yield if assert_ and (not py2konly or not compat.py3k): diff --git a/lib/sqlalchemy/testing/suite/test_types.py b/lib/sqlalchemy/testing/suite/test_types.py index 00b5fab27..48144f885 100644 --- a/lib/sqlalchemy/testing/suite/test_types.py +++ b/lib/sqlalchemy/testing/suite/test_types.py @@ -114,9 +114,7 @@ class _UnicodeFixture(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(unicode_table.insert(), {"unicode_data": self.data}) - row = connection.execute( - select([unicode_table.c.unicode_data]) - ).first() + row = connection.execute(select(unicode_table.c.unicode_data)).first() eq_(row, (self.data,)) assert isinstance(row[0], util.text_type) @@ -130,7 +128,7 @@ class _UnicodeFixture(_LiteralRoundTripFixture, fixtures.TestBase): ) rows = connection.execute( - select([unicode_table.c.unicode_data]) + select(unicode_table.c.unicode_data) ).fetchall() eq_(rows, [(self.data,) for i in range(3)]) for row in rows: @@ -140,18 +138,14 @@ class _UnicodeFixture(_LiteralRoundTripFixture, fixtures.TestBase): unicode_table = self.tables.unicode_table connection.execute(unicode_table.insert(), {"unicode_data": None}) - row = connection.execute( - select([unicode_table.c.unicode_data]) - ).first() + row = connection.execute(select(unicode_table.c.unicode_data)).first() eq_(row, (None,)) def _test_empty_strings(self, connection): unicode_table = self.tables.unicode_table connection.execute(unicode_table.insert(), {"unicode_data": u("")}) - row = connection.execute( - select([unicode_table.c.unicode_data]) - ).first() + row = connection.execute(select(unicode_table.c.unicode_data)).first() eq_(row, (u(""),)) def test_literal(self): @@ -214,7 +208,7 @@ class TextTest(_LiteralRoundTripFixture, fixtures.TablesTest): text_table = self.tables.text_table connection.execute(text_table.insert(), {"text_data": "some text"}) - row = connection.execute(select([text_table.c.text_data])).first() + row = connection.execute(select(text_table.c.text_data)).first() eq_(row, ("some text",)) @testing.requires.empty_strings_text @@ -222,14 +216,14 @@ class TextTest(_LiteralRoundTripFixture, fixtures.TablesTest): text_table = self.tables.text_table connection.execute(text_table.insert(), {"text_data": ""}) - row = connection.execute(select([text_table.c.text_data])).first() + row = connection.execute(select(text_table.c.text_data)).first() eq_(row, ("",)) def test_text_null_strings(self, connection): text_table = self.tables.text_table connection.execute(text_table.insert(), {"text_data": None}) - row = connection.execute(select([text_table.c.text_data])).first() + row = connection.execute(select(text_table.c.text_data)).first() eq_(row, (None,)) def test_literal(self): @@ -302,7 +296,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(date_table.insert(), {"date_data": self.data}) - row = connection.execute(select([date_table.c.date_data])).first() + row = connection.execute(select(date_table.c.date_data)).first() compare = self.compare or self.data eq_(row, (compare,)) @@ -313,7 +307,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(date_table.insert(), {"date_data": None}) - row = connection.execute(select([date_table.c.date_data])).first() + row = connection.execute(select(date_table.c.date_data)).first() eq_(row, (None,)) @testing.requires.datetime_literals @@ -332,7 +326,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase): date_table.insert(), {"date_data": self.data} ) id_ = result.inserted_primary_key[0] - stmt = select([date_table.c.id]).where( + stmt = select(date_table.c.id).where( case( [ ( @@ -438,7 +432,7 @@ class IntegerTest(_LiteralRoundTripFixture, fixtures.TestBase): connection.execute(int_table.insert(), {"integer_data": data}) - row = connection.execute(select([int_table.c.integer_data])).first() + row = connection.execute(select(int_table.c.integer_data)).first() eq_(row, (data,)) @@ -545,7 +539,7 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase): def test_float_coerce_round_trip(self, connection): expr = 15.7563 - val = connection.scalar(select([literal(expr)])) + val = connection.scalar(select(literal(expr))) eq_(val, expr) # this does not work in MySQL, see #4036, however we choose not @@ -556,14 +550,14 @@ class NumericTest(_LiteralRoundTripFixture, fixtures.TestBase): def test_decimal_coerce_round_trip(self, connection): expr = decimal.Decimal("15.7563") - val = connection.scalar(select([literal(expr)])) + val = connection.scalar(select(literal(expr))) eq_(val, expr) @testing.emits_warning(r".*does \*not\* support Decimal objects natively") def test_decimal_coerce_round_trip_w_cast(self, connection): expr = decimal.Decimal("15.7563") - val = connection.scalar(select([cast(expr, Numeric(10, 4))])) + val = connection.scalar(select(cast(expr, Numeric(10, 4)))) eq_(val, expr) @testing.requires.precision_numerics_general @@ -665,9 +659,7 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) row = connection.execute( - select( - [boolean_table.c.value, boolean_table.c.unconstrained_value] - ) + select(boolean_table.c.value, boolean_table.c.unconstrained_value) ).first() eq_(row, (True, False)) @@ -683,9 +675,7 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) row = connection.execute( - select( - [boolean_table.c.value, boolean_table.c.unconstrained_value] - ) + select(boolean_table.c.value, boolean_table.c.unconstrained_value) ).first() eq_(row, (None, None)) @@ -705,13 +695,13 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([boolean_table.c.id]).where(boolean_table.c.value) + select(boolean_table.c.id).where(boolean_table.c.value) ), 1, ) eq_( conn.scalar( - select([boolean_table.c.id]).where( + select(boolean_table.c.id).where( boolean_table.c.unconstrained_value ) ), @@ -719,13 +709,13 @@ class BooleanTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) eq_( conn.scalar( - select([boolean_table.c.id]).where(~boolean_table.c.value) + select(boolean_table.c.id).where(~boolean_table.c.value) ), 2, ) eq_( conn.scalar( - select([boolean_table.c.id]).where( + select(boolean_table.c.id).where( ~boolean_table.c.unconstrained_value ) ), @@ -760,7 +750,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): data_table.insert(), {"name": "row1", "data": data_element} ) - row = connection.execute(select([data_table.c.data])).first() + row = connection.execute(select(data_table.c.data)).first() eq_(row, (data_element,)) @@ -806,7 +796,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): expr = data_table.c.data["key1"] expr = getattr(expr, "as_%s" % datatype)() - roundtrip = conn.scalar(select([expr])) + roundtrip = conn.scalar(select(expr)) eq_(roundtrip, value) if util.py3k: # skip py2k to avoid comparing unicode to str etc. is_(type(roundtrip), type(value)) @@ -828,7 +818,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): expr = data_table.c.data["key1"] expr = getattr(expr, "as_%s" % datatype)() - row = conn.execute(select([expr]).where(expr == value)).first() + row = conn.execute(select(expr).where(expr == value)).first() # make sure we get a row even if value is None eq_(row, (value,)) @@ -850,7 +840,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): expr = data_table.c.data[("key1", "subkey1")] expr = getattr(expr, "as_%s" % datatype)() - row = conn.execute(select([expr]).where(expr == value)).first() + row = conn.execute(select(expr).where(expr == value)).first() # make sure we get a row even if value is None eq_(row, (value,)) @@ -882,7 +872,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) row = conn.execute( - select([data_table.c.data, data_table.c.nulldata]) + select(data_table.c.data, data_table.c.nulldata) ).first() eq_(row, (data_element, data_element)) @@ -903,7 +893,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): conn.execute( data_table.insert(), {"name": "row1", "data": data_element} ) - row = conn.execute(select([data_table.c.data])).first() + row = conn.execute(select(data_table.c.data)).first() eq_(row, (data_element,)) eq_(js.mock_calls, [mock.call(data_element)]) @@ -919,12 +909,12 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([self.tables.data_table.c.name]).where(col.is_(null())) + select(self.tables.data_table.c.name).where(col.is_(null())) ), "r1", ) - eq_(conn.scalar(select([col])), None) + eq_(conn.scalar(select(col)), None) def test_round_trip_json_null_as_json_null(self, connection): col = self.tables.data_table.c["data"] @@ -936,14 +926,14 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([self.tables.data_table.c.name]).where( + select(self.tables.data_table.c.name).where( cast(col, String) == "null" ) ), "r1", ) - eq_(conn.scalar(select([col])), None) + eq_(conn.scalar(select(col)), None) def test_round_trip_none_as_json_null(self): col = self.tables.data_table.c["data"] @@ -955,14 +945,14 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): eq_( conn.scalar( - select([self.tables.data_table.c.name]).where( + select(self.tables.data_table.c.name).where( cast(col, String) == "null" ) ), "r1", ) - eq_(conn.scalar(select([col])), None) + eq_(conn.scalar(select(col)), None) def test_unicode_round_trip(self): # note we include Unicode supplementary characters as well @@ -979,7 +969,7 @@ class JSONTest(_LiteralRoundTripFixture, fixtures.TablesTest): ) eq_( - conn.scalar(select([self.tables.data_table.c.data])), + conn.scalar(select(self.tables.data_table.c.data)), { util.u("réve🐍 illé"): util.u("réve🐍 illé"), "data": {"k1": util.u("drôl🐍e")}, @@ -1087,7 +1077,7 @@ class JSONStringCastIndexTest(_LiteralRoundTripFixture, fixtures.TablesTest): def _test_index_criteria(self, crit, expected, test_literal=True): self._criteria_fixture() with config.db.connect() as conn: - stmt = select([self.tables.data_table.c.name]).where(crit) + stmt = select(self.tables.data_table.c.name).where(crit) eq_(conn.scalar(stmt), expected) diff --git a/lib/sqlalchemy/testing/warnings.py b/lib/sqlalchemy/testing/warnings.py index 3850f65c8..298b20c11 100644 --- a/lib/sqlalchemy/testing/warnings.py +++ b/lib/sqlalchemy/testing/warnings.py @@ -31,9 +31,6 @@ def setup_filters(): "ignore", category=DeprecationWarning, message=".*inspect.get.*argspec" ) - # ignore 2.0 warnings unless we are explicitly testing for them - warnings.filterwarnings("ignore", category=sa_exc.RemovedIn20Warning) - # ignore things that are deprecated *as of* 2.0 :) warnings.filterwarnings( "ignore", diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index fb90975a1..b2407ea18 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -92,6 +92,7 @@ from .deprecations import deprecated_20_cls # noqa from .deprecations import deprecated_cls # noqa from .deprecations import deprecated_params # noqa from .deprecations import inject_docstring_text # noqa +from .deprecations import SQLALCHEMY_WARN_20 # noqa from .deprecations import warn_deprecated # noqa from .deprecations import warn_deprecated_20 # noqa from .langhelpers import add_parameter_text # noqa @@ -149,6 +150,3 @@ from .langhelpers import warn # noqa from .langhelpers import warn_exception # noqa from .langhelpers import warn_limited # noqa from .langhelpers import wrap_callable # noqa - - -SQLALCHEMY_WARN_20 = False diff --git a/lib/sqlalchemy/util/deprecations.py b/lib/sqlalchemy/util/deprecations.py index e0669c4e8..0a79344c5 100644 --- a/lib/sqlalchemy/util/deprecations.py +++ b/lib/sqlalchemy/util/deprecations.py @@ -8,6 +8,7 @@ """Helpers related to deprecation of functions, methods, classes, other functionality.""" +import os import re import warnings @@ -19,7 +20,19 @@ from .langhelpers import inject_param_text from .. import exc +SQLALCHEMY_WARN_20 = False + +if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"): + SQLALCHEMY_WARN_20 = True + + def _warn_with_version(msg, version, type_, stacklevel): + if type_ is exc.RemovedIn20Warning and not SQLALCHEMY_WARN_20: + return + + if type_ is exc.RemovedIn20Warning: + msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)" + warn = type_(msg) warn.deprecated_since = version @@ -41,7 +54,6 @@ def warn_deprecated_limited(msg, args, version, stacklevel=3): def warn_deprecated_20(msg, stacklevel=3): - msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)" _warn_with_version( msg, @@ -69,7 +81,7 @@ def deprecated_cls(version, message, constructor="__init__"): def deprecated_20_cls(clsname, alternative=None, constructor="__init__"): message = ( - ".. deprecated:: 2.0 The %s class is considered legacy as of the " + ".. deprecated:: 1.4 The %s class is considered legacy as of the " "1.x series of SQLAlchemy and will be removed in 2.0." % clsname ) @@ -108,8 +120,16 @@ def deprecated( """ + # nothing is deprecated "since" 2.0 at this time. All "removed in 2.0" + # should emit the RemovedIn20Warning, but messaging should be expressed + # in terms of "deprecated since 1.4". + + if version == "2.0": + if warning is None: + warning = exc.RemovedIn20Warning + version = "1.4" if add_deprecation_to_docstring: - header = ".. deprecated:: %s %s" % (version, (message or "")) + header = ".. deprecated:: %s %s" % (version, (message or ""),) else: header = None @@ -119,7 +139,8 @@ def deprecated( if warning is None: warning = exc.SADeprecationWarning - message += " (deprecated since: %s)" % version + if warning is not exc.RemovedIn20Warning: + message += " (deprecated since: %s)" % version def decorate(fn): return _decorate_with_warning( @@ -162,6 +183,7 @@ def deprecated_params(**specs): messages = {} versions = {} version_warnings = {} + for param, (version, message) in specs.items(): versions[param] = version messages[param] = _sanitize_restructured_text(message) @@ -173,6 +195,7 @@ def deprecated_params(**specs): def decorate(fn): spec = compat.inspect_getfullargspec(fn) + if spec.defaults is not None: defaults = dict( zip( @@ -186,6 +209,8 @@ def deprecated_params(**specs): check_defaults = () check_kw = set(messages) + check_any_kw = spec.varkw + @decorator def warned(fn, *args, **kwargs): for m in check_defaults: @@ -198,6 +223,18 @@ def deprecated_params(**specs): version_warnings[m], stacklevel=3, ) + + if check_any_kw in messages and set(kwargs).difference( + check_defaults + ): + + _warn_with_version( + messages[check_any_kw], + versions[check_any_kw], + version_warnings[check_any_kw], + stacklevel=3, + ) + for m in check_kw: if m in kwargs: _warn_with_version( @@ -206,7 +243,6 @@ def deprecated_params(**specs): version_warnings[m], stacklevel=3, ) - return fn(*args, **kwargs) doc = fn.__doc__ is not None and fn.__doc__ or "" @@ -214,7 +250,8 @@ def deprecated_params(**specs): doc = inject_param_text( doc, { - param: ".. deprecated:: %s %s" % (version, (message or "")) + param: ".. deprecated:: %s %s" + % ("1.4" if version == "2.0" else version, (message or "")) for param, (version, message) in specs.items() }, ) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 57d3be83b..28b7aa4cc 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -1701,9 +1701,9 @@ def inject_param_text(doctext, inject_params): while doclines: line = doclines.pop(0) if to_inject is None: - m = re.match(r"(\s+):param (?:\\\*\*?)?(.+?):", line) + m = re.match(r"(\s+):param (.+?):", line) if m: - param = m.group(2) + param = m.group(2).lstrip("*") if param in inject_params: # default indent to that of :param: plus one indent = " " * len(m.group(1)) + " " |
