Files
sqlalchemy/test/orm/test_loading.py
T
Mike Bayer b96321ae79 Support result.close() for all iterator patterns
This change contains new features for 2.0 only as well as some
behaviors that will be backported to 1.4.

For 1.4 and 2.0:

Fixed issue where the underlying DBAPI cursor would not be closed when
using :class:`_orm.Query` with :meth:`_orm.Query.yield_per` and direct
iteration, if a user-defined exception case were raised within the
iteration process, interrupting the iterator. This would lead to the usual
MySQL-related issues with server side cursors out of sync.

For 1.4 only:

A similar scenario can occur when using :term:`2.x` executions with direct
use of :class:`.Result`, in that case the end-user code has access to the
:class:`.Result` itself and should call :meth:`.Result.close` directly.
Version 2.0 will feature context-manager calling patterns to address this
use case.  However within the 1.4 scope, ensured that ``.close()`` methods
are available on all :class:`.Result` implementations including
:class:`.ScalarResult`, :class:`.MappingResult`.

For 2.0 only:

To better support the use case of iterating :class:`.Result` and
:class:`.AsyncResult` objects where user-defined exceptions may interrupt
the iteration, both objects as well as variants such as
:class:`.ScalarResult`, :class:`.MappingResult`,
:class:`.AsyncScalarResult`, :class:`.AsyncMappingResult` now support
context manager usage, where the result will be closed at the end of
iteration.

Corrected various typing issues within the engine and async engine
packages.

Fixes: #8710
Change-Id: I3166328bfd3900957eb33cbf1061d0495c9df670
2022-11-03 18:42:52 -04:00

259 lines
7.6 KiB
Python

