On 03/13/2017 08:34 AM, Simon King wrote:
Hi,

I'm not sure if the problem I've got is a bug or intended behaviour.
Here's a test script:


#########################################
import sqlalchemy as sa
import sqlalchemy.orm as saorm

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

ACTIVE_HISTORY = True

class Obj(Base):
    __tablename__ = 'obj'
    id = sa.Column(sa.Integer, primary_key=True)
    with_default = saorm.column_property(
        sa.Column(sa.String(), default='default'),
        active_history=ACTIVE_HISTORY,
    )
    with_server_default = saorm.column_property(
        sa.Column(sa.String(), server_default='server_default'),
        active_history=ACTIVE_HISTORY,
    )
    with_no_default = saorm.column_property(
        sa.Column(sa.String()),
        active_history=ACTIVE_HISTORY,
    )

if __name__ == '__main__':
    engine = sa.create_engine('sqlite://', echo='debug')
    Base.metadata.create_all(engine)
    session = saorm.sessionmaker(bind=engine)()
    o = Obj()
    session.add(o)
    session.flush()

    attrs = ['with_default', 'with_server_default', 'with_no_default']
    for attr in attrs:
        setattr(o, attr, 'new value')

    for attr in attrs:
        print '%20s: %s' % (attr, saorm.attributes.get_history(o, attr))

#########################################

And here's the output:

        with_default: History(added=['new value'], unchanged=(),
deleted=['default'])
 with_server_default: History(added=['new value'], unchanged=(),
deleted=[u'server_default'])
     with_no_default: History(added=['new value'], unchanged=(), deleted=())

The last line is the one I have a problem with. I'm trying to write a
library module that uses the before_flush event to log (before, after)
tuples for every change to certain tables. I had an assertion that the
"added" and "deleted" values were both lists of length 1 (because I'm
only looking at scalar columns), but the assertion was failing in some
cases. I think I tracked it down to the above case of an object that
was flushed without an explicit default having been assigned.

In the SQL output I can see that the with_no_default column was
explicitly inserted as NULL, and it has active_history=True, so I
can't see a reason why the "deleted" part of the history is empty. Is
this a bug, or an invalid assumption by me? As a workaround, is it
safe to assume that any empty "deleted" values mean that the column
was NULL (given active history, and that the value has actually
changed)?

so thanks for the great test case, and yes, this behavior is weird. What's happening is that because you've just created this instance and did an INSERT, the unit of work did not set up Obj.with_no_default as "expired" after the flush, because the object has no server-side value. It also didn't put anything in the object's __dict__ because nothing was set. The unit of work did decide to insert NULL for this value, but that NULL doesn't relate back to the attributes on the object; there's a lot of expectation here of people inserting objects into tables with lots of columns and they only care about a small part of them, so we try not to emphasize populating columns that the user doesn't seem to be looking at anyway.

Anyway, because the column isn't "expired", we assume there's no database-related value waiting for it. When the active history flag tells the attribute system to go and get the "old" value, it determines this value was "NEVER_SET". It puts this value into state.committed_state, so the view of all the "old" values looks like:

(Pdb) state.committed_state
{'with_server_default': u'server_default', 'with_default': 'default', 'with_no_default': symbol('NEVER_SET')}


when you call get_history(), the NEVER_SET symbol is one that means, "this attribute never had a value", so you get the blank tuple for "deleted". The attribute system tries to not expose these internal symbols to the front-facing API because it means end-user code has to anticipate them, and it makes backwards compatibility more of a challenge when we change the symbols around.

So to sum up, since you never set anything on Obj() for this attribute, and it has no database-side default, the answer it gives you is, "this attribute never had a value". Which is "right" from the object perspective, not exactly accurate from the database perspective. So this is the intended behavior. So yeah I think you can interpret () as "was never set" from the ORM's perspective.









Thanks a lot,

Simon


--
SQLAlchemy - The Python SQL Toolkit and Object Relational Mapper

http://www.sqlalchemy.org/

To post example code, please provide an MCVE: Minimal, Complete, and Verifiable 
Example.  See  http://stackoverflow.com/help/mcve for a full description.
--- You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.

Reply via email to