On Mon, Mar 13, 2017 at 1:50 PM, mike bayer <[email protected]> wrote: > > > 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. >
Excellent, thanks for the detailed answer. I wasn't at all sure that my workaround assumption was safe, but this all makes sense. Cheers, 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 [email protected]. To post to this group, send email to [email protected]. Visit this group at https://groups.google.com/group/sqlalchemy. For more options, visit https://groups.google.com/d/optout.
