summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/changelog_14.rst40
-rw-r--r--doc/build/changelog/changelog_20.rst17
-rw-r--r--doc/build/changelog/unreleased_14/9590.rst10
-rw-r--r--doc/build/changelog/unreleased_14/9634.rst11
-rw-r--r--doc/build/changelog/unreleased_20/9656.rst8
-rw-r--r--doc/build/changelog/unreleased_20/9715.rst8
-rw-r--r--doc/build/changelog/unreleased_20/9717.rst8
-rw-r--r--doc/build/changelog/unreleased_20/9731.rst12
-rw-r--r--doc/build/changelog/unreleased_20/9739.rst9
-rw-r--r--doc/build/changelog/unreleased_20/9746.rst8
-rw-r--r--doc/build/conf.py4
-rw-r--r--doc/build/core/internals.rst3
-rw-r--r--doc/build/orm/extensions/associationproxy.rst79
-rw-r--r--doc/build/orm/extensions/asyncio.rst78
-rw-r--r--examples/asyncio/async_orm.py15
-rw-r--r--examples/asyncio/async_orm_writeonly.py7
-rw-r--r--examples/asyncio/greenlet_orm.py7
-rw-r--r--lib/sqlalchemy/__init__.py3
-rw-r--r--lib/sqlalchemy/dialects/mysql/reflection.py40
-rw-r--r--lib/sqlalchemy/dialects/postgresql/asyncpg.py6
-rw-r--r--lib/sqlalchemy/engine/row.py2
-rw-r--r--lib/sqlalchemy/ext/asyncio/__init__.py1
-rw-r--r--lib/sqlalchemy/ext/asyncio/session.py99
-rw-r--r--lib/sqlalchemy/orm/attributes.py26
-rw-r--r--lib/sqlalchemy/orm/bulk_persistence.py13
-rw-r--r--lib/sqlalchemy/orm/decl_api.py2
-rw-r--r--lib/sqlalchemy/orm/path_registry.py11
-rw-r--r--lib/sqlalchemy/orm/strategies.py1
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py54
-rw-r--r--lib/sqlalchemy/sql/__init__.py1
-rw-r--r--lib/sqlalchemy/sql/_typing.py8
-rw-r--r--lib/sqlalchemy/testing/fixtures.py3
-rw-r--r--lib/sqlalchemy/testing/requirements.py7
-rw-r--r--lib/sqlalchemy/testing/suite/test_insert.py2
-rw-r--r--lib/sqlalchemy/testing/suite/test_reflection.py46
-rw-r--r--lib/sqlalchemy/testing/suite/test_types.py64
-rw-r--r--lib/sqlalchemy/util/typing.py2
-rw-r--r--test/dialect/mysql/test_reflection.py23
-rw-r--r--test/ext/asyncio/test_session_py3k.py118
-rw-r--r--test/orm/dml/test_bulk_statements.py72
-rw-r--r--test/orm/inheritance/test_assorted_poly.py211
-rw-r--r--test/orm/test_cache_key.py51
-rw-r--r--test/profiles.txt54
-rw-r--r--test/requirements.py4
44 files changed, 1111 insertions, 137 deletions
diff --git a/doc/build/changelog/changelog_14.rst b/doc/build/changelog/changelog_14.rst
index 1e80c684c..4b6ed1d77 100644
--- a/doc/build/changelog/changelog_14.rst
+++ b/doc/build/changelog/changelog_14.rst
@@ -14,10 +14,48 @@ This document details individual issue-level changes made throughout
.. changelog::
- :version: 1.4.48
+ :version: 1.4.49
:include_notes_from: unreleased_14
.. changelog::
+ :version: 1.4.48
+ :released: April 30, 2023
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 9728
+ :versions: 2.0.12
+
+ Fixed critical caching issue where the combination of
+ :func:`_orm.aliased()` and :func:`_hybrid.hybrid_property` expression
+ compositions would cause a cache key mismatch, leading to cache keys that
+ held onto the actual :func:`_orm.aliased` object while also not matching
+ that of equivalent constructs, filling up the cache.
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 9634
+ :versions: 2.0.10
+
+ Fixed bug where various ORM-specific getters such as
+ :attr:`.ORMExecuteState.is_column_load`,
+ :attr:`.ORMExecuteState.is_relationship_load`,
+ :attr:`.ORMExecuteState.loader_strategy_path` etc. would throw an
+ ``AttributeError`` if the SQL statement itself were a "compound select"
+ such as a UNION.
+
+ .. change::
+ :tags: bug, orm
+ :tickets: 9590
+ :versions: 2.0.9
+
+ Fixed endless loop which could occur when using "relationship to aliased
+ class" feature and also indicating a recursive eager loader such as
+ ``lazy="selectinload"`` in the loader, in combination with another eager
+ loader on the opposite side. The check for cycles has been fixed to include
+ aliased class relationships.
+
+.. changelog::
:version: 1.4.47
:released: March 18, 2023
diff --git a/doc/build/changelog/changelog_20.rst b/doc/build/changelog/changelog_20.rst
index ad54b7c5a..bb4144cd2 100644
--- a/doc/build/changelog/changelog_20.rst
+++ b/doc/build/changelog/changelog_20.rst
@@ -9,10 +9,25 @@
.. changelog::
- :version: 2.0.12
+ :version: 2.0.13
:include_notes_from: unreleased_20
.. changelog::
+ :version: 2.0.12
+ :released: April 30, 2023
+
+ .. change::
+ :tags: bug, mysql, mariadb
+ :tickets: 9722
+
+ Fixed issues regarding reflection of comments for :class:`_schema.Table`
+ and :class:`_schema.Column` objects, where the comments contained control
+ characters such as newlines. Additional testing support for these
+ characters as well as extended Unicode characters in table and column
+ comments (the latter of which aren't supported by MySQL/MariaDB) added to
+ testing overall.
+
+.. changelog::
:version: 2.0.11
:released: April 26, 2023
diff --git a/doc/build/changelog/unreleased_14/9590.rst b/doc/build/changelog/unreleased_14/9590.rst
deleted file mode 100644
index 472cfc70e..000000000
--- a/doc/build/changelog/unreleased_14/9590.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-.. change::
- :tags: bug, orm
- :tickets: 9590
- :versions: 2.0.9
-
- Fixed endless loop which could occur when using "relationship to aliased
- class" feature and also indicating a recursive eager loader such as
- ``lazy="selectinload"`` in the loader, in combination with another eager
- loader on the opposite side. The check for cycles has been fixed to include
- aliased class relationships.
diff --git a/doc/build/changelog/unreleased_14/9634.rst b/doc/build/changelog/unreleased_14/9634.rst
deleted file mode 100644
index 664e85716..000000000
--- a/doc/build/changelog/unreleased_14/9634.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. change::
- :tags: bug, orm
- :tickets: 9634
- :versions: 2.0.10
-
- Fixed bug where various ORM-specific getters such as
- :attr:`.ORMExecuteState.is_column_load`,
- :attr:`.ORMExecuteState.is_relationship_load`,
- :attr:`.ORMExecuteState.loader_strategy_path` etc. would throw an
- ``AttributeError`` if the SQL statement itself were a "compound select"
- such as a UNION.
diff --git a/doc/build/changelog/unreleased_20/9656.rst b/doc/build/changelog/unreleased_20/9656.rst
new file mode 100644
index 000000000..16c170aa8
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9656.rst
@@ -0,0 +1,8 @@
+.. change::
+ :tags: typing, sql
+ :tickets: 9656
+
+ Added type ``ColumnExpressionArgument`` as a public alias of an internal
+ type. This type is useful since it's what' accepted by the sqlalchemy in
+ many api calls, such as :meth:`_sql.Select.where`, :meth:`_sql.and` and
+ many other.
diff --git a/doc/build/changelog/unreleased_20/9715.rst b/doc/build/changelog/unreleased_20/9715.rst
new file mode 100644
index 000000000..107051b72
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9715.rst
@@ -0,0 +1,8 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 9715
+
+ Fixed loader strategy pathing issues where eager loaders such as
+ :func:`_orm.joinedload` / :func:`_orm.selectinload` would fail to traverse
+ fully for many-levels deep following a load that had a
+ :func:`_orm.with_polymorphic` or similar construct as an interim member.
diff --git a/doc/build/changelog/unreleased_20/9717.rst b/doc/build/changelog/unreleased_20/9717.rst
new file mode 100644
index 000000000..d70ffe17a
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9717.rst
@@ -0,0 +1,8 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 9717
+
+ Fixed issue where ORM Annotated Declarative would not resolve forward
+ references correctly in all cases; in particular, when using
+ ``from __future__ import annotations`` in combination with Pydantic
+ dataclasses.
diff --git a/doc/build/changelog/unreleased_20/9731.rst b/doc/build/changelog/unreleased_20/9731.rst
new file mode 100644
index 000000000..f5337b2c9
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9731.rst
@@ -0,0 +1,12 @@
+.. change::
+ :tags: usecase, asyncio
+ :tickets: 9731
+
+ Added a new helper mixin :class:`_asyncio.AsyncAttrs` that seeks to improve
+ the use of lazy-loader and other expired or deferred ORM attributes with
+ asyncio, providing a simple attribute accessor that provides an ``await``
+ interface to any ORM attribute, whether or not it needs to emit SQL.
+
+ .. seealso::
+
+ :class:`_asyncio.AsyncAttrs`
diff --git a/doc/build/changelog/unreleased_20/9739.rst b/doc/build/changelog/unreleased_20/9739.rst
new file mode 100644
index 000000000..987d69ce2
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9739.rst
@@ -0,0 +1,9 @@
+.. change::
+ :tags: bug, postgresql, regression
+ :tickets: 9739
+
+ Fixed another regression due to the "insertmanyvalues" change in 2.0.10 as
+ part of :ticket:`9618`, in a similar way as regression :ticket:`9701`, where
+ :class:`.LargeBinary` datatypes also need additional casts on when using the
+ asyncpg driver specifically in order to work with the new bulk INSERT
+ format.
diff --git a/doc/build/changelog/unreleased_20/9746.rst b/doc/build/changelog/unreleased_20/9746.rst
new file mode 100644
index 000000000..55d57925e
--- /dev/null
+++ b/doc/build/changelog/unreleased_20/9746.rst
@@ -0,0 +1,8 @@
+.. change::
+ :tags: bug, orm
+ :tickets: 9746
+
+ Fixed issue in new :ref:`orm_queryguide_upsert_returning` feature where the
+ ``populate_existing`` execution option was not being propagated to the
+ loading option, preventing existing attributes from being refreshed
+ in-place.
diff --git a/doc/build/conf.py b/doc/build/conf.py
index ccedb4906..0c1455bdb 100644
--- a/doc/build/conf.py
+++ b/doc/build/conf.py
@@ -242,9 +242,9 @@ copyright = "2007-2023, the SQLAlchemy authors and contributors" # noqa
# The short X.Y version.
version = "2.0"
# The full version, including alpha/beta/rc tags.
-release = "2.0.11"
+release = "2.0.12"
-release_date = "April 26, 2023"
+release_date = "April 30, 2023"
site_base = os.environ.get("RTD_SITE_BASE", "https://www.sqlalchemy.org")
site_adapter_template = "docs_adapter.mako"
diff --git a/doc/build/core/internals.rst b/doc/build/core/internals.rst
index e3769b342..26aa9831d 100644
--- a/doc/build/core/internals.rst
+++ b/doc/build/core/internals.rst
@@ -69,3 +69,6 @@ Some key internal constructs are listed here.
.. autoclass:: sqlalchemy.engine.AdaptedConnection
:members:
+
+.. autoattribute:: sqlalchemy.sql.ColumnExpressionArgument
+ :members:
diff --git a/doc/build/orm/extensions/associationproxy.rst b/doc/build/orm/extensions/associationproxy.rst
index 036969f37..7b1ae3399 100644
--- a/doc/build/orm/extensions/associationproxy.rst
+++ b/doc/build/orm/extensions/associationproxy.rst
@@ -8,13 +8,18 @@ Association Proxy
``associationproxy`` is used to create a read/write view of a
target attribute across a relationship. It essentially conceals
the usage of a "middle" attribute between two endpoints, and
-can be used to cherry-pick fields from a collection of
-related objects or to reduce the verbosity of using the association
-object pattern. Applied creatively, the association proxy allows
+can be used to cherry-pick fields from both a collection of
+related objects or scalar relationship. or to reduce the verbosity
+of using the association object pattern.
+Applied creatively, the association proxy allows
the construction of sophisticated collections and dictionary
views of virtually any geometry, persisted to the database using
standard, transparently configured relational patterns.
+.. contents:: Some example use cases for Association Proxy
+ :depth: 1
+ :local:
+
.. _associationproxy_scalar_collections:
Simplifying Scalar Collections
@@ -124,7 +129,7 @@ the underlying collection or attribute does.
.. _associationproxy_creator:
Creation of New Values
-----------------------
+^^^^^^^^^^^^^^^^^^^^^^
When a list ``append()`` event (or set ``add()``, dictionary ``__setitem__()``,
or scalar assignment event) is intercepted by the association proxy, it
@@ -689,6 +694,72 @@ deleted depends on the relationship cascade setting.
:ref:`unitofwork_cascades`
+Scalar Relationships
+--------------------
+
+The example below illustrates the use of the association proxy on the many
+side of of a one-to-many relationship, accessing attributes of a scalar
+object::
+
+ from __future__ import annotations
+
+ from typing import List
+
+ from sqlalchemy import ForeignKey
+ from sqlalchemy import String
+ from sqlalchemy.ext.associationproxy import association_proxy
+ from sqlalchemy.ext.associationproxy import AssociationProxy
+ from sqlalchemy.orm import DeclarativeBase
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_column
+ from sqlalchemy.orm import relationship
+
+
+ class Base(DeclarativeBase):
+ pass
+
+
+ class Recipe(Base):
+ __tablename__ = "recipe"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str] = mapped_column(String(64))
+
+ steps: Mapped[List[Step]] = relationship(back_populates="recipe")
+ step_descriptions: AssociationProxy[List[str]] = association_proxy(
+ "steps", "description"
+ )
+
+
+ class Step(Base):
+ __tablename__ = "step"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ description: Mapped[str]
+ recipe_id: Mapped[int] = mapped_column(ForeignKey("recipe.id"))
+ recipe: Mapped[Recipe] = relationship(back_populates="steps")
+
+ recipe_name: AssociationProxy[str] = association_proxy("recipe", "name")
+
+ def __init__(self, description: str) -> None:
+ self.description = description
+
+
+ my_snack = Recipe(
+ name="afternoon snack",
+ step_descriptions=[
+ "slice bread",
+ "spread peanut butted",
+ "eat sandwich",
+ ],
+ )
+
+A summary of the steps of ``my_snack`` can be printed using::
+
+ >>> for i, step in enumerate(my_snack.steps, 1):
+ ... print(f"Step {i} of {step.recipe_name!r}: {step.description}")
+ Step 1 of 'afternoon snack': slice bread
+ Step 2 of 'afternoon snack': spread peanut butted
+ Step 3 of 'afternoon snack': eat sandwich
+
API Documentation
-----------------
diff --git a/doc/build/orm/extensions/asyncio.rst b/doc/build/orm/extensions/asyncio.rst
index c57f1199c..0dff980e2 100644
--- a/doc/build/orm/extensions/asyncio.rst
+++ b/doc/build/orm/extensions/asyncio.rst
@@ -155,6 +155,7 @@ illustrates a complete example including mapper and session configuration::
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import select
+ from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
@@ -165,7 +166,7 @@ illustrates a complete example including mapper and session configuration::
from sqlalchemy.orm import selectinload
- class Base(DeclarativeBase):
+ class Base(AsyncAttrs, DeclarativeBase):
pass
@@ -175,7 +176,7 @@ illustrates a complete example including mapper and session configuration::
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str]
create_date: Mapped[datetime.datetime] = mapped_column(server_default=func.now())
- bs: Mapped[List[B]] = relationship(lazy="raise")
+ bs: Mapped[List[B]] = relationship()
class B(Base):
@@ -225,6 +226,11 @@ illustrates a complete example including mapper and session configuration::
# expire_on_commit=False allows
print(a1.data)
+ # alternatively, AsyncAttrs may be used to access any attribute
+ # as an awaitable (new in 2.0.13)
+ for b1 in await a1.awaitable_attrs.bs:
+ print(b1)
+
async def async_main() -> None:
engine = create_async_engine(
@@ -269,6 +275,64 @@ Using traditional asyncio, the application needs to avoid any points at which
IO-on-attribute access may occur. Techniques that can be used to help
this are below, many of which are illustrated in the preceding example.
+* Attributes that are lazy-loading relationships, deferred columns or
+ expressions, or are being accessed in expiration scenarios can take advantage
+ of the :class:`_asyncio.AsyncAttrs` mixin. This mixin, when added to a
+ specific class or more generally to the Declarative ``Base`` superclass,
+ provides an accessor :attr:`_asyncio.AsyncAttrs.awaitable_attrs`
+ which delivers any attribute as an awaitable::
+
+ from __future__ import annotations
+
+ from typing import List
+
+ from sqlalchemy.ext.asyncio import AsyncAttrs
+ from sqlalchemy.orm import DeclarativeBase
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import relationship
+
+
+ class Base(AsyncAttrs, DeclarativeBase):
+ pass
+
+
+ class A(Base):
+ __tablename__ = "a"
+
+ # ... rest of mapping ...
+
+ bs: Mapped[List[B]] = relationship()
+
+
+ class B(Base):
+ __tablename__ = "b"
+
+ # ... rest of mapping ...
+
+ Accessing the ``A.bs`` collection on newly loaded instances of ``A`` when
+ eager loading is not in use will normally use :term:`lazy loading`, which in
+ order to succeed will usually emit IO to the database, which will fail under
+ asyncio as no implicit IO is allowed. To access this attribute directly under
+ asyncio without any prior loading operations, the attribute can be accessed
+ as an awaitable by indicating the :attr:`_asyncio.AsyncAttrs.awaitable_attrs`
+ prefix::
+
+ a1 = await (session.scalars(select(A))).one()
+ for b1 in await a1.awaitable_attrs.bs:
+ print(b1)
+
+ The :class:`_asyncio.AsyncAttrs` mixin provides a succinct facade over the
+ internal approach that's also used by the
+ :meth:`_asyncio.AsyncSession.run_sync` method.
+
+
+ .. versionadded:: 2.0.13
+
+ .. seealso::
+
+ :class:`_asyncio.AsyncAttrs`
+
+
* Collections can be replaced with **write only collections** that will never
emit IO implicitly, by using the :ref:`write_only_relationship` feature in
SQLAlchemy 2.0. Using this feature, collections are never read from, only
@@ -283,10 +347,9 @@ this are below, many of which are illustrated in the preceding example.
bullets below address specific techniques when using traditional lazy-loaded
relationships with asyncio, which requires more care.
-* If using traditional ORM relationships which are subject to lazy loading,
- relationships can be declared with ``lazy="raise"`` so that by
- default they will not attempt to emit SQL. In order to load collections,
- :term:`eager loading` must be used in all cases.
+* If not using :class:`_asyncio.AsyncAttrs`, relationships can be declared
+ with ``lazy="raise"`` so that by default they will not attempt to emit SQL.
+ In order to load collections, :term:`eager loading` would be used instead.
* The most useful eager loading strategy is the
:func:`_orm.selectinload` eager loader, which is employed in the previous
@@ -1019,6 +1082,9 @@ ORM Session API Documentation
:members:
:inherited-members:
+.. autoclass:: AsyncAttrs
+ :members:
+
.. autoclass:: AsyncSession
:members:
:exclude-members: sync_session_class
diff --git a/examples/asyncio/async_orm.py b/examples/asyncio/async_orm.py
index 66501e545..eabc0250d 100644
--- a/examples/asyncio/async_orm.py
+++ b/examples/asyncio/async_orm.py
@@ -12,15 +12,18 @@ from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.future import select
-from sqlalchemy.orm import declarative_base
+from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
-Base = declarative_base()
+
+class Base(AsyncAttrs, DeclarativeBase):
+ pass
class A(Base):
@@ -31,7 +34,7 @@ class A(Base):
create_date: Mapped[datetime.datetime] = mapped_column(
server_default=func.now()
)
- bs: Mapped[List[B]] = relationship(lazy="raise")
+ bs: Mapped[List[B]] = relationship()
class B(Base):
@@ -93,11 +96,15 @@ async def async_main():
result = await session.scalars(select(A).order_by(A.id))
- a1 = result.first()
+ a1 = result.one()
a1.data = "new data"
await session.commit()
+ # use the AsyncAttrs interface to accommodate for a lazy load
+ for b1 in await a1.awaitable_attrs.bs:
+ print(b1)
+
asyncio.run(async_main())
diff --git a/examples/asyncio/async_orm_writeonly.py b/examples/asyncio/async_orm_writeonly.py
index cdc486524..263c0d291 100644
--- a/examples/asyncio/async_orm_writeonly.py
+++ b/examples/asyncio/async_orm_writeonly.py
@@ -11,15 +11,18 @@ from typing import Optional
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.future import select
-from sqlalchemy.orm import declarative_base
+from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import WriteOnlyMapped
-Base = declarative_base()
+
+class Base(AsyncAttrs, DeclarativeBase):
+ pass
class A(Base):
diff --git a/examples/asyncio/greenlet_orm.py b/examples/asyncio/greenlet_orm.py
index 7429b6853..92880b992 100644
--- a/examples/asyncio/greenlet_orm.py
+++ b/examples/asyncio/greenlet_orm.py
@@ -10,13 +10,16 @@ from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import String
+from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
-from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.future import select
+from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import relationship
-Base = declarative_base()
+
+class Base(AsyncAttrs, DeclarativeBase):
+ pass
class A(Base):
diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py
index e9f94f1d9..0bf16401c 100644
--- a/lib/sqlalchemy/__init__.py
+++ b/lib/sqlalchemy/__init__.py
@@ -79,6 +79,7 @@ from .schema import PrimaryKeyConstraint as PrimaryKeyConstraint
from .schema import Sequence as Sequence
from .schema import Table as Table
from .schema import UniqueConstraint as UniqueConstraint
+from .sql import ColumnExpressionArgument as ColumnExpressionArgument
from .sql import SelectLabelStyle as SelectLabelStyle
from .sql.expression import Alias as Alias
from .sql.expression import alias as alias
@@ -264,7 +265,7 @@ from .types import Uuid as Uuid
from .types import VARBINARY as VARBINARY
from .types import VARCHAR as VARCHAR
-__version__ = "2.0.12"
+__version__ = "2.0.13"
def __go(lcls: Any) -> None:
diff --git a/lib/sqlalchemy/dialects/mysql/reflection.py b/lib/sqlalchemy/dialects/mysql/reflection.py
index ec3f82a60..ce1b9261d 100644
--- a/lib/sqlalchemy/dialects/mysql/reflection.py
+++ b/lib/sqlalchemy/dialects/mysql/reflection.py
@@ -146,11 +146,8 @@ class MySQLTableDefinitionParser:
options = {}
- if not line or line == ")":
- pass
-
- else:
- rest_of_line = line[:]
+ if line and line != ")":
+ rest_of_line = line
for regex, cleanup in self._pr_options:
m = regex.search(rest_of_line)
if not m:
@@ -310,7 +307,7 @@ class MySQLTableDefinitionParser:
comment = spec.get("comment", None)
if comment is not None:
- comment = comment.replace("\\\\", "\\").replace("''", "'")
+ comment = cleanup_text(comment)
sqltext = spec.get("generated")
if sqltext is not None:
@@ -585,11 +582,7 @@ class MySQLTableDefinitionParser:
re.escape(directive),
self._optional_equals,
)
- self._pr_options.append(
- _pr_compile(
- regex, lambda v: v.replace("\\\\", "\\").replace("''", "'")
- )
- )
+ self._pr_options.append(_pr_compile(regex, cleanup_text))
def _add_option_word(self, directive):
regex = r"(?P<directive>%s)%s" r"(?P<val>\w+)" % (
@@ -652,3 +645,28 @@ def _strip_values(values):
a = a[1:-1].replace(a[0] * 2, a[0])
strip_values.append(a)
return strip_values
+
+
+def cleanup_text(raw_text: str) -> str:
+ if "\\" in raw_text:
+ raw_text = re.sub(
+ _control_char_regexp, lambda s: _control_char_map[s[0]], raw_text
+ )
+ return raw_text.replace("''", "'")
+
+
+_control_char_map = {
+ "\\\\": "\\",
+ "\\0": "\0",
+ "\\a": "\a",
+ "\\b": "\b",
+ "\\t": "\t",
+ "\\n": "\n",
+ "\\v": "\v",
+ "\\f": "\f",
+ "\\r": "\r",
+ # '\\e':'\e',
+}
+_control_char_regexp = re.compile(
+ "|".join(re.escape(k) for k in _control_char_map)
+)
diff --git a/lib/sqlalchemy/dialects/postgresql/asyncpg.py b/lib/sqlalchemy/dialects/postgresql/asyncpg.py
index c879205e4..3f33600f9 100644
--- a/lib/sqlalchemy/dialects/postgresql/asyncpg.py
+++ b/lib/sqlalchemy/dialects/postgresql/asyncpg.py
@@ -182,6 +182,7 @@ from .base import PGExecutionContext
from .base import PGIdentifierPreparer
from .base import REGCLASS
from .base import REGCONFIG
+from .types import BYTEA
from ... import exc
from ... import pool
from ... import util
@@ -212,6 +213,10 @@ class AsyncpgTime(sqltypes.Time):
render_bind_cast = True
+class AsyncpgByteA(BYTEA):
+ render_bind_cast = True
+
+
class AsyncpgDate(sqltypes.Date):
render_bind_cast = True
@@ -986,6 +991,7 @@ class PGDialect_asyncpg(PGDialect):
sqltypes.Numeric: AsyncpgNumeric,
sqltypes.Float: AsyncpgFloat,
sqltypes.JSON: AsyncpgJSON,
+ sqltypes.LargeBinary: AsyncpgByteA,
json.JSONB: AsyncpgJSONB,
sqltypes.JSON.JSONPathType: AsyncpgJSONPathType,
sqltypes.JSON.JSONIndexType: AsyncpgJSONIndexType,
diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py
index da781334a..95789ddba 100644
--- a/lib/sqlalchemy/engine/row.py
+++ b/lib/sqlalchemy/engine/row.py
@@ -39,8 +39,8 @@ else:
if TYPE_CHECKING:
from .result import _KeyType
- from .result import RMKeyView
from .result import _ProcessorsType
+ from .result import RMKeyView
_T = TypeVar("_T", bound=Any)
_TP = TypeVar("_TP", bound=Tuple[Any, ...])
diff --git a/lib/sqlalchemy/ext/asyncio/__init__.py b/lib/sqlalchemy/ext/asyncio/__init__.py
index 7195e1f07..ad6cd1526 100644
--- a/lib/sqlalchemy/ext/asyncio/__init__.py
+++ b/lib/sqlalchemy/ext/asyncio/__init__.py
@@ -19,5 +19,6 @@ from .scoping import async_scoped_session as async_scoped_session
from .session import async_object_session as async_object_session
from .session import async_session as async_session
from .session import async_sessionmaker as async_sessionmaker
+from .session import AsyncAttrs as AsyncAttrs
from .session import AsyncSession as AsyncSession
from .session import AsyncSessionTransaction as AsyncSessionTransaction
diff --git a/lib/sqlalchemy/ext/asyncio/session.py b/lib/sqlalchemy/ext/asyncio/session.py
index d819f546c..00fee9716 100644
--- a/lib/sqlalchemy/ext/asyncio/session.py
+++ b/lib/sqlalchemy/ext/asyncio/session.py
@@ -8,6 +8,7 @@ from __future__ import annotations
import asyncio
from typing import Any
+from typing import Awaitable
from typing import Callable
from typing import Dict
from typing import Generic
@@ -73,6 +74,99 @@ _EXECUTE_OPTIONS = util.immutabledict({"prebuffer_rows": True})
_STREAM_OPTIONS = util.immutabledict({"stream_results": True})
+class AsyncAttrs:
+ """Mixin class which provides an awaitable accessor for all attributes.
+
+ E.g.::
+
+ from __future__ import annotations
+
+ from typing import List
+
+ from sqlalchemy import ForeignKey
+ from sqlalchemy import func
+ from sqlalchemy.ext.asyncio import AsyncAttrs
+ from sqlalchemy.orm import DeclarativeBase
+ from sqlalchemy.orm import Mapped
+ from sqlalchemy.orm import mapped_column
+ from sqlalchemy.orm import relationship
+
+
+ class Base(AsyncAttrs, DeclarativeBase):
+ pass
+
+
+ class A(Base):
+ __tablename__ = "a"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ data: Mapped[str]
+ bs: Mapped[List[B]] = relationship()
+
+
+ class B(Base):
+ __tablename__ = "b"
+ id: Mapped[int] = mapped_column(primary_key=True)
+ a_id: Mapped[int] = mapped_column(ForeignKey("a.id"))
+ data: Mapped[str]
+
+ In the above example, the :class:`_asyncio.AsyncAttrs` mixin is applied to
+ the declarative ``Base`` class where it takes effect for all subclasses.
+ This mixin adds a single new attribute
+ :attr:`_asyncio.AsyncAttrs.awaitable_attrs` to all classes, which will
+ yield the value of any attribute as an awaitable. This allows attributes
+ which may be subject to lazy loading or deferred / unexpiry loading to be
+ accessed such that IO can still be emitted::
+
+ a1 = (await async_session.scalars(select(A).where(A.id == 5))).one()
+
+ # use the lazy loader on ``a1.bs`` via the ``.async_attrs``
+ # interface, so that it may be awaited
+ for b1 in await a1.async_attrs.bs:
+ print(b1)
+
+ The :attr:`_asyncio.AsyncAttrs.awaitable_attrs` performs a call against the
+ attribute that is approximately equivalent to using the
+ :meth:`_asyncio.AsyncSession.run_sync` method, e.g.::
+
+ for b1 in await async_session.run_sync(lambda sess: a1.bs):
+ print(b1)
+
+ .. versionadded:: 2.0.13
+
+ .. seealso::
+
+ :ref:`asyncio_orm_avoid_lazyloads`
+
+ """
+
+ class _AsyncAttrGetitem:
+ __slots__ = "_instance"
+
+ def __init__(self, _instance: Any):
+ self._instance = _instance
+
+ def __getattr__(self, name: str) -> Awaitable[Any]:
+ return greenlet_spawn(getattr, self._instance, name)
+
+ @property
+ def awaitable_attrs(self) -> AsyncAttrs._AsyncAttrGetitem:
+ """provide a namespace of all attributes on this object wrapped
+ as awaitables.
+
+ e.g.::
+
+
+ a1 = (await async_session.scalars(select(A).where(A.id == 5))).one()
+
+ some_attribute = await a1.async_attrs.some_deferred_attribute
+ some_collection = await a1.async_attrs.some_collection
+
+ """ # noqa: E501
+
+ return AsyncAttrs._AsyncAttrGetitem(self)
+
+
@util.create_proxy_methods(
Session,
":class:`_orm.Session`",
@@ -268,7 +362,7 @@ class AsyncSession(ReversibleProxy[Session]):
to the database connection by running the given callable in a
specially instrumented greenlet.
- .. note::
+ .. tip::
The provided callable is invoked inline within the asyncio event
loop, and will block on traditional IO calls. IO within this
@@ -277,6 +371,9 @@ class AsyncSession(ReversibleProxy[Session]):
.. seealso::
+ :class:`.AsyncAttrs` - a mixin for ORM mapped classes that provides
+ a similar feature more succinctly on a per-attribute basis
+
:meth:`.AsyncConnection.run_sync`
:ref:`session_run_sync`
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 69ddd3388..6a979219c 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -84,6 +84,9 @@ from ..sql import cache_key
from ..sql import coercions
from ..sql import roles
from ..sql import visitors
+from ..sql.cache_key import HasCacheKey
+from ..sql.visitors import _TraverseInternalsType
+from ..sql.visitors import InternalTraversal
from ..util.typing import Literal
from ..util.typing import Self
from ..util.typing import TypeGuard
@@ -326,13 +329,16 @@ class QueryableAttribute(
# non-string keys.
# ideally Proxy() would have a separate set of methods to deal
# with this case.
+ entity_namespace = self._entity_namespace
+ assert isinstance(entity_namespace, HasCacheKey)
+
if self.key is _UNKNOWN_ATTR_KEY: # type: ignore[comparison-overlap]
- annotations = {"entity_namespace": self._entity_namespace}
+ annotations = {"entity_namespace": entity_namespace}
else:
annotations = {
"proxy_key": self.key,
"proxy_owner": self._parententity,
- "entity_namespace": self._entity_namespace,
+ "entity_namespace": entity_namespace,
}
ce = self.comparator.__clause_element__()
@@ -558,13 +564,21 @@ class InstrumentedAttribute(QueryableAttribute[_T]):
@dataclasses.dataclass(frozen=True)
-class AdHocHasEntityNamespace:
+class AdHocHasEntityNamespace(HasCacheKey):
+ _traverse_internals: ClassVar[_TraverseInternalsType] = [
+ ("_entity_namespace", InternalTraversal.dp_has_cache_key),
+ ]
+
# py37 compat, no slots=True on dataclass
- __slots__ = ("entity_namespace",)
- entity_namespace: _ExternalEntityType[Any]
+ __slots__ = ("_entity_namespace",)
+ _entity_namespace: _InternalEntityType[Any]
is_mapper: ClassVar[bool] = False
is_aliased_class: ClassVar[bool] = False
+ @property
+ def entity_namespace(self):
+ return self._entity_namespace.entity_namespace
+
def create_proxied_attribute(
descriptor: Any,
@@ -638,7 +652,7 @@ def create_proxied_attribute(
else:
# used by hybrid attributes which try to remain
# agnostic of any ORM concepts like mappers
- return AdHocHasEntityNamespace(self.class_)
+ return AdHocHasEntityNamespace(self._parententity)
@property
def property(self):
diff --git a/lib/sqlalchemy/orm/bulk_persistence.py b/lib/sqlalchemy/orm/bulk_persistence.py
index cb416d69e..257d71db4 100644
--- a/lib/sqlalchemy/orm/bulk_persistence.py
+++ b/lib/sqlalchemy/orm/bulk_persistence.py
@@ -586,6 +586,7 @@ class ORMDMLState(AbstractORMCompileState):
load_options = execution_options.get(
"_sa_orm_load_options", QueryContext.default_load_options
)
+
querycontext = QueryContext(
compile_state.from_statement_ctx,
compile_state.select_statement,
@@ -1140,6 +1141,7 @@ class BulkORMInsert(ORMDMLState, InsertDMLState):
_return_defaults: bool = False
_subject_mapper: Optional[Mapper[Any]] = None
_autoflush: bool = True
+ _populate_existing: bool = False
select_statement: Optional[FromStatement] = None
@@ -1159,7 +1161,7 @@ class BulkORMInsert(ORMDMLState, InsertDMLState):
execution_options,
) = BulkORMInsert.default_insert_options.from_execution_options(
"_sa_orm_insert_options",
- {"dml_strategy", "autoflush"},
+ {"dml_strategy", "autoflush", "populate_existing"},
execution_options,
statement._execution_options,
)
@@ -1284,6 +1286,15 @@ class BulkORMInsert(ORMDMLState, InsertDMLState):
if not bool(statement._returning):
return result
+ if insert_options._populate_existing:
+ load_options = execution_options.get(
+ "_sa_orm_load_options", QueryContext.default_load_options
+ )
+ load_options += {"_populate_existing": True}
+ execution_options = execution_options.union(
+ {"_sa_orm_load_options": load_options}
+ )
+
return cls._return_orm_returning(
session,
statement,
diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py
index 2f8289acf..a3a817162 100644
--- a/lib/sqlalchemy/orm/decl_api.py
+++ b/lib/sqlalchemy/orm/decl_api.py
@@ -666,7 +666,7 @@ class DeclarativeBase(
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase
- bigint = Annotated(int, "bigint")
+ bigint = Annotated[int, "bigint"]
my_metadata = MetaData()
class Base(DeclarativeBase):
diff --git a/lib/sqlalchemy/orm/path_registry.py b/lib/sqlalchemy/orm/path_registry.py
index e7084fbf6..e1cbd9313 100644
--- a/lib/sqlalchemy/orm/path_registry.py
+++ b/lib/sqlalchemy/orm/path_registry.py
@@ -119,6 +119,8 @@ class PathRegistry(HasCacheKey):
is_property = False
is_entity = False
+ is_unnatural: bool
+
path: _PathRepresentation
natural_path: _PathRepresentation
parent: Optional[PathRegistry]
@@ -510,7 +512,12 @@ class PropRegistry(PathRegistry):
# given MapperProperty's parent.
insp = cast("_InternalEntityType[Any]", parent[-1])
natural_parent: AbstractEntityRegistry = parent
- self.is_unnatural = False
+
+ # inherit "is_unnatural" from the parent
+ if parent.parent.is_unnatural:
+ self.is_unnatural = True
+ else:
+ self.is_unnatural = False
if not insp.is_aliased_class or insp._use_mapper_path: # type: ignore
parent = natural_parent = parent.parent[prop.parent]
@@ -570,6 +577,7 @@ class PropRegistry(PathRegistry):
self.parent = parent
self.path = parent.path + (prop,)
self.natural_path = natural_parent.natural_path + (prop,)
+
self.has_entity = prop._links_to_entity
if prop._is_relationship:
if TYPE_CHECKING:
@@ -674,7 +682,6 @@ class AbstractEntityRegistry(CreatesToken):
# elif not parent.path and self.is_aliased_class:
# self.natural_path = (self.entity._generate_cache_key()[0], )
else:
- # self.natural_path = parent.natural_path + (entity, )
self.natural_path = self.path
def _truncate_recursive(self) -> AbstractEntityRegistry:
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 5581e5c7f..8e06c4f59 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -1063,6 +1063,7 @@ class LazyLoader(
if extra_options:
stmt._with_options += extra_options
+
stmt._compile_options += {"_current_path": effective_path}
if use_get:
diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
index 48e69aef2..2e073f326 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -927,7 +927,9 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
) -> Optional[_PathRepresentation]:
i = -1
- for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)):
+ for i, (c_token, p_token) in enumerate(
+ zip(to_chop, path.natural_path)
+ ):
if isinstance(c_token, str):
if i == 0 and c_token.endswith(f":{_DEFAULT_TOKEN}"):
return to_chop
@@ -942,36 +944,8 @@ class _AbstractLoad(traversals.GenerativeOnTraversal, LoaderOption):
elif (
isinstance(c_token, InspectionAttr)
and insp_is_mapper(c_token)
- and (
- (insp_is_mapper(p_token) and c_token.isa(p_token))
- or (
- # a too-liberal check here to allow a path like
- # A->A.bs->B->B.cs->C->C.ds, natural path, to chop
- # against current path
- # A->A.bs->B(B, B2)->B(B, B2)->cs, in an of_type()
- # scenario which should only be occurring in a loader
- # that is against a non-aliased lead element with
- # single path. otherwise the
- # "B" won't match into the B(B, B2).
- #
- # i>=2 prevents this check from proceeding for
- # the first path element.
- #
- # if we could do away with the "natural_path"
- # concept, we would not need guessy checks like this
- #
- # two conflicting tests for this comparison are:
- # test_eager_relations.py->
- # test_lazyload_aliased_abs_bcs_two
- # and
- # test_of_type.py->test_all_subq_query
- #
- i >= 2
- and insp_is_aliased_class(p_token)
- and p_token._is_with_polymorphic
- and c_token in p_token.with_polymorphic_mappers
- )
- )
+ and insp_is_mapper(p_token)
+ and c_token.isa(p_token)
):
continue
@@ -1321,7 +1295,7 @@ class _WildcardLoad(_AbstractLoad):
strategy: Optional[Tuple[Any, ...]]
local_opts: _OptsType
- path: Tuple[str, ...]
+ path: Union[Tuple[()], Tuple[str]]
propagate_to_loaders = False
def __init__(self) -> None:
@@ -1366,6 +1340,7 @@ class _WildcardLoad(_AbstractLoad):
it may be used as the sub-option of a :class:`_orm.Load` object.
"""
+ assert self.path
attr = self.path[0]
if attr.endswith(_DEFAULT_TOKEN):
attr = f"{attr.split(':')[0]}:{_WILDCARD_TOKEN}"
@@ -1396,13 +1371,16 @@ class _WildcardLoad(_AbstractLoad):
start_path: _PathRepresentation = self.path
- # TODO: chop_path already occurs in loader.process_compile_state()
- # so we will seek to simplify this
if current_path:
+ # TODO: no cases in test suite where we actually get
+ # None back here
new_path = self._chop_path(start_path, current_path)
- if not new_path:
+ if new_path is None:
return
- start_path = new_path
+
+ # chop_path does not actually "chop" a wildcard token path,
+ # just returns it
+ assert new_path == start_path
# start_path is a single-token tuple
assert start_path and len(start_path) == 1
@@ -1618,7 +1596,9 @@ class _LoadElement(
"""
- chopped_start_path = Load._chop_path(effective_path.path, current_path)
+ chopped_start_path = Load._chop_path(
+ effective_path.natural_path, current_path
+ )
if not chopped_start_path:
return None
diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py
index e5bbe65da..96cffed7d 100644
--- a/lib/sqlalchemy/sql/__init__.py
+++ b/lib/sqlalchemy/sql/__init__.py
@@ -7,6 +7,7 @@
from typing import Any
from typing import TYPE_CHECKING
+from ._typing import ColumnExpressionArgument as ColumnExpressionArgument
from .base import Executable as Executable
from .compiler import COLLECT_CARTESIAN_PRODUCTS as COLLECT_CARTESIAN_PRODUCTS
from .compiler import FROM_LINTING as FROM_LINTING
diff --git a/lib/sqlalchemy/sql/_typing.py b/lib/sqlalchemy/sql/_typing.py
index 596493b7c..9e83c3f42 100644
--- a/lib/sqlalchemy/sql/_typing.py
+++ b/lib/sqlalchemy/sql/_typing.py
@@ -26,6 +26,7 @@ from .. import util
from ..inspection import Inspectable
from ..util.typing import Literal
from ..util.typing import Protocol
+from ..util.typing import TypeAlias
if TYPE_CHECKING:
from datetime import date
@@ -175,7 +176,10 @@ _ColumnExpressionArgument = Union[
Callable[[], "ColumnElement[_T]"],
"LambdaElement",
]
-"""narrower "column expression" argument.
+"See docs in public alias ColumnExpressionArgument."
+
+ColumnExpressionArgument: TypeAlias = _ColumnExpressionArgument[_T]
+"""Narrower "column expression" argument.
This type is used for all the other "column" kinds of expressions that
typically represent a single SQL column expression, not a set of columns the
@@ -184,10 +188,8 @@ way a table or ORM entity does.
This includes ColumnElement, or ORM-mapped attributes that will have a
`__clause_element__()` method, it also has the ExpressionElementRole
overall which brings in the TextClause object also.
-
"""
-
_ColumnExpressionOrLiteralArgument = Union[Any, _ColumnExpressionArgument[_T]]
_ColumnExpressionOrStrLabelArgument = Union[str, _ColumnExpressionArgument[_T]]
diff --git a/lib/sqlalchemy/testing/fixtures.py b/lib/sqlalchemy/testing/fixtures.py
index fc1fa1483..bff251b0f 100644
--- a/lib/sqlalchemy/testing/fixtures.py
+++ b/lib/sqlalchemy/testing/fixtures.py
@@ -13,6 +13,7 @@ import itertools
import random
import re
import sys
+from typing import Any
import sqlalchemy as sa
from . import assertions
@@ -675,7 +676,7 @@ class MappedTest(TablesTest, assertions.AssertsExecutionResults):
# 'once', 'each', None
run_setup_mappers = "each"
- classes = None
+ classes: Any = None
@config.fixture(autouse=True, scope="class")
def _setup_tables_test_class(self):
diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py
index 7286cd81b..a2a9dc822 100644
--- a/lib/sqlalchemy/testing/requirements.py
+++ b/lib/sqlalchemy/testing/requirements.py
@@ -656,6 +656,13 @@ class SuiteRequirements(Requirements):
return exclusions.closed()
@property
+ def comment_reflection_full_unicode(self):
+ """Indicates if the database support table comment reflection in the
+ full unicode range, including emoji etc.
+ """
+ return exclusions.closed()
+
+ @property
def constraint_comment_reflection(self):
"""indicates if the database support constraint on constraints
and their reflection"""
diff --git a/lib/sqlalchemy/testing/suite/test_insert.py b/lib/sqlalchemy/testing/suite/test_insert.py
index d49eb3284..391503422 100644
--- a/lib/sqlalchemy/testing/suite/test_insert.py
+++ b/lib/sqlalchemy/testing/suite/test_insert.py
@@ -430,6 +430,8 @@ class ReturningTest(fixtures.TablesTest):
this tests insertmanyvalues as well as decimal / floating point
RETURNING types
+ TODO: this might be better in suite/test_types?
+
"""
t = Table(
diff --git a/lib/sqlalchemy/testing/suite/test_reflection.py b/lib/sqlalchemy/testing/suite/test_reflection.py
index 8c26c265b..0162799d7 100644
--- a/lib/sqlalchemy/testing/suite/test_reflection.py
+++ b/lib/sqlalchemy/testing/suite/test_reflection.py
@@ -564,6 +564,7 @@ class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest):
sa.String(20),
comment=r"""Comment types type speedily ' " \ '' Fun!""",
),
+ Column("d3", sa.String(42), comment="Comment\nwith\rescapes"),
schema=schema,
comment=r"""the test % ' " \ table comment""",
)
@@ -572,6 +573,7 @@ class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest):
metadata,
Column("data", sa.String(20)),
schema=schema,
+ comment="no\nconstraints\rhas\fescaped\vcomment",
)
if testing.requires.cross_schema_fk_reflection.enabled:
@@ -831,7 +833,9 @@ class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest):
(schema, "comment_test"): {
"text": r"""the test % ' " \ table comment"""
},
- (schema, "no_constraints"): empty,
+ (schema, "no_constraints"): {
+ "text": "no\nconstraints\rhas\fescaped\vcomment"
+ },
(schema, "local_table"): empty,
(schema, "remote_table"): empty,
(schema, "remote_table_2"): empty,
@@ -921,6 +925,7 @@ class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest):
"d2",
comment=r"""Comment types type speedily ' " \ '' Fun!""",
),
+ col("d3", comment="Comment\nwith\rescapes"),
],
(schema, "no_constraints"): [col("data")],
(schema, "local_table"): [pk("id"), col("data"), col("remote_id")],
@@ -2294,6 +2299,45 @@ class ComponentReflectionTest(ComparesTables, OneConnectionTablesTest):
tables = [f"{schema}.{t}" for t in tables]
eq_(sorted(m.tables), sorted(tables))
+ @testing.requires.comment_reflection
+ def test_comments_unicode(self, connection, metadata):
+ Table(
+ "unicode_comments",
+ metadata,
+ Column("unicode", Integer, comment="é試蛇ẟΩ"),
+ Column("emoji", Integer, comment="☁️✨"),
+ comment="試蛇ẟΩ✨",
+ )
+
+ metadata.create_all(connection)
+
+ insp = inspect(connection)
+ tc = insp.get_table_comment("unicode_comments")
+ eq_(tc, {"text": "試蛇ẟΩ✨"})
+
+ cols = insp.get_columns("unicode_comments")
+ value = {c["name"]: c["comment"] for c in cols}
+ exp = {"unicode": "é試蛇ẟΩ", "emoji": "☁️✨"}
+ eq_(value, exp)
+
+ @testing.requires.comment_reflection_full_unicode
+ def test_comments_unicode_full(self, connection, metadata):
+
+ Table(
+ "unicode_comments",
+ metadata,
+ Column("emoji", Integer, comment="🐍🧙🝝🧙‍♂️🧙‍♀️"),
+ comment="🎩🁰🝑🤷‍♀️🤷‍♂️",
+ )
+
+ metadata.create_all(connection)
+
+ insp = inspect(connection)
+ tc = insp.get_table_comment("unicode_comments")
+ eq_(tc, {"text": "🎩🁰🝑🤷‍♀️🤷‍♂️"})
+ c = insp.get_columns("unicode_comments")[0]
+ eq_({c["name"]: c["comment"]}, {"emoji": "🐍🧙🝝🧙‍♂️🧙‍♀️"})
+
class TableNoColumnsTest(fixtures.TestBase):
__requires__ = ("reflect_tables_no_columns",)
diff --git a/lib/sqlalchemy/testing/suite/test_types.py b/lib/sqlalchemy/testing/suite/test_types.py
index 72f1e8c10..ba2dda9ef 100644
--- a/lib/sqlalchemy/testing/suite/test_types.py
+++ b/lib/sqlalchemy/testing/suite/test_types.py
@@ -26,6 +26,7 @@ from ... import cast
from ... import Date
from ... import DateTime
from ... import Float
+from ... import Identity
from ... import Integer
from ... import JSON
from ... import literal
@@ -45,6 +46,7 @@ from ... import Unicode
from ... import UnicodeText
from ... import UUID
from ... import Uuid
+from ...dialects.postgresql import BYTEA
from ...orm import declarative_base
from ...orm import Session
from ...sql.sqltypes import LargeBinary
@@ -313,6 +315,68 @@ class BinaryTest(_LiteralRoundTripFixture, fixtures.TablesTest):
row = connection.execute(select(binary_table.c.pickle_data)).first()
eq_(row, ({"foo": [1, 2, 3], "bar": "bat"},))
+ @testing.combinations(
+ (
+ LargeBinary(),
+ b"this is binary",
+ ),
+ (LargeBinary(), b"7\xe7\x9f"),
+ (BYTEA(), b"7\xe7\x9f", testing.only_on("postgresql")),
+ argnames="type_,value",
+ )
+ @testing.variation("sort_by_parameter_order", [True, False])
+ @testing.variation("multiple_rows", [True, False])
+ @testing.requires.insert_returning
+ def test_imv_returning(
+ self,
+ connection,
+ metadata,
+ sort_by_parameter_order,
+ type_,
+ value,
+ multiple_rows,
+ ):
+ """test #9739 (similar to #9701).
+
+ this tests insertmanyvalues as well as binary
+ RETURNING types
+
+ """
+ t = Table(
+ "t",
+ metadata,
+ Column("id", Integer, Identity(), primary_key=True),
+ Column("value", type_),
+ )
+
+ t.create(connection)
+
+ result = connection.execute(
+ t.insert().returning(
+ t.c.id,
+ t.c.value,
+ sort_by_parameter_order=bool(sort_by_parameter_order),
+ ),
+ [{"value": value} for i in range(10)]
+ if multiple_rows
+ else {"value": value},
+ )
+
+ if multiple_rows:
+ i_range = range(1, 11)
+ else:
+ i_range = range(1, 2)
+
+ eq_(
+ set(result),
+ {(id_, value) for id_ in i_range},
+ )
+
+ eq_(
+ set(connection.scalars(select(t.c.value))),
+ {value},
+ )
+
class TextTest(_LiteralRoundTripFixture, fixtures.TablesTest):
__requires__ = ("text_type",)
diff --git a/lib/sqlalchemy/util/typing.py b/lib/sqlalchemy/util/typing.py
index 9c38ae344..3ac67aad9 100644
--- a/lib/sqlalchemy/util/typing.py
+++ b/lib/sqlalchemy/util/typing.py
@@ -146,7 +146,7 @@ def de_stringify_annotation(
original_annotation = annotation
- if is_fwd_ref(annotation) and not annotation.__forward_evaluated__:
+ if is_fwd_ref(annotation):
annotation = annotation.__forward_arg__
if isinstance(annotation, str):
diff --git a/test/dialect/mysql/test_reflection.py b/test/dialect/mysql/test_reflection.py
index f9975a973..a75c05f09 100644
--- a/test/dialect/mysql/test_reflection.py
+++ b/test/dialect/mysql/test_reflection.py
@@ -1368,6 +1368,29 @@ class ReflectionTest(fixtures.TestBase, AssertsCompiledSQL):
},
)
+ def test_reflect_comment_escapes(self, connection, metadata):
+ c = "\\ - \\\\ - \\0 - \\a - \\b - \\t - \\n - \\v - \\f - \\r"
+ Table("t", metadata, Column("c", Integer, comment=c), comment=c)
+ metadata.create_all(connection)
+
+ insp = inspect(connection)
+ tc = insp.get_table_comment("t")
+ eq_(tc, {"text": c})
+ col = insp.get_columns("t")[0]
+ eq_({col["name"]: col["comment"]}, {"c": c})
+
+ def test_reflect_comment_unicode(self, connection, metadata):
+ c = "☁️✨🐍🁰🝝"
+ c_exp = "☁️✨???"
+ Table("t", metadata, Column("c", Integer, comment=c), comment=c)
+ metadata.create_all(connection)
+
+ insp = inspect(connection)
+ tc = insp.get_table_comment("t")
+ eq_(tc, {"text": c_exp})
+ col = insp.get_columns("t")[0]
+ eq_({col["name"]: col["comment"]}, {"c": c_exp})
+
class RawReflectionTest(fixtures.TestBase):
def setup_test(self):
diff --git a/test/ext/asyncio/test_session_py3k.py b/test/ext/asyncio/test_session_py3k.py
index 36135a43d..a374728f7 100644
--- a/test/ext/asyncio/test_session_py3k.py
+++ b/test/ext/asyncio/test_session_py3k.py
@@ -1,20 +1,31 @@
+from __future__ import annotations
+
+from typing import List
+from typing import Optional
+
from sqlalchemy import Column
from sqlalchemy import event
from sqlalchemy import exc
from sqlalchemy import ForeignKey
from sqlalchemy import func
+from sqlalchemy import Identity
from sqlalchemy import inspect
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy import Sequence
+from sqlalchemy import String
from sqlalchemy import Table
from sqlalchemy import testing
from sqlalchemy import update
from sqlalchemy.ext.asyncio import async_object_session
from sqlalchemy.ext.asyncio import async_sessionmaker
+from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import exc as async_exc
from sqlalchemy.ext.asyncio.base import ReversibleProxy
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
@@ -24,6 +35,7 @@ from sqlalchemy.testing import config
from sqlalchemy.testing import engines
from sqlalchemy.testing import eq_
from sqlalchemy.testing import expect_raises_message
+from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
from sqlalchemy.testing import is_true
from sqlalchemy.testing import mock
@@ -45,6 +57,12 @@ class AsyncFixture(_AsyncFixture, _fixtures.FixtureTest):
def async_engine(self):
return engines.testing_engine(asyncio=True, transfer_staticpool=True)
+ # TODO: this seems to cause deadlocks in
+ # OverrideSyncSession for some reason
+ # @testing.fixture
+ # def async_engine(self, async_testing_engine):
+ # return async_testing_engine(transfer_staticpool=True)
+
@testing.fixture
def async_session(self, async_engine):
return AsyncSession(async_engine)
@@ -1005,3 +1023,103 @@ class OverrideSyncSession(AsyncFixture):
is_true(not isinstance(ass.sync_session, _MySession))
is_(ass.sync_session_class, Session)
+
+
+class AsyncAttrsTest(
+ testing.AssertsExecutionResults, _AsyncFixture, fixtures.TestBase
+):
+ __requires__ = ("async_dialect",)
+
+ @config.fixture
+ def decl_base(self, metadata):
+ _md = metadata
+
+ class Base(fixtures.ComparableEntity, AsyncAttrs, DeclarativeBase):
+ metadata = _md
+ type_annotation_map = {
+ str: String().with_variant(
+ String(50), "mysql", "mariadb", "oracle"
+ )
+ }
+
+ yield Base
+ Base.registry.dispose()
+
+ @testing.fixture
+ def async_engine(self, async_testing_engine):
+ yield async_testing_engine(transfer_staticpool=True)
+
+ @testing.fixture
+ def ab_fixture(self, decl_base):
+ class A(decl_base):
+ __tablename__ = "a"
+
+ id: Mapped[int] = mapped_column(Identity(), primary_key=True)
+ data: Mapped[Optional[str]]
+ bs: Mapped[List[B]] = relationship(order_by=lambda: B.id)
+
+ class B(decl_base):
+ __tablename__ = "b"
+ id: Mapped[int] = mapped_column(Identity(), primary_key=True)
+ a_id: Mapped[int] = mapped_column(ForeignKey("a.id"))
+ data: Mapped[Optional[str]]
+
+ decl_base.metadata.create_all(testing.db)
+
+ return A, B
+
+ @async_test
+ async def test_lazyloaders(self, async_engine, ab_fixture):
+ A, B = ab_fixture
+
+ async with AsyncSession(async_engine) as session:
+ b1, b2, b3 = B(data="b1"), B(data="b2"), B(data="b3")
+ a1 = A(data="a1", bs=[b1, b2, b3])
+ session.add(a1)
+
+ await session.commit()
+
+ assert inspect(a1).expired
+
+ with self.assert_statement_count(async_engine.sync_engine, 1):
+ eq_(await a1.awaitable_attrs.data, "a1")
+
+ with self.assert_statement_count(async_engine.sync_engine, 1):
+ eq_(await a1.awaitable_attrs.bs, [b1, b2, b3])
+
+ # now it's loaded, lazy loading not used anymore
+ eq_(a1.bs, [b1, b2, b3])
+
+ @async_test
+ async def test_it_didnt_load_but_is_ok(self, async_engine, ab_fixture):
+ A, B = ab_fixture
+
+ async with AsyncSession(async_engine) as session:
+ b1, b2, b3 = B(data="b1"), B(data="b2"), B(data="b3")
+ a1 = A(data="a1", bs=[b1, b2, b3])
+ session.add(a1)
+
+ await session.commit()
+
+ async with AsyncSession(async_engine) as session:
+ a1 = (
+ await session.scalars(select(A).options(selectinload(A.bs)))
+ ).one()
+
+ with self.assert_statement_count(async_engine.sync_engine, 0):
+ eq_(await a1.awaitable_attrs.bs, [b1, b2, b3])
+
+ @async_test
+ async def test_the_famous_lazyloader_gotcha(
+ self, async_engine, ab_fixture
+ ):
+ A, B = ab_fixture
+
+ async with AsyncSession(async_engine) as session:
+ a1 = A(data="a1")
+ session.add(a1)
+
+ await session.flush()
+
+ with self.assert_statement_count(async_engine.sync_engine, 1):
+ eq_(await a1.awaitable_attrs.bs, [])
diff --git a/test/orm/dml/test_bulk_statements.py b/test/orm/dml/test_bulk_statements.py
index ab03b251d..ca4e857ed 100644
--- a/test/orm/dml/test_bulk_statements.py
+++ b/test/orm/dml/test_bulk_statements.py
@@ -302,6 +302,78 @@ class InsertStmtTest(testing.AssertsExecutionResults, fixtures.TestBase):
else:
eq_(result.first(), (10, expected_qs[0]))
+ @testing.variation("populate_existing", [True, False])
+ @testing.requires.provisioned_upsert
+ @testing.requires.update_returning
+ def test_upsert_populate_existing(self, decl_base, populate_existing):
+ """test #9742"""
+
+ class Employee(ComparableEntity, decl_base):
+ __tablename__ = "employee"
+
+ uuid: Mapped[uuid.UUID] = mapped_column(primary_key=True)
+ user_name: Mapped[str] = mapped_column(nullable=False)
+
+ decl_base.metadata.create_all(testing.db)
+ s = fixture_session()
+
+ uuid1 = uuid.uuid4()
+ uuid2 = uuid.uuid4()
+ e1 = Employee(uuid=uuid1, user_name="e1 old name")
+ e2 = Employee(uuid=uuid2, user_name="e2 old name")
+ s.add_all([e1, e2])
+ s.flush()
+
+ stmt = provision.upsert(
+ config,
+ Employee,
+ (Employee,),
+ set_lambda=lambda inserted: {"user_name": inserted.user_name},
+ ).values(
+ [
+ dict(uuid=uuid1, user_name="e1 new name"),
+ dict(uuid=uuid2, user_name="e2 new name"),
+ ]
+ )
+ if populate_existing:
+ rows = s.scalars(
+ stmt, execution_options={"populate_existing": True}
+ )
+ # SPECIAL: before we actually receive the returning rows,
+ # the existing objects have not been updated yet
+ eq_(e1.user_name, "e1 old name")
+ eq_(e2.user_name, "e2 old name")
+
+ eq_(
+ set(rows),
+ {
+ Employee(uuid=uuid1, user_name="e1 new name"),
+ Employee(uuid=uuid2, user_name="e2 new name"),
+ },
+ )
+
+ # now they are updated
+ eq_(e1.user_name, "e1 new name")
+ eq_(e2.user_name, "e2 new name")
+ else:
+ # no populate existing
+ rows = s.scalars(stmt)
+ eq_(e1.user_name, "e1 old name")
+ eq_(e2.user_name, "e2 old name")
+ eq_(
+ set(rows),
+ {
+ Employee(uuid=uuid1, user_name="e1 old name"),
+ Employee(uuid=uuid2, user_name="e2 old name"),
+ },
+ )
+ eq_(e1.user_name, "e1 old name")
+ eq_(e2.user_name, "e2 old name")
+ s.commit()
+ s.expire_all()
+ eq_(e1.user_name, "e1 new name")
+ eq_(e2.user_name, "e2 new name")
+
class UpdateStmtTest(fixtures.TestBase):
__backend__ = True
diff --git a/test/orm/inheritance/test_assorted_poly.py b/test/orm/inheritance/test_assorted_poly.py
index 4bebc9b10..a40a9ae74 100644
--- a/test/orm/inheritance/test_assorted_poly.py
+++ b/test/orm/inheritance/test_assorted_poly.py
@@ -3,6 +3,10 @@ These are generally tests derived from specific user issues.
"""
+from __future__ import annotations
+
+from typing import Optional
+
from sqlalchemy import exists
from sqlalchemy import ForeignKey
from sqlalchemy import func
@@ -17,10 +21,14 @@ from sqlalchemy.orm import aliased
from sqlalchemy.orm import class_mapper
from sqlalchemy.orm import column_property
from sqlalchemy.orm import contains_eager
+from sqlalchemy.orm import immediateload
from sqlalchemy.orm import join
from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import polymorphic_union
from sqlalchemy.orm import relationship
+from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import with_polymorphic
@@ -2554,3 +2562,206 @@ class Issue8168Test(AssertsCompiledSQL, fixtures.TestBase):
)
else:
scenario.fail()
+
+
+class PolyIntoSelfReferentialTest(
+ fixtures.DeclarativeMappedTest, AssertsExecutionResults
+):
+ """test for #9715"""
+
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ class A(Base):
+ __tablename__ = "a"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, autoincrement=True
+ )
+
+ rel_id: Mapped[int] = mapped_column(ForeignKey("related.id"))
+
+ related = relationship("Related")
+
+ class Related(Base):
+ __tablename__ = "related"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, autoincrement=True
+ )
+ rel_data: Mapped[str]
+ type: Mapped[str] = mapped_column()
+
+ other_related_id: Mapped[int] = mapped_column(
+ ForeignKey("other_related.id")
+ )
+
+ other_related = relationship("OtherRelated")
+
+ __mapper_args__ = {
+ "polymorphic_identity": "related",
+ "polymorphic_on": type,
+ }
+
+ class SubRelated(Related):
+ __tablename__ = "sub_related"
+
+ id: Mapped[int] = mapped_column(
+ ForeignKey("related.id"), primary_key=True
+ )
+ sub_rel_data: Mapped[str]
+
+ __mapper_args__ = {"polymorphic_identity": "sub_related"}
+
+ class OtherRelated(Base):
+ __tablename__ = "other_related"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, autoincrement=True
+ )
+ name: Mapped[str]
+
+ parent_id: Mapped[Optional[int]] = mapped_column(
+ ForeignKey("other_related.id")
+ )
+ parent = relationship("OtherRelated", lazy="raise", remote_side=id)
+
+ @classmethod
+ def insert_data(cls, connection):
+ A, SubRelated, OtherRelated = cls.classes(
+ "A", "SubRelated", "OtherRelated"
+ )
+
+ with Session(connection) as sess:
+
+ grandparent_otherrel1 = OtherRelated(name="GP1")
+ grandparent_otherrel2 = OtherRelated(name="GP2")
+
+ parent_otherrel1 = OtherRelated(
+ name="P1", parent=grandparent_otherrel1
+ )
+ parent_otherrel2 = OtherRelated(
+ name="P2", parent=grandparent_otherrel2
+ )
+
+ otherrel1 = OtherRelated(name="A1", parent=parent_otherrel1)
+ otherrel3 = OtherRelated(name="A2", parent=parent_otherrel2)
+
+ address1 = SubRelated(
+ rel_data="ST1", other_related=otherrel1, sub_rel_data="w1"
+ )
+ address3 = SubRelated(
+ rel_data="ST2", other_related=otherrel3, sub_rel_data="w2"
+ )
+
+ a1 = A(related=address1)
+ a2 = A(related=address3)
+
+ sess.add_all([a1, a2])
+ sess.commit()
+
+ def _run_load(self, *opt):
+ A = self.classes.A
+ stmt = select(A).options(*opt)
+
+ sess = fixture_session()
+ all_a = sess.scalars(stmt).all()
+
+ sess.close()
+
+ with self.assert_statement_count(testing.db, 0):
+ for a1 in all_a:
+ d1 = a1.related
+ d2 = d1.other_related
+ d3 = d2.parent
+ d4 = d3.parent
+ assert d4.name in ("GP1", "GP2")
+
+ @testing.variation("use_workaround", [True, False])
+ def test_workaround(self, use_workaround):
+ A, Related, SubRelated, OtherRelated = self.classes(
+ "A", "Related", "SubRelated", "OtherRelated"
+ )
+
+ related = with_polymorphic(Related, [SubRelated], flat=True)
+
+ opt = [
+ (
+ joinedload(A.related.of_type(related))
+ .joinedload(related.other_related)
+ .joinedload(
+ OtherRelated.parent,
+ )
+ )
+ ]
+ if use_workaround:
+ opt.append(
+ joinedload(
+ A.related,
+ Related.other_related,
+ OtherRelated.parent,
+ OtherRelated.parent,
+ )
+ )
+ else:
+ opt[0] = opt[0].joinedload(OtherRelated.parent)
+
+ self._run_load(*opt)
+
+ @testing.combinations(
+ (("joined", "joined", "joined", "joined"),),
+ (("selectin", "selectin", "selectin", "selectin"),),
+ (("selectin", "selectin", "joined", "joined"),),
+ (("selectin", "selectin", "joined", "selectin"),),
+ (("joined", "selectin", "joined", "selectin"),),
+ # TODO: immediateload (and lazyload) do not support the target item
+ # being a with_polymorphic. this seems to be a limitation in the
+ # current_path logic
+ # (("immediate", "joined", "joined", "joined"),),
+ argnames="loaders",
+ )
+ @testing.variation("use_wpoly", [True, False])
+ def test_all_load(self, loaders, use_wpoly):
+ A, Related, SubRelated, OtherRelated = self.classes(
+ "A", "Related", "SubRelated", "OtherRelated"
+ )
+
+ if use_wpoly:
+ related = with_polymorphic(Related, [SubRelated], flat=True)
+ else:
+ related = SubRelated
+
+ opt = None
+ for i, (load_type, element) in enumerate(
+ zip(
+ loaders,
+ [
+ A.related.of_type(related),
+ related.other_related,
+ OtherRelated.parent,
+ OtherRelated.parent,
+ ],
+ )
+ ):
+ if i == 0:
+ if load_type == "joined":
+ opt = joinedload(element)
+ elif load_type == "selectin":
+ opt = selectinload(element)
+ elif load_type == "immediate":
+ opt = immediateload(element)
+ else:
+ assert False
+ else:
+ assert opt is not None
+ if load_type == "joined":
+ opt = opt.joinedload(element)
+ elif load_type == "selectin":
+ opt = opt.selectinload(element)
+ elif load_type == "immediate":
+ opt = opt.immediateload(element)
+ else:
+ assert False
+
+ self._run_load(opt)
diff --git a/test/orm/test_cache_key.py b/test/orm/test_cache_key.py
index f1c3e6a54..884baed62 100644
--- a/test/orm/test_cache_key.py
+++ b/test/orm/test_cache_key.py
@@ -2,6 +2,7 @@ import random
import sqlalchemy as sa
from sqlalchemy import Column
+from sqlalchemy import column
from sqlalchemy import func
from sqlalchemy import inspect
from sqlalchemy import Integer
@@ -16,6 +17,7 @@ from sqlalchemy import true
from sqlalchemy import update
from sqlalchemy import util
from sqlalchemy.ext.declarative import ConcreteBase
+from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import aliased
from sqlalchemy.orm import Bundle
from sqlalchemy.orm import defaultload
@@ -785,6 +787,55 @@ class PolyCacheKeyTest(fixtures.CacheKeyFixture, _poly_fixtures._Polymorphic):
compare_values=True,
)
+ @testing.variation(
+ "exprtype", ["plain_column", "self_standing_case", "case_w_columns"]
+ )
+ def test_hybrid_w_case_ac(self, decl_base, exprtype):
+ """test #9728"""
+
+ class Employees(decl_base):
+ __tablename__ = "employees"
+ id = Column(String(128), primary_key=True)
+ first_name = Column(String(length=64))
+
+ @hybrid_property
+ def name(self):
+ return self.first_name
+
+ @name.expression
+ def name(
+ cls,
+ ):
+ if exprtype.plain_column:
+ return cls.first_name
+ elif exprtype.self_standing_case:
+ return case(
+ (column("x") == 1, column("q")),
+ else_=column("q"),
+ )
+ elif exprtype.case_w_columns:
+ return case(
+ (column("x") == 1, column("q")),
+ else_=cls.first_name,
+ )
+ else:
+ exprtype.fail()
+
+ def go1():
+ employees_2 = aliased(Employees, name="employees_2")
+ stmt = select(employees_2.name)
+ return stmt
+
+ def go2():
+ employees_2 = aliased(Employees, name="employees_2")
+ stmt = select(employees_2)
+ return stmt
+
+ self._run_cache_key_fixture(
+ lambda: stmt_20(go1(), go2()),
+ compare_values=True,
+ )
+
class RoundTripTest(QueryTest, AssertsCompiledSQL):
__dialect__ = "default"
diff --git a/test/profiles.txt b/test/profiles.txt
index fd229ed03..5bdd5ac07 100644
--- a/test/profiles.txt
+++ b/test/profiles.txt
@@ -20,8 +20,8 @@ test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.
test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_cextensions 75
test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 75
test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 75
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 75
-test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 75
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 77
+test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 77
test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 75
test.aaa_profiling.test_compiler.CompileTest.test_insert x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 75
@@ -34,8 +34,8 @@ test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.
test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_cextensions 195
test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 195
test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 195
-test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 195
-test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 195
+test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 219
+test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 219
test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 193
test.aaa_profiling.test_compiler.CompileTest.test_select x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 193
@@ -48,8 +48,8 @@ test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpy
test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_cextensions 219
test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 219
test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 219
-test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 219
-test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 219
+test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 243
+test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 243
test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 217
test.aaa_profiling.test_compiler.CompileTest.test_select_labels x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 217
@@ -62,8 +62,8 @@ test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_cextensions 81
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 81
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 81
-test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 81
-test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 81
+test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 86
+test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 86
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 79
test.aaa_profiling.test_compiler.CompileTest.test_update x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 79
@@ -76,8 +76,8 @@ test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linu
test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_cextensions 180
test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 180
test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 180
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 180
-test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 180
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 184
+test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 187
test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 180
test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 180
@@ -328,8 +328,8 @@ test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 53
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 53
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 53
-test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 53
-test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 53
+test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 55
+test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 55
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 53
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_connection_execute x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 53
@@ -343,8 +343,8 @@ test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 106
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 106
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 106
-test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 106
-test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 106
+test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 110
+test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 110
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 105
test.aaa_profiling.test_resultset.ExecutionTest.test_minimal_engine_execute x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 105
@@ -373,8 +373,8 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_6
test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 15601
test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 2637
test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 15641
-test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 2592
-test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 25595
+test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 2651
+test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 14655
test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 2539
test.aaa_profiling.test_resultset.ResultSetTest.test_fetch_by_key_mappings x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 14614
@@ -404,7 +404,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 14
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 16
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 14
-test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 16
+test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 15
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 14
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-1] x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 15
@@ -419,7 +419,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 14
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 16
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 14
-test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 16
+test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 15
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 14
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[False-2] x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 15
@@ -434,7 +434,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 17
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 19
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 17
-test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 19
+test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 18
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 17
test.aaa_profiling.test_resultset.ResultSetTest.test_one_or_none[True-1] x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 18
@@ -448,8 +448,8 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpy
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 6269
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 361
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 6361
-test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 291
-test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 6291
+test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 301
+test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 5301
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 257
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_string x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 5277
@@ -463,8 +463,8 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cp
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 6269
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 361
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 6361
-test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 291
-test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 6291
+test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 301
+test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 5301
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 257
test.aaa_profiling.test_resultset.ResultSetTest.test_raw_unicode x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 5277
@@ -478,8 +478,8 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython
test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 6594
test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 630
test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 6634
-test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 585
-test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 6589
+test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 642
+test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 5646
test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 532
test.aaa_profiling.test_resultset.ResultSetTest.test_string x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 5605
@@ -493,7 +493,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpytho
test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_mssql_pyodbc_dbapiunicode_nocextensions 6594
test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_cextensions 630
test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_oracle_cx_oracle_dbapiunicode_nocextensions 6634
-test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 585
-test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 6589
+test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_cextensions 642
+test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_postgresql_psycopg2_dbapiunicode_nocextensions 5646
test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_cextensions 532
test.aaa_profiling.test_resultset.ResultSetTest.test_unicode x86_64_linux_cpython_3.10_sqlite_pysqlite_dbapiunicode_nocextensions 5605
diff --git a/test/requirements.py b/test/requirements.py
index 2023428b8..436f7d3d7 100644
--- a/test/requirements.py
+++ b/test/requirements.py
@@ -167,6 +167,10 @@ class DefaultRequirements(SuiteRequirements):
return only_on(["postgresql", "mysql", "mariadb", "oracle", "mssql"])
@property
+ def comment_reflection_full_unicode(self):
+ return only_on(["postgresql", "oracle", "mssql"])
+
+ @property
def constraint_comment_reflection(self):
return only_on(["postgresql"])