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.

Reply via email to