diff options
author | mike bayer <mike_mp@zzzcomputing.com> | 2020-08-05 04:49:57 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@bbpush.zzzcomputing.com> | 2020-08-05 04:49:57 +0000 |
commit | ba9380ef28871b2274ab0bab75e5efddf2ced467 (patch) | |
tree | a69613dca1434c25ed2b8bfb877338206213f4e4 /lib/sqlalchemy | |
parent | c813fe1678c4edbce32f0652353ed70f0a14566f (diff) | |
parent | 14fdd6260a578488bdad95b738ea6af5c2fcd13c (diff) | |
download | sqlalchemy-ba9380ef28871b2274ab0bab75e5efddf2ced467.tar.gz |
Merge "Establish future behavior for Session cascade backrefs, bind"
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r-- | lib/sqlalchemy/orm/dependency.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 27 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/unitofwork.py | 25 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/fixtures.py | 15 |
6 files changed, 62 insertions, 15 deletions
diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index 082998ba8..d4680e394 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -1184,7 +1184,7 @@ class ManyToManyDP(DependencyProcessor): if secondary_delete: associationrow = secondary_delete[0] - statement = self.secondary.delete( + statement = self.secondary.delete().where( sql.and_( *[ c == sql.bindparam(c.key, type_=c.type) diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index d991f6229..b8226dfc0 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -1291,7 +1291,9 @@ class Query( those being selected. """ + return self._from_self(*entities) + def _from_self(self, *entities): fromclause = ( self.with_labels() .enable_eagerloads(False) @@ -2935,7 +2937,7 @@ class Query( """ col = sql.func.count(sql.literal_column("*")) - return self.from_self(col).scalar() + return self._from_self(col).scalar() def delete(self, synchronize_session="evaluate"): r"""Perform a bulk delete query. diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index d4c5b4665..cb490b7d7 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -408,6 +408,10 @@ class RelationshipProperty(StrategizedProperty): will not cascade an incoming transient object into the session of a persistent parent, if the event is received via backref. + .. deprecated:: 1.4 The + :paramref:`_orm.relationship.cascade_backrefs` + flag will default to False in all cases in SQLAlchemy 2.0. + .. seealso:: :ref:`backref_cascade` - Full discussion and examples on how diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 01163b8d4..25aedd52d 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -1937,16 +1937,34 @@ class Session(_SessionClassMethods): # now we are in legacy territory. looking for "bind" on tables # that are via bound metadata. this goes away in 2.0. + + future_msg = "" + future_code = "" + if mapper and clause is None: clause = mapper.persist_selectable if clause is not None: if clause.bind: - return clause.bind + if self.future: + future_msg = ( + " A bind was located via legacy bound metadata, but " + "since future=True is set on this Session, this " + "bind is ignored." + ) + else: + return clause.bind if mapper: if mapper.persist_selectable.bind: - return mapper.persist_selectable.bind + if self.future: + future_msg = ( + " A bind was located via legacy bound metadata, but " + "since future=True is set on this Session, this " + "bind is ignored." + ) + else: + return mapper.persist_selectable.bind context = [] if mapper is not None: @@ -1955,8 +1973,9 @@ class Session(_SessionClassMethods): context.append("SQL expression") raise sa_exc.UnboundExecutionError( - "Could not locate a bind configured on %s or this Session" - % (", ".join(context)) + "Could not locate a bind configured on %s or this Session.%s" + % (", ".join(context), future_msg), + code=future_code, ) def query(self, *entities, **kwargs): diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 97eea4864..9c67130ce 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -21,6 +21,16 @@ from .. import util from ..util import topological +def _warn_for_cascade_backrefs(state, prop): + util.warn_deprecated_20( + '"%s" object is being merged into a Session along the backref ' + 'cascade path for relationship "%s"; in SQLAlchemy 2.0, this ' + "reverse cascade will not take place. Set cascade_backrefs to " + "False for the 2.0 behavior; or to set globally for the whole " + "Session, set the future=True flag" % (state.class_.__name__, prop) + ) + + def track_cascade_events(descriptor, prop): """Establish event listeners on object attributes which handle cascade-on-set/append. @@ -42,11 +52,17 @@ def track_cascade_events(descriptor, prop): prop = state.manager.mapper._props[key] item_state = attributes.instance_state(item) + if ( prop._cascade.save_update - and (prop.cascade_backrefs or key == initiator.key) + and ( + (prop.cascade_backrefs and not sess.future) + or key == initiator.key + ) and not sess._contains_state(item_state) ): + if key != initiator.key: + _warn_for_cascade_backrefs(item_state, prop) sess._save_or_update_state(item_state) return item @@ -101,9 +117,14 @@ def track_cascade_events(descriptor, prop): newvalue_state = attributes.instance_state(newvalue) if ( prop._cascade.save_update - and (prop.cascade_backrefs or key == initiator.key) + and ( + (prop.cascade_backrefs and not sess.future) + or key == initiator.key + ) and not sess._contains_state(newvalue_state) ): + if key != initiator.key: + _warn_for_cascade_backrefs(newvalue_state, prop) sess._save_or_update_state(newvalue_state) if ( diff --git a/lib/sqlalchemy/testing/fixtures.py b/lib/sqlalchemy/testing/fixtures.py index 1eac76598..1583147d4 100644 --- a/lib/sqlalchemy/testing/fixtures.py +++ b/lib/sqlalchemy/testing/fixtures.py @@ -293,13 +293,14 @@ class TablesTest(TestBase): continue if table not in headers: continue - cls.bind.execute( - table.insert(), - [ - dict(zip(headers[table], column_values)) - for column_values in rows[table] - ], - ) + with cls.bind.begin() as conn: + conn.execute( + table.insert(), + [ + dict(zip(headers[table], column_values)) + for column_values in rows[table] + ], + ) class RemovesEvents(object): |