From: Federico Caselli Date: Sat, 17 Apr 2021 09:56:51 +0000 (+0200) Subject: Fixed ``scalar_one`` usage after ``unique``. X-Git-Tag: rel_1_4_10~10^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b12438aa61058b993be8ebcc80fa6df1401f4664;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Fixed ``scalar_one`` usage after ``unique``. Fixed an issue that prevented using ``scalar_one`` or ``scalar_one_or_none()`` after a call to ``unique``. Additionally includes some clarifications in result.py and also removes pep-484 annotations for now as these are duplicate on top of sqlalchemy2-stubs. Fixes: #6299 Change-Id: Ia04f3d078c7a4f0d8488745e43d2fd63b60de9a0 --- diff --git a/doc/build/changelog/unreleased_14/6299.rst b/doc/build/changelog/unreleased_14/6299.rst new file mode 100644 index 0000000000..1fd5cadfc3 --- /dev/null +++ b/doc/build/changelog/unreleased_14/6299.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm, result + :tickets: 6299 + + Fixed an issue when using 2.0 style execution that prevented using + :meth:`_result.Result.scalar_one` or + :meth:`_result.Result.scalar_one_or_none` after calling + :meth:`_result.Result.unique`, for the case where the ORM is returning a + single-element row in any case. diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 205efe4415..f02ceff156 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -22,14 +22,6 @@ from ..sql.base import InPlaceGenerative from ..util import collections_abc from ..util import py2k -if util.TYPE_CHECKING: - from typing import Any - from typing import List - from typing import Optional - from typing import Int - from typing import Iterator - from typing import Mapping - if _baserow_usecext: from sqlalchemy.cresultproxy import tuplegetter @@ -573,6 +565,7 @@ class ResultInternal(InPlaceGenerative): return None if scalar and self._source_supports_scalars: + self._generate_rows = False make_row = None else: make_row = self._row_getter @@ -625,8 +618,6 @@ class ResultInternal(InPlaceGenerative): ) else: next_row = _NO_ROW - - if not raise_for_second_row: # if we checked for second row then that would have # closed us :) self._soft_close(hard=True) @@ -777,7 +768,6 @@ class Result(_WithKeys, ResultInternal): @_generative def unique(self, strategy=None): - # type: (Optional[object]) -> Result """Apply unique filtering to the objects returned by this :class:`_engine.Result`. @@ -817,7 +807,6 @@ class Result(_WithKeys, ResultInternal): self._unique_filter_state = (set(), strategy) def columns(self, *col_expressions): - # type: (*object) -> Result r"""Establish the columns that should be returned in each row. This method may be used to limit the columns returned as well @@ -856,7 +845,6 @@ class Result(_WithKeys, ResultInternal): return self._column_slices(col_expressions) def scalars(self, index=0): - # type: (Int) -> ScalarResult """Return a :class:`_result.ScalarResult` filtering object which will return single elements rather than :class:`_row.Row` objects. @@ -953,7 +941,6 @@ class Result(_WithKeys, ResultInternal): return self._next_impl() def partitions(self, size=None): - # type: (Optional[Int]) -> Iterator[List[Row]] """Iterate through sub-lists of rows of the size given. Each list will be of the size given, excluding the last list to @@ -992,13 +979,11 @@ class Result(_WithKeys, ResultInternal): break def fetchall(self): - # type: () -> List[Row] """A synonym for the :meth:`_engine.Result.all` method.""" return self._allrows() def fetchone(self): - # type: () -> Row """Fetch one row. When all rows are exhausted, returns None. @@ -1021,7 +1006,6 @@ class Result(_WithKeys, ResultInternal): return row def fetchmany(self, size=None): - # type: (Optional[Int]) -> List[Row] """Fetch many rows. When all rows are exhausted, returns an empty list. @@ -1039,7 +1023,6 @@ class Result(_WithKeys, ResultInternal): return self._manyrow_getter(self, size) def all(self): - # type: () -> List[Row] """Return all rows in a list. Closes the result set after invocation. Subsequent invocations @@ -1054,7 +1037,6 @@ class Result(_WithKeys, ResultInternal): return self._allrows() def first(self): - # type: () -> Row """Fetch the first row or None if no row is present. Closes the result set and discards remaining rows. @@ -1074,10 +1056,12 @@ class Result(_WithKeys, ResultInternal): :meth:`_result.Result.one` """ - return self._only_one_row(False, False, False) + + return self._only_one_row( + raise_for_second_row=False, raise_for_none=False, scalar=False + ) def one_or_none(self): - # type: () -> Optional[Row] """Return at most one result or raise an exception. Returns ``None`` if the result has no rows. @@ -1097,10 +1081,11 @@ class Result(_WithKeys, ResultInternal): :meth:`_result.Result.one` """ - return self._only_one_row(True, False, False) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=False, scalar=False + ) def scalar_one(self): - # type: () -> Any """Return exactly one scalar result or raise an exception. This is equivalent to calling :meth:`.Result.scalars` and then @@ -1113,10 +1098,11 @@ class Result(_WithKeys, ResultInternal): :meth:`.Result.scalars` """ - return self._only_one_row(True, True, True) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=True, scalar=True + ) def scalar_one_or_none(self): - # type: () -> Optional[Any] """Return exactly one or no scalar result. This is equivalent to calling :meth:`.Result.scalars` and then @@ -1129,10 +1115,11 @@ class Result(_WithKeys, ResultInternal): :meth:`.Result.scalars` """ - return self._only_one_row(True, False, True) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=False, scalar=True + ) def one(self): - # type: () -> Row """Return exactly one row or raise an exception. Raises :class:`.NoResultFound` if the result returns no @@ -1159,10 +1146,11 @@ class Result(_WithKeys, ResultInternal): :meth:`_result.Result.scalar_one` """ - return self._only_one_row(True, True, False) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=True, scalar=False + ) def scalar(self): - # type: () -> Optional[Any] """Fetch the first column of the first row, and close the result set. Returns None if there are no rows to fetch. @@ -1176,7 +1164,9 @@ class Result(_WithKeys, ResultInternal): :return: a Python scalar value , or None if no rows remain. """ - return self._only_one_row(False, False, True) + return self._only_one_row( + raise_for_second_row=False, raise_for_none=False, scalar=True + ) def freeze(self): """Return a callable object that will produce copies of this @@ -1277,7 +1267,6 @@ class ScalarResult(FilterResult): self._unique_filter_state = real_result._unique_filter_state def unique(self, strategy=None): - # type: () -> ScalarResult """Apply unique filtering to the objects returned by this :class:`_engine.ScalarResult`. @@ -1288,7 +1277,6 @@ class ScalarResult(FilterResult): return self def partitions(self, size=None): - # type: (Optional[Int]) -> Iterator[List[Any]] """Iterate through sub-lists of elements of the size given. Equivalent to :meth:`_result.Result.partitions` except that @@ -1307,13 +1295,11 @@ class ScalarResult(FilterResult): break def fetchall(self): - # type: () -> List[Any] """A synonym for the :meth:`_engine.ScalarResult.all` method.""" return self._allrows() def fetchmany(self, size=None): - # type: (Optional[Int]) -> List[Any] """Fetch many objects. Equivalent to :meth:`_result.Result.fetchmany` except that @@ -1324,7 +1310,6 @@ class ScalarResult(FilterResult): return self._manyrow_getter(self, size) def all(self): - # type: () -> List[Any] """Return all scalar values in a list. Equivalent to :meth:`_result.Result.all` except that @@ -1346,7 +1331,6 @@ class ScalarResult(FilterResult): return self._next_impl() def first(self): - # type: () -> Optional[Any] """Fetch the first object or None if no object is present. Equivalent to :meth:`_result.Result.first` except that @@ -1355,10 +1339,11 @@ class ScalarResult(FilterResult): """ - return self._only_one_row(False, False, False) + return self._only_one_row( + raise_for_second_row=False, raise_for_none=False, scalar=False + ) def one_or_none(self): - # type: () -> Optional[Any] """Return at most one object or raise an exception. Equivalent to :meth:`_result.Result.one_or_none` except that @@ -1366,10 +1351,11 @@ class ScalarResult(FilterResult): are returned. """ - return self._only_one_row(True, False, False) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=False, scalar=False + ) def one(self): - # type: () -> Any """Return exactly one object or raise an exception. Equivalent to :meth:`_result.Result.one` except that @@ -1377,7 +1363,9 @@ class ScalarResult(FilterResult): are returned. """ - return self._only_one_row(True, True, False) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=True, scalar=False + ) class MappingResult(_WithKeys, FilterResult): @@ -1401,7 +1389,6 @@ class MappingResult(_WithKeys, FilterResult): self._metadata = self._metadata._reduce([0]) def unique(self, strategy=None): - # type: () -> MappingResult """Apply unique filtering to the objects returned by this :class:`_engine.MappingResult`. @@ -1412,12 +1399,10 @@ class MappingResult(_WithKeys, FilterResult): return self def columns(self, *col_expressions): - # type: (*object) -> MappingResult r"""Establish the columns that should be returned in each row.""" return self._column_slices(col_expressions) def partitions(self, size=None): - # type: (Optional[Int]) -> Iterator[List[Mapping]] """Iterate through sub-lists of elements of the size given. Equivalent to :meth:`_result.Result.partitions` except that @@ -1436,13 +1421,11 @@ class MappingResult(_WithKeys, FilterResult): break def fetchall(self): - # type: () -> List[Mapping] """A synonym for the :meth:`_engine.MappingResult.all` method.""" return self._allrows() def fetchone(self): - # type: () -> Mapping """Fetch one object. Equivalent to :meth:`_result.Result.fetchone` except that @@ -1458,7 +1441,6 @@ class MappingResult(_WithKeys, FilterResult): return row def fetchmany(self, size=None): - # type: (Optional[Int]) -> List[Mapping] """Fetch many objects. Equivalent to :meth:`_result.Result.fetchmany` except that @@ -1470,7 +1452,6 @@ class MappingResult(_WithKeys, FilterResult): return self._manyrow_getter(self, size) def all(self): - # type: () -> List[Mapping] """Return all scalar values in a list. Equivalent to :meth:`_result.Result.all` except that @@ -1493,7 +1474,6 @@ class MappingResult(_WithKeys, FilterResult): return self._next_impl() def first(self): - # type: () -> Optional[Mapping] """Fetch the first object or None if no object is present. Equivalent to :meth:`_result.Result.first` except that @@ -1502,10 +1482,11 @@ class MappingResult(_WithKeys, FilterResult): """ - return self._only_one_row(False, False, False) + return self._only_one_row( + raise_for_second_row=False, raise_for_none=False, scalar=False + ) def one_or_none(self): - # type: () -> Optional[Mapping] """Return at most one object or raise an exception. Equivalent to :meth:`_result.Result.one_or_none` except that @@ -1513,10 +1494,11 @@ class MappingResult(_WithKeys, FilterResult): are returned. """ - return self._only_one_row(True, False, False) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=False, scalar=False + ) def one(self): - # type: () -> Mapping """Return exactly one object or raise an exception. Equivalent to :meth:`_result.Result.one` except that @@ -1524,7 +1506,9 @@ class MappingResult(_WithKeys, FilterResult): are returned. """ - return self._only_one_row(True, True, False) + return self._only_one_row( + raise_for_second_row=True, raise_for_none=True, scalar=False + ) class FrozenResult(object): diff --git a/test/base/test_result.py b/test/base/test_result.py index a15bf1cfae..d94602203c 100644 --- a/test/base/test_result.py +++ b/test/base/test_result.py @@ -766,7 +766,7 @@ class ResultTest(fixtures.TestBase): result = result.scalars(1).unique() eq_(result.all(), [None, 4]) - def test_scalar_only_on_filter(self): + def test_scalar_only_on_filter_w_unique(self): # test a mixture of the "real" result and the # scalar filter, where scalar has unique and real result does not. @@ -793,7 +793,7 @@ class ResultTest(fixtures.TestBase): eq_(next(result), (3, 4, 5)) # non-unique fifth row eq_(u_s.all(), []) # unique set is done because only 3 is left - def test_scalar_none_one(self): + def test_scalar_none_one_w_unique(self): result = self._fixture(data=[(1, None, 2)]) result = result.scalars(1).unique() @@ -801,7 +801,7 @@ class ResultTest(fixtures.TestBase): # one is returning None, see? eq_(result.one(), None) - def test_scalar_none_one_or_none(self): + def test_scalar_none_one_or_none_w_unique(self): result = self._fixture(data=[(1, None, 2)]) result = result.scalars(1).unique() @@ -810,6 +810,20 @@ class ResultTest(fixtures.TestBase): # have to allow for this unforuntately, unless we want to raise? eq_(result.one_or_none(), None) + def test_scalar_one_w_unique(self): + result = self._fixture(data=[(1, None, 2)]) + + result = result.unique() + + eq_(result.scalar_one(), 1) + + def test_scalars_one_w_unique(self): + result = self._fixture(data=[(1, None, 2)]) + + result = result.unique() + + eq_(result.scalars().one(), 1) + def test_scalar_none_first(self): result = self._fixture(data=[(1, None, 2)]) @@ -1075,6 +1089,27 @@ class OnlyScalarsTest(fixtures.TestBase): eq_(r.all(), [(1,), (2,), (4,)]) + @testing.combinations( + lambda r: r.scalar(), + lambda r: r.scalar_one(), + lambda r: r.scalar_one_or_none(), + argnames="get", + ) + def test_unique_scalar_accessors(self, no_tuple_one_fixture, get): + metadata = result.SimpleResultMetaData( + ["a", "b", "c"], _unique_filters=[int] + ) + + r = result.ChunkedIteratorResult( + metadata, + no_tuple_one_fixture, + source_supports_scalars=True, + ) + + r = r.unique() + + eq_(get(r), 1) + def test_scalar_mode_mfiltered_unique_mappings_all(self, no_tuple_fixture): metadata = result.SimpleResultMetaData( ["a", "b", "c"], _unique_filters=[int] @@ -1176,20 +1211,32 @@ class OnlyScalarsTest(fixtures.TestBase): eq_(list(r), [(1,), (2,), (1,), (1,), (4,)]) - def test_scalar_mode_first(self, no_tuple_one_fixture): + @testing.combinations( + lambda r: r.one(), + lambda r: r.first(), + lambda r: r.one_or_none(), + argnames="get", + ) + def test_scalar_mode_first(self, no_tuple_one_fixture, get): metadata = result.SimpleResultMetaData(["a", "b", "c"]) r = result.ChunkedIteratorResult( metadata, no_tuple_one_fixture, source_supports_scalars=True ) - eq_(r.one(), (1,)) + eq_(get(r), (1,)) - def test_scalar_mode_scalar_one(self, no_tuple_one_fixture): + @testing.combinations( + lambda r: r.scalar(), + lambda r: r.scalar_one(), + lambda r: r.scalar_one_or_none(), + argnames="get", + ) + def test_scalar_mode_scalar_one(self, no_tuple_one_fixture, get): metadata = result.SimpleResultMetaData(["a", "b", "c"]) r = result.ChunkedIteratorResult( metadata, no_tuple_one_fixture, source_supports_scalars=True ) - eq_(r.scalar_one(), 1) + eq_(get(r), 1) diff --git a/test/orm/test_relationship_criteria.py b/test/orm/test_relationship_criteria.py index 04afe34779..43e890eab0 100644 --- a/test/orm/test_relationship_criteria.py +++ b/test/orm/test_relationship_criteria.py @@ -1025,6 +1025,27 @@ class RelationshipCriteriaTest(_Fixtures, testing.AssertsCompiledSQL): ), ) + @testing.combinations( + lambda r: r.scalar(), + lambda r: r.scalar_one(), + lambda r: r.scalar_one_or_none(), + argnames="get", + ) + def test_joinedload_scalar(self, user_address_fixture, get): + User, Address = user_address_fixture + + s = Session(testing.db, future=True) + + stmt = ( + select(User) + .options(joinedload(User.addresses)) + .where(User.name == "jack") + ) + r = s.execute(stmt).unique() + + jack = get(r) + eq_(jack.name, "jack") + def test_selectinload_local_criteria(self, user_address_fixture): User, Address = user_address_fixture