summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2021-07-21 15:44:27 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2021-07-21 18:02:02 -0400
commit8822d832679cdc13bb631dd221d0f5932f6540c7 (patch)
tree1d91be9f3c6843df368bb89cfa894d9678a14ce1 /lib/sqlalchemy
parent54ca40b8b3e4286183b64198573b55731b1ce363 (diff)
downloadsqlalchemy-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.py13
-rw-r--r--lib/sqlalchemy/orm/loading.py36
-rw-r--r--lib/sqlalchemy/orm/query.py4
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py2
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(