new stack trace when combining non-eq compare_values w/ SQL expression in ORM persist

Issue #3402 resolved
Mike Bayer repo owner created an issue

this is a 1.0 regression, though I'm pretty curious to see what 0.9 is doing that it isn't hitting this exception:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

def comparator(a, b):
    return a > b

class A(Base):
    __tablename__ = 'a'
    id = Column(Integer, primary_key=True)
    data = Column(PickleType(comparator=comparator))


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)
s.add(A(data='some data'))
s.commit()

a1 = s.query(A).first()
a1.data = func.foo("im a SQL expression")
s.commit()
#!

Traceback (most recent call last):
  File "test.py", line 27, in <module>
    s.commit()
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 790, in commit
    self.transaction.commit()
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 392, in commit
    self._prepare_impl()
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 372, in _prepare_impl
    self.session.flush()
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 2004, in flush
    self._flush(objects)
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 2122, in _flush
    transaction.rollback(_capture_exception=True)
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/util/langhelpers.py", line 60, in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/session.py", line 2086, in _flush
    flush_context.execute()
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/unitofwork.py", line 373, in execute
    rec.execute(self)
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/unitofwork.py", line 532, in execute
    uow
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/persistence.py", line 170, in save_obj
    mapper, table, update)
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/persistence.py", line 613, in _emit_update_statements
    lambda rec: (
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/orm/persistence.py", line 456, in _collect_update_commands
    value, state.committed_state[propkey]):
  File "/Users/classic/dev/sqlalchemy/lib/sqlalchemy/sql/elements.py", line 2726, in __bool__
    raise TypeError("Boolean value of this clause is not defined")
TypeError: Boolean value of this clause is not defined

Comments (3)

  1. Mike Bayer reporter

    well if we have a SQL expression we know it has to be run, so in that case we skip the comparison. Confirm this is what 0.9 is doing and add a test to test_unitofworkv2:

    diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py
    index 34c37da..c3974ed 100644
    --- a/lib/sqlalchemy/orm/persistence.py
    +++ b/lib/sqlalchemy/orm/persistence.py
    @@ -452,12 +452,11 @@ def _collect_update_commands(
                     value = state_dict[propkey]
                     col = propkey_to_col[propkey]
    
    -                if not state.manager[propkey].impl.is_equal(
    +                if isinstance(value, sql.ClauseElement):
    +                    value_params[col] = value
    +                elif not state.manager[propkey].impl.is_equal(
                             value, state.committed_state[propkey]):
    -                    if isinstance(value, sql.ClauseElement):
    -                        value_params[col] = value
    -                    else:
    -                        params[col.key] = value
    +                    params[col.key] = value
    
             if update_version_id is not None and \
                     mapper.version_id_col in mapper._cols_by_table[table]:
    
  2. Mike Bayer reporter

    Here's the test case using something much more normal, occurs with boolean expressions, as the operator comes out as "is_()":

    from sqlalchemy import *
    from sqlalchemy.orm import *
    from sqlalchemy.ext.declarative import declarative_base
    
    Base = declarative_base()
    
    class A(Base):
        __tablename__ = 'a'
        id = Column(Integer, primary_key=True)
        data = Column(Boolean)
    
    e = create_engine("sqlite://", echo=True)
    Base.metadata.create_all(e)
    
    s = Session(e)
    s.add(A(data=None))
    s.commit()
    
    a1 = s.query(A).first()
    a1.data = false()
    s.commit()
    

    we might want to consider "is" and "isnot" OK for the __bool__ here also but I'm happy not getting into that for now.

  3. Mike Bayer reporter
    • Fixed regression within the flush process when an attribute were set to a SQL expression for an UPDATE, and the SQL expression when compared to the previous value of the attribute would produce a SQL comparison other than == or !=, the exception "Boolean value of this clause is not defined" would raise. The fix ensures that the unit of work will not interpret the SQL expression in this way. fixes #3402

    → <<cset b0be9211c9a2>>

  4. Log in to comment