mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-16 13:47:20 -04:00
e545298e35
large patch to get ORM / typing efforts started. this is to support adding new test cases to mypy, support dropping sqlalchemy2-stubs entirely from the test suite, validate major ORM typing reorganization to eliminate the need for the mypy plugin. * New declarative approach which uses annotation introspection, fixes: #7535 * Mapped[] is now at the base of all ORM constructs that find themselves in classes, to support direct typing without plugins * Mypy plugin updated for new typing structures * Mypy test suite broken out into "plugin" tests vs. "plain" tests, and enhanced to better support test structures where we assert that various objects are introspected by the type checker as we expect. as we go forward with typing, we will add new use cases to "plain" where we can assert that types are introspected as we expect. * For typing support, users will be much more exposed to the class names of things. Add these all to "sqlalchemy" import space. * Column(ForeignKey()) no longer needs to be `@declared_attr` if the FK refers to a remote table * composite() attributes mapped to a dataclass no longer need to implement a `__composite_values__()` method * with_variant() accepts multiple dialect names Change-Id: I22797c0be73a8fbbd2d6f5e0c0b7258b17fe145d Fixes: #7535 Fixes: #7551 References: #6810
341 lines
10 KiB
Python
341 lines
10 KiB
Python
# coding: utf-8
|
|
|
|
from sqlalchemy import desc
|
|
from sqlalchemy import ForeignKey
|
|
from sqlalchemy import func
|
|
from sqlalchemy import Integer
|
|
from sqlalchemy import join
|
|
from sqlalchemy import literal_column
|
|
from sqlalchemy import MetaData
|
|
from sqlalchemy import select
|
|
from sqlalchemy import String
|
|
from sqlalchemy import testing
|
|
from sqlalchemy.ext import serializer
|
|
from sqlalchemy.orm import aliased
|
|
from sqlalchemy.orm import class_mapper
|
|
from sqlalchemy.orm import column_property
|
|
from sqlalchemy.orm import configure_mappers
|
|
from sqlalchemy.orm import joinedload
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.orm import scoped_session
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.testing import AssertsCompiledSQL
|
|
from sqlalchemy.testing import eq_
|
|
from sqlalchemy.testing import fixtures
|
|
from sqlalchemy.testing.schema import Column
|
|
from sqlalchemy.testing.schema import Table
|
|
|
|
|
|
def pickle_protocols():
|
|
return iter([-1, 1, 2])
|
|
# return iter([-1, 0, 1, 2])
|
|
|
|
|
|
class User(fixtures.ComparableEntity):
|
|
pass
|
|
|
|
|
|
class Address(fixtures.ComparableEntity):
|
|
pass
|
|
|
|
|
|
class SerializeTest(AssertsCompiledSQL, fixtures.MappedTest):
|
|
|
|
run_setup_mappers = "once"
|
|
run_inserts = "once"
|
|
run_deletes = None
|
|
|
|
@classmethod
|
|
def define_tables(cls, metadata):
|
|
global users, addresses
|
|
users = Table(
|
|
"users",
|
|
metadata,
|
|
Column("id", Integer, primary_key=True),
|
|
Column("name", String(50)),
|
|
)
|
|
addresses = Table(
|
|
"addresses",
|
|
metadata,
|
|
Column("id", Integer, primary_key=True),
|
|
Column("email", String(50)),
|
|
Column("user_id", Integer, ForeignKey("users.id")),
|
|
)
|
|
|
|
@classmethod
|
|
def setup_mappers(cls):
|
|
global Session
|
|
Session = scoped_session(sessionmaker(testing.db))
|
|
cls.mapper_registry.map_imperatively(
|
|
User,
|
|
users,
|
|
properties={
|
|
"addresses": relationship(
|
|
Address, backref="user", order_by=addresses.c.id
|
|
)
|
|
},
|
|
)
|
|
cls.mapper_registry.map_imperatively(Address, addresses)
|
|
configure_mappers()
|
|
|
|
@classmethod
|
|
def insert_data(cls, connection):
|
|
params = [
|
|
dict(list(zip(("id", "name"), column_values)))
|
|
for column_values in [
|
|
(7, "jack"),
|
|
(8, "ed"),
|
|
(9, "fred"),
|
|
(10, "chuck"),
|
|
]
|
|
]
|
|
connection.execute(users.insert(), params)
|
|
connection.execute(
|
|
addresses.insert(),
|
|
[
|
|
dict(list(zip(("id", "user_id", "email"), column_values)))
|
|
for column_values in [
|
|
(1, 7, "jack@bean.com"),
|
|
(2, 8, "ed@wood.com"),
|
|
(3, 8, "ed@bettyboop.com"),
|
|
(4, 8, "ed@lala.com"),
|
|
(5, 9, "fred@fred.com"),
|
|
]
|
|
],
|
|
)
|
|
|
|
def test_tables(self):
|
|
assert (
|
|
serializer.loads(
|
|
serializer.dumps(users, -1), users.metadata, Session
|
|
)
|
|
is users
|
|
)
|
|
|
|
def test_columns(self):
|
|
assert (
|
|
serializer.loads(
|
|
serializer.dumps(users.c.name, -1), users.metadata, Session
|
|
)
|
|
is users.c.name
|
|
)
|
|
|
|
def test_mapper(self):
|
|
user_mapper = class_mapper(User)
|
|
assert (
|
|
serializer.loads(serializer.dumps(user_mapper, -1), None, None)
|
|
is user_mapper
|
|
)
|
|
|
|
def test_attribute(self):
|
|
assert (
|
|
serializer.loads(serializer.dumps(User.name, -1), None, None)
|
|
is User.name
|
|
)
|
|
|
|
def test_expression(self):
|
|
expr = select(users).select_from(users.join(addresses)).limit(5)
|
|
re_expr = serializer.loads(
|
|
serializer.dumps(expr, -1), users.metadata, None
|
|
)
|
|
eq_(str(expr), str(re_expr))
|
|
eq_(
|
|
Session.connection().execute(re_expr).fetchall(),
|
|
[(7, "jack"), (8, "ed"), (8, "ed"), (8, "ed"), (9, "fred")],
|
|
)
|
|
|
|
def test_query_one(self):
|
|
q = (
|
|
Session.query(User)
|
|
.filter(User.name == "ed")
|
|
.options(joinedload(User.addresses))
|
|
)
|
|
|
|
q2 = serializer.loads(serializer.dumps(q, -1), users.metadata, Session)
|
|
|
|
def go():
|
|
eq_(
|
|
q2.all(),
|
|
[
|
|
User(
|
|
name="ed",
|
|
addresses=[
|
|
Address(id=2),
|
|
Address(id=3),
|
|
Address(id=4),
|
|
],
|
|
)
|
|
],
|
|
)
|
|
|
|
self.assert_sql_count(testing.db, go, 1)
|
|
|
|
eq_(
|
|
q2.join(User.addresses)
|
|
.filter(Address.email == "ed@bettyboop.com")
|
|
.enable_eagerloads(False)
|
|
.with_entities(func.count(literal_column("*")))
|
|
.scalar(),
|
|
1,
|
|
)
|
|
u1 = Session.get(User, 8)
|
|
q = (
|
|
Session.query(Address)
|
|
.filter(Address.user == u1)
|
|
.order_by(desc(Address.email))
|
|
)
|
|
q2 = serializer.loads(serializer.dumps(q, -1), users.metadata, Session)
|
|
eq_(
|
|
q2.all(),
|
|
[
|
|
Address(email="ed@wood.com"),
|
|
Address(email="ed@lala.com"),
|
|
Address(email="ed@bettyboop.com"),
|
|
],
|
|
)
|
|
|
|
def test_query_two(self):
|
|
q = (
|
|
Session.query(User)
|
|
.join(User.addresses)
|
|
.filter(Address.email.like("%fred%"))
|
|
)
|
|
q2 = serializer.loads(serializer.dumps(q, -1), users.metadata, Session)
|
|
eq_(q2.all(), [User(name="fred")])
|
|
eq_(list(q2.with_entities(User.id, User.name)), [(9, "fred")])
|
|
|
|
def test_query_three(self):
|
|
ua = aliased(User)
|
|
q = (
|
|
Session.query(ua)
|
|
.join(ua.addresses)
|
|
.filter(Address.email.like("%fred%"))
|
|
)
|
|
for prot in pickle_protocols():
|
|
q2 = serializer.loads(
|
|
serializer.dumps(q, prot), users.metadata, Session
|
|
)
|
|
eq_(q2.all(), [User(name="fred")])
|
|
|
|
# try to pull out the aliased entity here...
|
|
ua_2 = q2._compile_state()._entities[0].entity_zero.entity
|
|
eq_(list(q2.with_entities(ua_2.id, ua_2.name)), [(9, "fred")])
|
|
|
|
def test_annotated_one(self):
|
|
j = join(users, addresses)._annotate({"foo": "bar"})
|
|
query = select(addresses).select_from(j)
|
|
|
|
str(query)
|
|
for prot in pickle_protocols():
|
|
pickled_failing = serializer.dumps(j, prot)
|
|
serializer.loads(pickled_failing, users.metadata, None)
|
|
|
|
def test_orm_join(self):
|
|
from sqlalchemy.orm import join
|
|
|
|
j = join(User, Address, User.addresses)
|
|
|
|
j2 = serializer.loads(serializer.dumps(j, -1), users.metadata)
|
|
assert j2.left is j.left
|
|
assert j2.right is j.right
|
|
|
|
@testing.exclude(
|
|
"sqlite", "<=", (3, 5, 9), "id comparison failing on the buildbot"
|
|
)
|
|
def test_aliases(self):
|
|
u7, u8, u9, u10 = Session.query(User).order_by(User.id).all()
|
|
ualias = aliased(User)
|
|
q = (
|
|
Session.query(User, ualias)
|
|
.join(ualias, User.id < ualias.id)
|
|
.filter(User.id < 9)
|
|
.order_by(User.id, ualias.id)
|
|
)
|
|
eq_(
|
|
list(q.all()), [(u7, u8), (u7, u9), (u7, u10), (u8, u9), (u8, u10)]
|
|
)
|
|
q2 = serializer.loads(serializer.dumps(q, -1), users.metadata, Session)
|
|
eq_(
|
|
list(q2.all()),
|
|
[(u7, u8), (u7, u9), (u7, u10), (u8, u9), (u8, u10)],
|
|
)
|
|
|
|
def test_any(self):
|
|
r = User.addresses.any(Address.email == "x")
|
|
ser = serializer.dumps(r, -1)
|
|
x = serializer.loads(ser, users.metadata)
|
|
eq_(str(r), str(x))
|
|
|
|
def test_unicode(self):
|
|
m = MetaData()
|
|
t = Table("\u6e2c\u8a66", m, Column("\u6e2c\u8a66_id", Integer))
|
|
|
|
expr = select(t).where(t.c["\u6e2c\u8a66_id"] == 5)
|
|
|
|
expr2 = serializer.loads(serializer.dumps(expr, -1), m)
|
|
|
|
self.assert_compile(
|
|
expr2,
|
|
'SELECT "\u6e2c\u8a66"."\u6e2c\u8a66_id" FROM "\u6e2c\u8a66" '
|
|
'WHERE "\u6e2c\u8a66"."\u6e2c\u8a66_id" = :\u6e2c\u8a66_id_1',
|
|
dialect="default",
|
|
)
|
|
|
|
|
|
class ColumnPropertyWParamTest(
|
|
AssertsCompiledSQL, fixtures.DeclarativeMappedTest
|
|
):
|
|
__dialect__ = "default"
|
|
|
|
run_create_tables = None
|
|
|
|
@classmethod
|
|
def setup_classes(cls):
|
|
Base = cls.DeclarativeBasic
|
|
|
|
global TestTable
|
|
|
|
class TestTable(Base):
|
|
__tablename__ = "test"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
_some_id = Column("some_id", String)
|
|
some_primary_id = column_property(
|
|
func.left(_some_id, 6).cast(Integer)
|
|
)
|
|
|
|
def test_deserailize_colprop(self):
|
|
TestTable = self.classes.TestTable
|
|
|
|
s = scoped_session(sessionmaker())
|
|
|
|
expr = s.query(TestTable).filter(TestTable.some_primary_id == 123456)
|
|
|
|
expr2 = serializer.loads(serializer.dumps(expr), TestTable.metadata, s)
|
|
|
|
# note in the original, the same bound parameter is used twice
|
|
self.assert_compile(
|
|
expr,
|
|
"SELECT test.some_id AS test_some_id, "
|
|
"CAST(left(test.some_id, :left_1) AS INTEGER) AS anon_1, "
|
|
"test.id AS test_id FROM test WHERE "
|
|
"CAST(left(test.some_id, :left_1) AS INTEGER) = :param_1",
|
|
checkparams={"left_1": 6, "param_1": 123456},
|
|
)
|
|
|
|
# in the deserialized, it's two separate parameter objects which
|
|
# need to have different anonymous names. they still have
|
|
# the same value however
|
|
self.assert_compile(
|
|
expr2,
|
|
"SELECT test.some_id AS test_some_id, "
|
|
"CAST(left(test.some_id, :left_1) AS INTEGER) AS anon_1, "
|
|
"test.id AS test_id FROM test WHERE "
|
|
"CAST(left(test.some_id, :left_2) AS INTEGER) = :param_1",
|
|
checkparams={"left_1": 6, "left_2": 6, "param_1": 123456},
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
testing.main()
|