On 09/20/2016 09:13 AM, Chris Wilson wrote:
Dear Michael,
I have been trying to add row versioning to our existing multi-table
polymorphism setup and I think I may have found an issue that I hope you
can help with.
hi Chris -
thanks for the clear test case, this is very helpful.
So you're doing the xmin thing which was a newer use case I got the
library to support once someone asked that it be possible. As you
noticed, there are no values to actually UPDATE. If we hit an attribute
on the base table as well as the child table, then we see two UPDATEs
like we expect:
UPDATE animal SET species=%(species)s WHERE animal.id = %(animal_id)s
AND animal.xmin = %(animal_xmin)s RETURNING animal.xmin
2016-09-20 09:28:06,700 INFO sqlalchemy.engine.base.Engine
{'animal_xmin': '547653', 'animal_id': 1, 'species': 'asdf'}
2016-09-20 09:28:06,701 INFO sqlalchemy.engine.base.Engine UPDATE dog
SET toy=%(toy)s WHERE dog.id_animal = %(dog_id_animal)s
2016-09-20 09:28:06,701 INFO sqlalchemy.engine.base.Engine {'toy':
'Bone', 'dog_id_animal': 1}
the problem is in the absence of any columns on the base table, we still
need to bump "xmin" but there's no values to put into the UPDATE, except
the set of a column to itself which is of course something the ORM never
does normally.
The only thing I could find on google about this, which refers to
setting a value to itself at
http://dba.stackexchange.com/questions/40815/is-there-better-way-to-touch-a-row-than-performing-a-dummy-update,
only concludes that it's better to use a distinct version counter, which
is how I'd do it also. I believe the XMIN thing here someone wanted so
that a particular schema need not be changed.
To add a dummy "id=id", one way is to just intercept UPDATE statements
that have no SET clause and to just add this in (using the
before_execute() event). To do this at the ORM is presenting more of a
challenge, mostly because this column is a primary key column which
doesn't support updating against a SQL expression at this level; issue
https://bitbucket.org/zzzeek/sqlalchemy/issues/3801/cant-use-sql-expression-for-pk-col
is added.
We can set a non-PK column to a SQL expression that is the same column
like this:
with session.begin():
dog.name = Animal.name.__clause_element__()
dog.toy = 'Bone'
Bugs here also include that I need to produce a ClauseElement directly
for persistence to pick up on it, I shouldn't need to call upon
__clause_element__() like that; issue
https://bitbucket.org/zzzeek/sqlalchemy/issues/3802/sql-expression-in-orm-update-only-look-for
is added.
That's the most immediate workaround. We can put that into an event:
from sqlalchemy import event
@event.listens_for(Dog, "before_update")
def upd(mapper, connection, obj):
# TODO: check for other columns already changed, check that "name"
# not already modified
obj.name = Animal.name.__clause_element__()
with session.begin():
dog.toy = 'Bone'
Above would be your solution for now (short of "use a normal version
column" which I would recommend :) ). I don't know that I want the ORM
to do this automatically just yet. I would prefer to just add more
docs to the "server versioned column" examples noting that you need to
do something like this for joined inheritance (once the bugs are fixed).
Note the point of setting to a SQL expression is to force the ORM to
actually include the column; if we set it to itself, it sees no net
change and it wouldn't work, unless you used a special value that didn't
naturally compare against itself.
The following example appears to generate invalid SQL when it tries to
update the base class table, but there are no columns to fetch; it just
wants to get the current version number:
from sqlalchemy import Column, FetchedValue, ForeignKey, Integer,
Text, create_engine
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.attributes import InstrumentedAttribute
Base = declarative_base()
class Animal(Base):
__tablename__ = 'animal'
id = Column(Integer, primary_key=True)
name = Column(Text)
species = Column(Text)
_version = Column("xmin", Integer,
server_default=FetchedValue(), server_onupdate=FetchedValue(),
system=True)
# _version = Column("xmin", Integer)
@declared_attr
def __mapper_args__(cls):
if isinstance(cls._version, InstrumentedAttribute):
version_col = cls._version.property.columns[0]
else:
version_col = cls._version
return {
'polymorphic_on': cls.species,
'polymorphic_identity': cls.__name__,
'version_id_col': version_col,
'version_id_generator': False,
}
class Dog(Animal):
__tablename__ = 'dog'
id_animal = Column(Integer, ForeignKey(Animal.id), primary_key=True)
toy = Column(Text)
engine = create_engine('postgresql://user@host/database')
engine.echo = True
Base.metadata.create_all(engine)
DBSession = sessionmaker(bind=engine)
session = DBSession(autocommit=True)
with session.begin():
dog = Dog(name="fido")
session.add(dog)
with session.begin():
dog.toy = 'Bone'
Which raises the following exception:
File sqlalchemy_issue_repro_2.py, line 59, in : dog.toy = 'Bone'
<file:\user!wilsoc!nexus!versioning!sqlalchemy_issue_repro_2.py:59:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 490, in __exit__ : self.rollback()
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:490:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\util\langhelpers.py,
line 60, in __exit__ : compat.reraise(exc_type, exc_value, exc_tb)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\util\langhelpers.py:60:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 487, in __exit__ : self.commit()
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:487:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 392, in commit : self._prepare_impl()
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:392:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 372, in _prepare_impl : self.session.flush()
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:372:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 2015, in flush : self._flush(objects)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:2015:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 2133, in _flush : transaction.rollback(_capture_exception=True)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:2133:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\util\langhelpers.py,
line 60, in __exit__ : compat.reraise(exc_type, exc_value, exc_tb)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\util\langhelpers.py:60:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py,
line 2097, in _flush : flush_context.execute()
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\session.py:2097:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\unitofwork.py,
line 373, in execute : rec.execute(self)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\unitofwork.py:373:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\unitofwork.py,
line 532, in execute : uow
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\unitofwork.py:532:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\persistence.py,
line 170, in save_obj : mapper, table, update)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\persistence.py:170:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\persistence.py,
line 672, in _emit_update_statements : execute(statement, params)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\orm\persistence.py:672:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py,
line 914, in execute : return meth(self, multiparams, params)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py:914:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\sql\elements.py,
line 323, in _execute_on_connection : return
connection._execute_clauseelement(self, multiparams, params)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\sql\elements.py:323:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py,
line 1010, in _execute_clauseelement : compiled_sql,
distilled_params
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py:1010:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py,
line 1146, in _execute_context : context)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py:1146:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py,
line 1341, in _handle_dbapi_exception : exc_info
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py:1341:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\util\compat.py,
line 199, in raise_from_cause : reraise(type(exception), exception,
tb=exc_tb)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\util\compat.py:199:exception>
File sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py,
line 1139, in _execute_context : context)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\base.py:1139:exception>
File
sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\default.py,
line 450, in do_execute : cursor.execute(statement, parameters)
<file:\R:\sw\external\20160915-0-64\python27\lib\site-packages\sqlalchemy-1.0.9-py2.7-win-amd64.egg\sqlalchemy\engine\default.py:450:exception>
ProgrammingError: (psycopg2.ProgrammingError) syntax error at or
near "WHERE"
LINE 1: UPDATE animal SET WHERE animal.id = 2 AND animal.xmin = '63...
^
[SQL: 'UPDATE animal SET WHERE animal.id = %(animal_id)s AND
animal.xmin = %(animal_xmin)s RETURNING animal.xmin'] [parameters:
{'animal_id': 2, 'animal_xmin': '635104190'}]
I think the problem is in sqlalchemy\orm\persistence.py, in
the _collect_update_commands() function. It tries to generate a dict of
columns that need updating, but none of the columns in the base table
have changed, so the dict is empty. Normally it would skip updating this
table here:
elif not (params or value_params):
continue
But because versioning is enabled, it takes this branch instead:
if update_version_id is not None and \
mapper.version_id_col in mapper._cols_by_table[table]:
and then tries to execute a null UPDATE just in order to get the new
version number, which fails.
It would work if we did a dummy update, for example assigning the PK to
itself, as this would change the version number (xmin) returned by the
server. Keeping the version number on the base table allows it to be
shared by all polymorphic subclasses.
Thanks in advance for your consideration,
Chris Wilson.
--
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
<mailto:sqlalchemy+unsubscr...@googlegroups.com>.
To post to this group, send email to sqlalchemy@googlegroups.com
<mailto:sqlalchemy@googlegroups.com>.
Visit this group at https://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/d/optout.
--
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.