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.
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 [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.