--- /dev/null
+.. 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.
+
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):
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)
) = 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)
):
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)
return list(res)
else:
if item == -1:
+ _no_negative_indexes()
return list(iterable_query)[-1]
else:
return list(iterable_query[item : item + 1])[0]
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
"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
)
+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"
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
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
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:]
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]
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
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.
self.assert_sql(testing.db, lambda: q[-2:-5], [])
- 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[:],