mirror of
https://github.com/sqlalchemy/sqlalchemy.git
synced 2026-05-20 15:42:11 -04:00
b0cfa7379c
A variety of caching issues found by running all tests with statement caching turned on. The cache system now has a more conservative approach where any subclass of a SQL element will by default invalidate the cache key unless it adds the flag inherit_cache=True at the class level, or if it implements its own caching. Add working caching to a few elements that were omitted previously; fix some caching implementations to suit lesser used edge cases such as json casts and array slices. Refine the way BaseCursorResult and CursorMetaData interact with caching; to suit cases like Alembic modifying table structures, don't cache the cursor metadata if it were created against a cursor.description using non-positional matching, e.g. "select *". if a table re-ordered its columns or added/removed, now that data is obsolete. Additionally we have to adapt the cursor metadata _keymap regardless of if we just processed cursor.description, because if we ran against a cached SQLCompiler we won't have the right columns in _keymap. Other refinements to how and when we do this adaption as some weird cases were exposed in the Postgresql dialect, a text() construct that names just one column that is not actually in the statement. Fixed that also as it looks like a cut-and-paste artifact that doesn't actually affect anything. Various issues with re-use of compiled result maps and cursor metadata in conjunction with tables being changed, such as change in order of columns. mappers can be cleared but the class remains, meaning a mapper has to use itself as the cache key not the class. lots of bound parameter / literal issues, due to Alembic creating a straight subclass of bindparam that renders inline directly. While we can update Alembic to not do this, we have to assume other people might be doing this, so bindparam() implements the inherit_cache=True logic as well that was a bit involved. turn on cache stats in logging. Includes a fix to subqueryloader which moves all setup to the create_row_processor() phase and elminates any storage within the compiled context. This includes some changes to create_row_processor() signature and a revising of the technique used to determine if the loader can participate in polymorphic queries, which is also applied to selectinloading. DML update.values() and ordered_values() now coerces the keys as we have tests that pass an arbitrary class here which only includes __clause_element__(), so the key can't be cached unless it is coerced. this in turn changed how composite attributes support bulk update to use the standard approach of ClauseElement with annotations that are parsed in the ORM context. memory profiling successfully caught that the Session from Query was getting passed into _statement_20() so that was a big win for that test suite. Apparently Compiler had .execute() and .scalar() methods stuck on it, these date back to version 0.4 and there was a single test in the PostgreSQL dialect tests that exercised it for no apparent reason. Removed these methods as well as the concept of a Compiler holding onto a "bind". Fixes: #5386 Change-Id: I990b43aab96b42665af1b2187ad6020bee778784
130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
from sqlalchemy import Column
|
|
from sqlalchemy import Enum
|
|
from sqlalchemy import ForeignKey
|
|
from sqlalchemy import Integer
|
|
from sqlalchemy import MetaData
|
|
from sqlalchemy import select
|
|
from sqlalchemy import String
|
|
from sqlalchemy import Table
|
|
from sqlalchemy import testing
|
|
from sqlalchemy.orm import join as ormjoin
|
|
from sqlalchemy.orm import mapper
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.testing import eq_
|
|
from sqlalchemy.testing import fixtures
|
|
from sqlalchemy.testing import profiling
|
|
from sqlalchemy.util import classproperty
|
|
|
|
|
|
class EnumTest(fixtures.TestBase):
|
|
__requires__ = ("cpython", "python_profiling_backend")
|
|
|
|
def setup(self):
|
|
class SomeEnum(object):
|
|
# Implements PEP 435 in the minimal fashion needed by SQLAlchemy
|
|
|
|
_members = {}
|
|
|
|
@classproperty
|
|
def __members__(cls):
|
|
"""simulate a very expensive ``__members__`` getter"""
|
|
for i in range(10):
|
|
x = {}
|
|
x.update({k: v for k, v in cls._members.items()}.copy())
|
|
return x.copy()
|
|
|
|
def __init__(self, name, value):
|
|
self.name = name
|
|
self.value = value
|
|
self._members[name] = self
|
|
setattr(self.__class__, name, self)
|
|
|
|
for i in range(400):
|
|
SomeEnum("some%d" % i, i)
|
|
|
|
self.SomeEnum = SomeEnum
|
|
|
|
@profiling.function_call_count()
|
|
def test_create_enum_from_pep_435_w_expensive_members(self):
|
|
Enum(self.SomeEnum)
|
|
|
|
|
|
class CacheKeyTest(fixtures.TestBase):
|
|
# python3 is just to have less variability in test counts
|
|
__requires__ = ("cpython", "python_profiling_backend", "python3")
|
|
|
|
@testing.fixture(scope="class")
|
|
def mapping_fixture(self):
|
|
# note in order to work nicely with "fixture" we are emerging
|
|
# a whole new model of setup/teardown, since pytest "fixture"
|
|
# sort of purposely works badly with setup/teardown
|
|
|
|
metadata = MetaData()
|
|
parent = Table(
|
|
"parent",
|
|
metadata,
|
|
Column("id", Integer, primary_key=True),
|
|
Column("data", String(20)),
|
|
)
|
|
child = Table(
|
|
"child",
|
|
metadata,
|
|
Column("id", Integer, primary_key=True),
|
|
Column("data", String(20)),
|
|
Column(
|
|
"parent_id", Integer, ForeignKey("parent.id"), nullable=False
|
|
),
|
|
)
|
|
|
|
class Parent(testing.entities.BasicEntity):
|
|
pass
|
|
|
|
class Child(testing.entities.BasicEntity):
|
|
pass
|
|
|
|
mapper(
|
|
Parent,
|
|
parent,
|
|
properties={"children": relationship(Child, backref="parent")},
|
|
)
|
|
mapper(Child, child)
|
|
|
|
return Parent, Child
|
|
|
|
@testing.fixture(scope="function")
|
|
def stmt_fixture_one(self, mapping_fixture):
|
|
# note that by using ORM elements we will have annotations in these
|
|
# items also which is part of the performance hit
|
|
Parent, Child = mapping_fixture
|
|
|
|
return [
|
|
(
|
|
select([Parent.id, Child.id])
|
|
.select_from(ormjoin(Parent, Child, Parent.children))
|
|
.where(Child.id == 5)
|
|
)
|
|
for i in range(100)
|
|
]
|
|
|
|
@profiling.function_call_count(variance=0.15, warmup=2)
|
|
def test_statement_key_is_cached(self, stmt_fixture_one):
|
|
current_key = None
|
|
for stmt in stmt_fixture_one:
|
|
key = stmt._generate_cache_key()
|
|
assert key is not None
|
|
if current_key:
|
|
eq_(key, current_key)
|
|
else:
|
|
current_key = key
|
|
|
|
@profiling.function_call_count(variance=0.15, warmup=0)
|
|
def test_statement_key_is_not_cached(self, stmt_fixture_one):
|
|
current_key = None
|
|
for stmt in stmt_fixture_one:
|
|
key = stmt._generate_cache_key()
|
|
assert key is not None
|
|
if current_key:
|
|
eq_(key, current_key)
|
|
else:
|
|
current_key = key
|