from sqlalchemy import exc
from sqlalchemy import literal
from sqlalchemy import literal_column
from sqlalchemy import select
from sqlalchemy import testing
from sqlalchemy import text
from sqlalchemy.orm import loading
from sqlalchemy.orm import relationship
from sqlalchemy.testing import is_true
from sqlalchemy.testing import mock
from sqlalchemy.testing.assertions import assert_raises
from sqlalchemy.testing.assertions import assert_raises_message
from sqlalchemy.testing.assertions import eq_
from sqlalchemy.testing.assertions import expect_raises_message
from sqlalchemy.testing.fixtures import fixture_session
from . import _fixtures
# class GetFromIdentityTest(_fixtures.FixtureTest):
# class LoadOnIdentTest(_fixtures.FixtureTest):
class SelectStarTest(_fixtures.FixtureTest):
run_setup_mappers = "once"
run_inserts = "once"
run_deletes = None
@classmethod
def setup_mappers(cls):
cls._setup_stock_mapping()
@testing.combinations(
"plain", "text", "literal_column", argnames="exprtype"
)
@testing.combinations("core", "orm", argnames="coreorm")
def test_single_star(self, exprtype, coreorm):
"""test for #8235"""
User, Address = self.classes("User", "Address")
if exprtype == "plain":
star = "*"
elif exprtype == "text":
star = text("*")
elif exprtype == "literal_column":
star = literal_column("*")
else:
assert False
stmt = (
select(star)
.select_from(User)
.join(Address)
.where(User.id == 7)
.order_by(User.id, Address.id)
)
s = fixture_session()
if coreorm == "core":
result = s.connection().execute(stmt)
elif coreorm == "orm":
result = s.execute(stmt)
else:
assert False
eq_(result.all(), [(7, "jack", 1, 7, "jack@bean.com")])
@testing.combinations(
"plain", "text", "literal_column", argnames="exprtype"
)
@testing.combinations(
lambda User, star: (star, User.id),
lambda User, star: (star, User),
lambda User, star: (User.id, star),
lambda User, star: (User, star),
lambda User, star: (literal("some text"), star),
lambda User, star: (star, star),
lambda User, star: (star, text("some text")),
argnames="testcase",
)
def test_no_star_orm_combinations(self, exprtype, testcase):
"""test for #8235"""
User = self.classes.User
if exprtype == "plain":
star = "*"
elif exprtype == "text":
star = text("*")
elif exprtype == "literal_column":
star = literal_column("*")
else:
assert False
args = testing.resolve_lambda(testcase, User=User, star=star)
stmt = select(*args).select_from(User)
s = fixture_session()
with expect_raises_message(
exc.CompileError,
r"Can't generate ORM query that includes multiple expressions "
r"at the same time as '\*';",
):
s.execute(stmt)
class InstanceProcessorTest(_fixtures.FixtureTest):
def test_state_no_load_path_comparison(self):
# test issue #5110
User, Order, Address = self.classes("User", "Order", "Address")
users, orders, addresses = self.tables("users", "orders", "addresses")
self.mapper_registry.map_imperatively(
User,
users,
properties={
"addresses": relationship(Address, lazy="joined"),
"orders": relationship(
Order, lazy="joined", order_by=orders.c.id
),
},
)
self.mapper_registry.map_imperatively(
Order,
orders,
properties={"address": relationship(Address, lazy="joined")},
)
self.mapper_registry.map_imperatively(Address, addresses)
s = fixture_session()
def go():
eq_(
User(
id=7,
orders=[
Order(id=1, address=Address(id=1)),
Order(id=3, address=Address(id=1)),
Order(id=5, address=None),
],
),
s.get(User, 7, populate_existing=True),
)
self.assert_sql_count(testing.db, go, 1)
class InstancesTest(_fixtures.FixtureTest):
run_setup_mappers = "once"
run_inserts = "once"
run_deletes = None
@classmethod
def setup_mappers(cls):
cls._setup_stock_mapping()
def test_cursor_close_exception_raised_in_iteration(self):
"""test #8710"""
User = self.classes.User
s = fixture_session()
stmt = select(User).execution_options(yield_per=1)
result = s.execute(stmt)
raw_cursor = result.raw
for row in result:
with expect_raises_message(Exception, "whoops"):
for row in result:
raise Exception("whoops")
is_true(raw_cursor._soft_closed)
def test_cursor_close_w_failed_rowproc(self):
User = self.classes.User
s = fixture_session()
q = s.query(User)
ctx = q._compile_context()
cursor = mock.Mock()
ctx.compile_state._entities = [
mock.Mock(row_processor=mock.Mock(side_effect=Exception("boom")))
]
assert_raises(Exception, loading.instances, cursor, ctx)
assert cursor.close.called, "Cursor wasn't closed"
def test_row_proc_not_created(self):
User = self.classes.User
s = fixture_session()
q = s.query(User.id, User.name)
stmt = select(User.id)
assert_raises_message(
exc.NoSuchColumnError,
"Could not locate column in row for column 'users.name'",
q.from_statement(stmt).all,
)
class MergeResultTest(_fixtures.FixtureTest):
run_setup_mappers = "once"
run_inserts = "once"
run_deletes = None
@classmethod
def setup_mappers(cls):
cls._setup_stock_mapping()
def _fixture(self):
User = self.classes.User
s = fixture_session()
u1, u2, u3, u4 = (
User(id=1, name="u1"),
User(id=2, name="u2"),
User(id=7, name="u3"),
User(id=8, name="u4"),
)
s.query(User).filter(User.id.in_([7, 8])).all()
s.close()
return s, [u1, u2, u3, u4]
def test_single_entity_frozen(self):
s = fixture_session()
User = self.classes.User
stmt = select(User).where(User.id.in_([7, 8, 9])).order_by(User.id)
result = s.execute(stmt)
it = loading.merge_frozen_result(s, stmt, result.freeze())
eq_([x.id for x in it().scalars()], [7, 8, 9])
def test_single_column_frozen(self):
User = self.classes.User
s = fixture_session()
stmt = select(User.id).where(User.id.in_([7, 8, 9])).order_by(User.id)
result = s.execute(stmt)
it = loading.merge_frozen_result(s, stmt, result.freeze())
eq_([x.id for x in it()], [7, 8, 9])
def test_entity_col_mix_plain_tuple_frozen(self):
s = fixture_session()
User = self.classes.User
stmt = (
select(User, User.id)
.where(User.id.in_([7, 8, 9]))
.order_by(User.id)
)
result = s.execute(stmt)
it = loading.merge_frozen_result(s, stmt, result.freeze())
it = list(it())
eq_([(x.id, y) for x, y in it], [(7, 7), (8, 8), (9, 9)])
eq_(list(it[0]._mapping.keys()), ["User", "id"])