ensure unhashable types work the same in 1.0 as in 0.9 and former
Before SA 1.0 I could have denormalized schemas where foreign keys pointed to unhashable types (let's say HSTORE). This was very common in same cases for me as I had for example denormalized translation columns using HSTORE as the underlying data type. Then foreign keys with onupdate='CASCADE' where used for automatic real-time denormalization when data changed. I created a simplified test case:
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.postgresql import HSTORE
engine = sa.create_engine(
'postgres://postgres@localhost/sqlalchemy_utils_test'
)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()
session.execute('CREATE EXTENSION IF NOT EXISTS hstore')
session.commit()
class Category(Base):
__tablename__ = 'category'
id = sa.Column(sa.Integer, primary_key=True)
data = sa.Column(HSTORE)
__table_args__ = (
sa.Index(
'some_index',
data,
id,
unique=True
),
)
class Article(Base):
__tablename__ = 'article'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
category_id = sa.Column(sa.Integer)
category_data = sa.Column(HSTORE)
category = sa.orm.relationship(Category)
__table_args__ = (
sa.ForeignKeyConstraint(
[category_data, category_id],
['category.data', 'category.id'],
onupdate='CASCADE'
),
)
Base.metadata.create_all(bind=session.bind)
article = Article(name='Some article', category=Category(data={'1': '2'}))
session.add(article)
session.commit()
category = Category(data={'2': '2'})
session.commit()
article.category = category
session.commit()
Base.metadata.drop_all(bind=session.bind)
throws Exception
...
elif orm_util._never_set.intersection(params.values()):
TypeError: unhashable type: 'dict'
The problem is even deeper than this as the following code block illustrates:
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.dialects.postgresql import HSTORE
engine = sa.create_engine(
'postgres://postgres@localhost/sqlalchemy_utils_test'
)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()
session.execute('CREATE EXTENSION IF NOT EXISTS hstore')
session.commit()
class Article(Base):
__tablename__ = 'article'
id = sa.Column(HSTORE, primary_key=True)
Base.metadata.create_all(bind=session.bind)
article = Article(id={'1': '2'})
session.add(article)
session.commit()
Base.metadata.drop_all(bind=session.bind)
Which throws exception:
Traceback (most recent call last):
File "sa_test.py", line 42, in <module>
session.commit()
File ".../sqlalchemy/orm/session.py", line 790, in commit
self.transaction.commit()
File ".../sqlalchemy/orm/session.py", line 392, in commit
self._prepare_impl()
File ".../sqlalchemy/orm/session.py", line 372, in _prepare_impl
self.session.flush()
File ".../sqlalchemy/orm/session.py", line 2004, in flush
self._flush(objects)
File ".../sqlalchemy/orm/session.py", line 2122, in _flush
transaction.rollback(_capture_exception=True)
File ".../sqlalchemy/util/langhelpers.py", line 60, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File ".../sqlalchemy/util/compat.py", line 182, in reraise
raise value
File ".../sqlalchemy/orm/session.py", line 2086, in _flush
flush_context.execute()
File ".../sqlalchemy/orm/unitofwork.py", line 373, in execute
rec.execute(self)
File ".../sqlalchemy/orm/unitofwork.py", line 532, in execute
uow
File ".../sqlalchemy/orm/persistence.py", line 149, in save_obj
base_mapper, states, uowtransaction
File ".../sqlalchemy/orm/persistence.py", line 292, in _organize_states_for_save
instance_key in uowtransaction.session.identity_map:
File ".../sqlalchemy/orm/identity.py", line 96, in __contains__
if key in self._dict:
TypeError: unhashable type: 'dict'
The second issue is not critical for me but the first is. I have many projects using HSTORE based translations and denormalization.
Comments (10)
-
repo owner -
repo owner - changed milestone to 1.0.4
- changed component to orm
- changed title to ensure unhashable types work the same in 1.0 as in 0.9 and former
-
repo owner second case: nothing new:
#! classics-MacBook-Pro:~ classic$ cd tmp/sa099 classics-MacBook-Pro:sa099 classic$ python ~/dev/sqlalchemy/test.py Traceback (most recent call last): File "/Users/classic/dev/sqlalchemy/test.py", line 28, in <module> session.commit() File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/session.py", line 788, in commit self.transaction.commit() File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/session.py", line 384, in commit self._prepare_impl() File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/session.py", line 364, in _prepare_impl self.session.flush() File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/session.py", line 1985, in flush self._flush(objects) File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/session.py", line 2103, in _flush transaction.rollback(_capture_exception=True) File "/Users/classic/tmp/sa099/lib/sqlalchemy/util/langhelpers.py", line 60, in __exit__ compat.reraise(exc_type, exc_value, exc_tb) File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/session.py", line 2067, in _flush flush_context.execute() File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/unitofwork.py", line 372, in execute rec.execute(self) File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/unitofwork.py", line 526, in execute uow File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/persistence.py", line 46, in save_obj uowtransaction) File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/persistence.py", line 162, in _organize_states_for_save instance_key in uowtransaction.session.identity_map: File "/Users/classic/tmp/sa099/lib/sqlalchemy/orm/identity.py", line 87, in __contains__ if dict.__contains__(self, key): TypeError: unhashable type: 'dict' classics-MacBook-Pro:sa099 classic$ cd ../ classics-MacBook-Pro:tmp classic$ cd sa084/ classics-MacBook-Pro:sa084 classic$ python ~/dev/sqlalchemy/test.py Traceback (most recent call last): File "/Users/classic/dev/sqlalchemy/test.py", line 28, in <module> session.commit() File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/session.py", line 721, in commit self.transaction.commit() File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/session.py", line 354, in commit self._prepare_impl() File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/session.py", line 334, in _prepare_impl self.session.flush() File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/session.py", line 1818, in flush self._flush(objects) File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/session.py", line 1936, in _flush transaction.rollback(_capture_exception=True) File "/Users/classic/tmp/sa084/lib/sqlalchemy/util/langhelpers.py", line 58, in __exit__ compat.reraise(exc_type, exc_value, exc_tb) File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/session.py", line 1900, in _flush flush_context.execute() File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/unitofwork.py", line 372, in execute rec.execute(self) File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/unitofwork.py", line 525, in execute uow File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/persistence.py", line 45, in save_obj uowtransaction) File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/persistence.py", line 161, in _organize_states_for_save instance_key in uowtransaction.session.identity_map: File "/Users/classic/tmp/sa084/lib/sqlalchemy/orm/identity.py", line 83, in __contains__ if dict.__contains__(self, key): TypeError: unhashable type: 'dict'
-
repo owner in 0.9, if we have a mapping like this:
class Article(Base): __tablename__ = 'article' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String) category_id = sa.Column(sa.Integer) category_data = sa.Column(HSTORE) category = sa.orm.relationship(Category, load_on_pending=True)
then load from pending:
article = Article(name='Some article', category_id=cat.id, category_data={"foo": "bar"}) session.add(article) print article.category
we hit the same logic that's failing here. in 0.9, it is this:
if pending: bind_values = sql_util.bind_values(lazy_clause) if None in bind_values: return None
so doesn't fail. in 1.0 because we're checking for more than None it's this:
if pending and orm_util._none_set.intersection(params.values()): return None elif orm_util._never_set.intersection(params.values()): return None
obviously if i change that to do a piecemeal check for each value, we are less efficient in the vast majority of cases. not too big a deal to fix though. relationship() on a dictionary type, pretty odd case (I'd never do it, but we can support it).
-
repo owner - changed status to resolved
- Fixed unexpected-use regression where in the odd case that the
primaryjoin of a relationship involved comparison to an unhashable
type such as an HSTORE, lazy loads would fail due to a hash-oriented
check on the statement parameters, modified in 1.0 as a result of
3061
to use hashing and modified in3368
to occur in cases more common than "load on pending". The values are now checked for the__hash__
attribute beforehand. fixes#3416
→ <<cset 1b120563905e>>
-
repo owner unhashable PK values is a much bigger deal, but also I'd favor that PK values always use types that are hashable, in the case of an HSTORE a custom type might need to implement something; such as returning as an immutabledict which is hashable.
-
reporter Thanks for very fast response Mike!
Also never mind the unhashable PKs (I thought it was a bug but I apparently didn't test it with 0.9 series). Could we make it fail faster though and with a better error message? We could check that the python types of the primary key column types are hashable.
from collections import Hashable isinstance(column.type.python_type, Hashable)
-
repo owner OK I'd kind of want that to be just a warning if its for 1.0.4. Tons of things broke in 1.0 and I really don't want any more surprises for people
-
repo owner but also 1.0.4 should be today maybe put that into 1.1 as a new issue? it's pretty low priority.
-
reporter Warning is perfectly ok or putting this as a new 1.1 issue.
- Log in to comment
OK that second issue, that is definitely not a new problem - we hash PKs in every SQLA version. Can you confirm that ? the first one with the _never_set(), that's fixable, we don't require that mapped classes or datatypes are hashable in general.