diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-07-21 15:44:27 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-07-21 18:02:02 -0400 |
| commit | 8822d832679cdc13bb631dd221d0f5932f6540c7 (patch) | |
| tree | 1d91be9f3c6843df368bb89cfa894d9678a14ce1 /lib/sqlalchemy | |
| parent | 54ca40b8b3e4286183b64198573b55731b1ce363 (diff) | |
| download | sqlalchemy-8822d832679cdc13bb631dd221d0f5932f6540c7.tar.gz | |
Apply new uniquing rules for future ORM selects
Fixed issue where usage of the :meth:`_result.Result.unique` method with an
ORM result that included column expressions with unhashable types, such as
``JSON`` or ``ARRAY`` using non-tuples would silently fall back to using
the ``id()`` function, rather than raising an error. This now raises an
error when the :meth:`_result.Result.unique` method is used in a 2.0 style
ORM query. Additionally, hashability is assumed to be True for result
values of unknown type, such as often happens when using SQL functions of
unknown return type; if values are truly not hashable then the ``hash()``
itself will raise.
For legacy ORM queries, since the legacy :class:`_orm.Query` object
uniquifies in all cases, the old rules remain in place, which is to use
``id()`` for result values of unknown type as this legacy uniquing is
mostly for the purpose of uniquing ORM entities and not column values.
Fixes: #6769
Change-Id: I5747f706f1e97c78867b5cf28c73360497273808
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/orm/context.py | 13 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/loading.py | 36 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 2 |
4 files changed, 45 insertions, 10 deletions
diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 0af3fd6af..c4b695687 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -81,6 +81,7 @@ class QueryContext(object): _yield_per = None _refresh_state = None _lazy_loaded_from = None + _legacy_uniquing = False def __init__( self, @@ -2257,6 +2258,10 @@ class _QueryEntity(object): __slots__ = () + _non_hashable_value = False + _null_column_type = False + use_id_for_hash = False + @classmethod def to_compile_state(cls, compile_state, entities, entities_collection): @@ -2387,6 +2392,7 @@ class _MapperEntity(_QueryEntity): supports_single_entity = True + _non_hashable_value = True use_id_for_hash = True @property @@ -2483,7 +2489,6 @@ class _MapperEntity(_QueryEntity): class _BundleEntity(_QueryEntity): - use_id_for_hash = False _extra_entities = () @@ -2663,9 +2668,13 @@ class _ColumnEntity(_QueryEntity): return self.column.type @property - def use_id_for_hash(self): + def _non_hashable_value(self): return not self.column.type.hashable + @property + def _null_column_type(self): + return self.column.type._isnull + def row_processor(self, context, result): compile_state = context.compile_state diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index 948f33ad5..abc8780ed 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -92,17 +92,43 @@ def instances(cursor, context): "Can't use the ORM yield_per feature in conjunction with unique()" ) - row_metadata = SimpleResultMetaData( - labels, - extra, - _unique_filters=[ + def _not_hashable(datatype): + def go(obj): + raise sa_exc.InvalidRequestError( + "Can't apply uniqueness to row tuple containing value of " + "type %r; this datatype produces non-hashable values" + % datatype + ) + + return go + + if context.load_options._legacy_uniquing: + unique_filters = [ + _no_unique + if context.yield_per + else id + if ( + ent.use_id_for_hash + or ent._non_hashable_value + or ent._null_column_type + ) + else None + for ent in context.compile_state._entities + ] + else: + unique_filters = [ _no_unique if context.yield_per + else _not_hashable(ent.column.type) + if (not ent.use_id_for_hash and ent._non_hashable_value) else id if ent.use_id_for_hash else None for ent in context.compile_state._entities - ], + ] + + row_metadata = SimpleResultMetaData( + labels, extra, _unique_filters=unique_filters ) def chunks(size): diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 6ad7f3020..9a97d37b0 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -130,7 +130,9 @@ class Query( _compile_options = ORMCompileState.default_compile_options - load_options = QueryContext.default_load_options + load_options = QueryContext.default_load_options + { + "_legacy_uniquing": True + } _params = util.EMPTY_DICT diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 1b05465c9..44431d38f 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -3184,8 +3184,6 @@ class NullType(TypeEngine): _isnull = True - hashable = False - def literal_processor(self, dialect): def process(value): raise exc.CompileError( |
