]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Fixed ``scalar_one`` usage after ``unique``.
authorFederico Caselli <cfederico87@gmail.com>
Sat, 17 Apr 2021 09:56:51 +0000 (11:56 +0200)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 17 Apr 2021 17:18:32 +0000 (13:18 -0400)
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

doc/build/changelog/unreleased_14/6299.rst [new file with mode: 0644]
lib/sqlalchemy/engine/result.py
test/base/test_result.py
test/orm/test_relationship_criteria.py

diff --git a/doc/build/changelog/unreleased_14/6299.rst b/doc/build/changelog/unreleased_14/6299.rst
new file mode 100644 (file)
index 0000000..1fd5cad
--- /dev/null
@@ -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.
index 205efe441587b7c2c2cbb6ee327d7429e8ffad88..f02ceff156be90b1c569cfd1983b9f5c0a14691f 100644 (file)
@@ -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):
index a15bf1cfaee19927eaff8fee9846156c73c89528..d94602203ce1a199453374767f92cbc06bb1b799 100644 (file)
@@ -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)
index 04afe34779bf91ee27e941efcb8072b197d9746b..43e890eab0c4123276ae4359bf1703d5adb55178 100644 (file)
@@ -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