diff options
author | mike bayer <mike_mp@zzzcomputing.com> | 2020-09-22 19:47:21 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@bbpush.zzzcomputing.com> | 2020-09-22 19:47:21 +0000 |
commit | 9e31cce582569dfe6fb33b33aadd43d70c2ae593 (patch) | |
tree | 0eaacdd685be3d3968d9a1f9e64294f864ba963f | |
parent | c3e9f76e8d29db35875cb7ee956fe29e49a6fc95 (diff) | |
parent | 96888aee7611c15532af2ae47e567f68c28b2474 (diff) | |
download | sqlalchemy-9e31cce582569dfe6fb33b33aadd43d70c2ae593.tar.gz |
Merge "Deprecate negative slice indexes"
-rw-r--r-- | doc/build/changelog/unreleased_14/5606.rst | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 20 | ||||
-rw-r--r-- | test/orm/test_deprecations.py | 95 | ||||
-rw-r--r-- | test/orm/test_dynamic.py | 31 | ||||
-rw-r--r-- | test/orm/test_generative.py | 9 | ||||
-rw-r--r-- | test/orm/test_query.py | 58 |
8 files changed, 198 insertions, 36 deletions
diff --git a/doc/build/changelog/unreleased_14/5606.rst b/doc/build/changelog/unreleased_14/5606.rst new file mode 100644 index 000000000..0ce9ebd5f --- /dev/null +++ b/doc/build/changelog/unreleased_14/5606.rst @@ -0,0 +1,11 @@ +.. change:: + :tags: change, orm + :tickets: 5606 + + The "slice index" feature used by :class:`_orm.Query` as well as by the + dynamic relationship loader will no longer accept negative indexes in + SQLAlchemy 2.0. These operations do not work efficiently and load the + entire collection in, which is both surprising and undesirable. These + will warn in 1.4 unless the :paramref:`_orm.Session.future` flag is set in + which case they will raise IndexError. + diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index af67b4772..48161a256 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -510,7 +510,9 @@ class AppenderQuery(Generative): attributes.PASSIVE_NO_INITIALIZE, ).indexed(index) else: - return orm_util._getitem(self, index) + return orm_util._getitem( + self, index, allow_negative=not self.session.future + ) @_generative def limit(self, limit): diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index c6e6f6466..42987932d 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -2495,7 +2495,11 @@ class Query( self._compile_options += {"_enable_single_crit": False} def __getitem__(self, item): - return orm_util._getitem(self, item) + return orm_util._getitem( + self, + item, + allow_negative=not self.session or not self.session.future, + ) @_generative @_assertions(_no_statement_condition) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 27b14d95b..c8d639e8c 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -1811,12 +1811,26 @@ def randomize_unitofwork(): ) = session.set = mapper.set = dependency.set = RandomSet -def _getitem(iterable_query, item): +def _getitem(iterable_query, item, allow_negative): """calculate __getitem__ in terms of an iterable query object that also has a slice() method. """ + def _no_negative_indexes(): + if not allow_negative: + raise IndexError( + "negative indexes are not accepted by SQL " + "index / slice operators" + ) + else: + util.warn_deprecated_20( + "Support for negative indexes for SQL index / slice operators " + "will be " + "removed in 2.0; these operators fetch the complete result " + "and do not work efficiently." + ) + if isinstance(item, slice): start, stop, step = util.decode_slice(item) @@ -1827,11 +1841,10 @@ def _getitem(iterable_query, item): ): return [] - # perhaps we should execute a count() here so that we - # can still use LIMIT/OFFSET ? elif (isinstance(start, int) and start < 0) or ( isinstance(stop, int) and stop < 0 ): + _no_negative_indexes() return list(iterable_query)[item] res = iterable_query.slice(start, stop) @@ -1841,6 +1854,7 @@ def _getitem(iterable_query, item): return list(res) else: if item == -1: + _no_negative_indexes() return list(iterable_query)[-1] else: return list(iterable_query[item : item + 1])[0] diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index 532d0aa10..bcba1f031 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -62,6 +62,7 @@ from sqlalchemy.testing.schema import Column from sqlalchemy.testing.schema import Table from . import _fixtures from .inheritance import _poly_fixtures +from .test_dynamic import _DynamicFixture from .test_events import _RemoveListeners from .test_options import PathTest as OptionsPathTest from .test_query import QueryTest @@ -88,6 +89,73 @@ class DeprecatedQueryTest(_fixtures.FixtureTest, AssertsCompiledSQL): "subquery object." ) + def test_deprecated_negative_slices(self): + User = self.classes.User + + sess = create_session() + q = sess.query(User).order_by(User.id) + + with testing.expect_deprecated( + "Support for negative indexes for SQL index / slice operators" + ): + eq_(q[-5:-2], [User(id=7), User(id=8)]) + + with testing.expect_deprecated( + "Support for negative indexes for SQL index / slice operators" + ): + eq_(q[-1], User(id=10)) + + with testing.expect_deprecated( + "Support for negative indexes for SQL index / slice operators" + ): + eq_(q[-2], User(id=9)) + + with testing.expect_deprecated( + "Support for negative indexes for SQL index / slice operators" + ): + eq_(q[:-2], [User(id=7), User(id=8)]) + + # this doesn't evaluate anything because it's a net-negative + eq_(q[-2:-5], []) + + def test_deprecated_negative_slices_compile(self): + User = self.classes.User + + sess = create_session() + q = sess.query(User).order_by(User.id) + + with testing.expect_deprecated( + "Support for negative indexes for SQL index / slice operators" + ): + self.assert_sql( + testing.db, + lambda: q[-5:-2], + [ + ( + "SELECT users.id AS users_id, users.name " + "AS users_name " + "FROM users ORDER BY users.id", + {}, + ) + ], + ) + + with testing.expect_deprecated( + "Support for negative indexes for SQL index / slice operators" + ): + self.assert_sql( + testing.db, + lambda: q[-5:], + [ + ( + "SELECT users.id AS users_id, users.name " + "AS users_name " + "FROM users ORDER BY users.id", + {}, + ) + ], + ) + def test_invalid_column(self): User = self.classes.User @@ -721,6 +789,33 @@ class SelfRefFromSelfTest(fixtures.MappedTest, AssertsCompiledSQL): ) +class DynamicTest(_DynamicFixture, _fixtures.FixtureTest): + def test_negative_slice_access_raises(self): + User, Address = self._user_address_fixture() + sess = create_session(testing.db) + u1 = sess.get(User, 8) + + with testing.expect_deprecated_20( + "Support for negative indexes for SQL index / slice" + ): + eq_(u1.addresses[-1], Address(id=4)) + + with testing.expect_deprecated_20( + "Support for negative indexes for SQL index / slice" + ): + eq_(u1.addresses[-5:-2], [Address(id=2)]) + + with testing.expect_deprecated_20( + "Support for negative indexes for SQL index / slice" + ): + eq_(u1.addresses[-2], Address(id=3)) + + with testing.expect_deprecated_20( + "Support for negative indexes for SQL index / slice" + ): + eq_(u1.addresses[:-2], [Address(id=2)]) + + class FromSelfTest(QueryTest, AssertsCompiledSQL): __dialect__ = "default" diff --git a/test/orm/test_dynamic.py b/test/orm/test_dynamic.py index 836d9f88c..5f18b9bce 100644 --- a/test/orm/test_dynamic.py +++ b/test/orm/test_dynamic.py @@ -18,6 +18,7 @@ from sqlalchemy.testing import assert_raises from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import eq_ +from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import is_ from sqlalchemy.testing.assertsql import CompiledSQL from test.orm import _fixtures @@ -158,7 +159,35 @@ class DynamicTest(_DynamicFixture, _fixtures.FixtureTest, AssertsCompiledSQL): eq_(u1.addresses[0], Address(id=2)) eq_(u1.addresses[0:2], [Address(id=2), Address(id=3)]) - eq_(u1.addresses[-1], Address(id=4)) + + def test_negative_slice_access_raises(self): + User, Address = self._user_address_fixture() + sess = create_session(testing.db, future=True) + u1 = sess.get(User, 8) + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + u1.addresses[-1] + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + u1.addresses[-5:-2] + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + u1.addresses[-2] + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + u1.addresses[:-2] def test_statement(self): """test that the .statement accessor returns the actual statement that diff --git a/test/orm/test_generative.py b/test/orm/test_generative.py index c5cf4a2c0..0bca2f975 100644 --- a/test/orm/test_generative.py +++ b/test/orm/test_generative.py @@ -57,8 +57,6 @@ class GenerativeQueryTest(fixtures.MappedTest): orig = query.all() assert query[1] == orig[1] - assert query[-4] == orig[-4] - assert query[-1] == orig[-1] assert list(query[10:20]) == orig[10:20] assert list(query[10:]) == orig[10:] @@ -66,10 +64,9 @@ class GenerativeQueryTest(fixtures.MappedTest): assert list(query[:10]) == orig[:10] assert list(query[5:5]) == orig[5:5] assert list(query[10:40:3]) == orig[10:40:3] - assert list(query[-5:]) == orig[-5:] - assert list(query[-2:-5]) == orig[-2:-5] - assert list(query[-5:-2]) == orig[-5:-2] - assert list(query[:-2]) == orig[:-2] + + # negative slices and indexes are deprecated and are tested + # in test_query.py and test_deprecations.py assert query[10:20][5] == orig[10:20][5] diff --git a/test/orm/test_query.py b/test/orm/test_query.py index cee583ffb..31643e5ff 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -61,6 +61,7 @@ from sqlalchemy.sql import expression from sqlalchemy.sql import operators from sqlalchemy.sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from sqlalchemy.testing import AssertsCompiledSQL +from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_ from sqlalchemy.testing import is_false @@ -2414,6 +2415,39 @@ class SliceTest(QueryTest): create_session().query(User).filter(User.id == 27).first() is None ) + def test_negative_indexes_raise(self): + User = self.classes.User + + sess = create_session(future=True) + q = sess.query(User).order_by(User.id) + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + q[-5:-2] + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + q[-1] + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + q[-5] + + with expect_raises_message( + IndexError, + "negative indexes are not accepted by SQL index / slice operators", + ): + q[:-2] + + # this doesn't evaluate anything because it's a net-negative + eq_(q[-2:-5], []) + def test_limit_offset_applies(self): """Test that the expected LIMIT/OFFSET is applied for slices. @@ -2473,30 +2507,6 @@ class SliceTest(QueryTest): self.assert_sql( testing.db, - lambda: q[-5:-2], - [ - ( - "SELECT users.id AS users_id, users.name AS users_name " - "FROM users ORDER BY users.id", - {}, - ) - ], - ) - - self.assert_sql( - testing.db, - lambda: q[-5:], - [ - ( - "SELECT users.id AS users_id, users.name AS users_name " - "FROM users ORDER BY users.id", - {}, - ) - ], - ) - - self.assert_sql( - testing.db, lambda: q[:], [ ( |