--- /dev/null
+.. change::
+ :tags: bug, examples
+ :tickets: 10267
+
+ Fixed issue in history_meta example where the "version" column in the
+ versioned table needs to default to the most recent version number in the
+ history table on INSERT, to suit the use case of a table where rows are
+ deleted, and can then be replaced by new rows that re-use the same primary
+ key identity. This fix adds an additonal SELECT query per INSERT in the
+ main table, which may be inefficient; for cases where primary keys are not
+ re-used, the default function may be omitted. Patch courtesy Philipp H.
+ v. Loewenfeld.
+
Compare to the :ref:`examples_versioned_rows` examples which write updates
as new rows in the same table, without using a separate history table.
-Usage is illustrated via a unit test module ``test_versioning.py``, which can
-be run like any other module, using ``unittest`` internally::
+Usage is illustrated via a unit test module ``test_versioning.py``, which is
+run using SQLAlchemy's internal pytest plugin::
- python -m examples.versioned_history.test_versioning
+ pytest test/base/test_examples.py
A fragment of example usage, using declarative::
import datetime
+from sqlalchemy import and_
from sqlalchemy import Column
from sqlalchemy import DateTime
from sqlalchemy import event
from sqlalchemy import ForeignKeyConstraint
+from sqlalchemy import func
from sqlalchemy import inspect
from sqlalchemy import Integer
from sqlalchemy import PrimaryKeyConstraint
+from sqlalchemy import select
from sqlalchemy import util
from sqlalchemy.orm import attributes
from sqlalchemy.orm import object_mapper
super_history_table.append_column(col)
if not super_mapper:
+
+ def default_version_from_history(context):
+ # Set default value of version column to the maximum of the
+ # version in history columns already present +1
+ # Otherwise re-appearance of deleted rows would cause an error
+ # with the next update
+ current_parameters = context.get_current_parameters()
+ return context.connection.scalar(
+ select(
+ func.coalesce(func.max(history_table.c.version), 0) + 1
+ ).where(
+ and_(
+ *[
+ history_table.c[c.name]
+ == current_parameters.get(c.name, None)
+ for c in inspect(
+ local_mapper.local_table
+ ).primary_key
+ ]
+ )
+ )
+ )
+
local_mapper.local_table.append_column(
- Column("version", Integer, default=1, nullable=False),
+ Column(
+ "version",
+ Integer,
+ # if rows are not being deleted from the main table with
+ # subsequent re-use of primary key, this default can be
+ # "1" instead of running a query per INSERT
+ default=default_version_from_history,
+ nullable=False,
+ ),
replace_existing=True,
)
local_mapper.add_property(
sc2.name = "sc2 modified"
sess.commit()
+ def test_external_id(self):
+ class ObjectExternal(Versioned, self.Base, ComparableEntity):
+ __tablename__ = "externalobjects"
+
+ id1 = Column(String(3), primary_key=True)
+ id2 = Column(String(3), primary_key=True)
+ name = Column(String(50))
+
+ self.create_tables()
+ sess = self.session
+ sc = ObjectExternal(id1="aaa", id2="bbb", name="sc1")
+ sess.add(sc)
+ sess.commit()
+
+ sc.name = "sc1modified"
+ sess.commit()
+
+ assert sc.version == 2
+
+ ObjectExternalHistory = ObjectExternal.__history_mapper__.class_
+
+ eq_(
+ sess.query(ObjectExternalHistory).all(),
+ [
+ ObjectExternalHistory(
+ version=1, id1="aaa", id2="bbb", name="sc1"
+ ),
+ ],
+ )
+
+ sess.delete(sc)
+ sess.commit()
+
+ assert sess.query(ObjectExternal).count() == 0
+
+ eq_(
+ sess.query(ObjectExternalHistory).all(),
+ [
+ ObjectExternalHistory(
+ version=1, id1="aaa", id2="bbb", name="sc1"
+ ),
+ ObjectExternalHistory(
+ version=2, id1="aaa", id2="bbb", name="sc1modified"
+ ),
+ ],
+ )
+
+ sc = ObjectExternal(id1="aaa", id2="bbb", name="sc1reappeared")
+ sess.add(sc)
+ sess.commit()
+
+ assert sc.version == 3
+
+ sc.name = "sc1reappearedmodified"
+ sess.commit()
+
+ assert sc.version == 4
+
+ eq_(
+ sess.query(ObjectExternalHistory).all(),
+ [
+ ObjectExternalHistory(
+ version=1, id1="aaa", id2="bbb", name="sc1"
+ ),
+ ObjectExternalHistory(
+ version=2, id1="aaa", id2="bbb", name="sc1modified"
+ ),
+ ObjectExternalHistory(
+ version=3, id1="aaa", id2="bbb", name="sc1reappeared"
+ ),
+ ],
+ )
+
class TestVersioningNewBase(TestVersioning):
def make_base(self):