SQLALCHEMY-CONTINUUM(1)

NAME
sqlalchemy-continuum - SQLAlchemy-Continuum Documentation

SQLAlchemy-Continuum is a versioning extension for SQLAlchemy.

INTRODUCTION

  Why?

SQLAlchemy already has a versioning extension <https://docs.sqlalchemy.org/en/13/orm/examples.html#module-examples.versioned_history>. This extension however is very limited. It does not support versioning entire transactions.

Hibernate for Java has Envers, which had nice features but lacks a nice API. Ruby on Rails has papertrail <https://github.com/airblade/paper_trail>, which has very nice API but lacks the efficiency and feature set of Envers.

As a Python/SQLAlchemy enthusiast I wanted to create a database versioning tool for Python with all the features of Envers and with as intuitive API as papertrail. Also I wanted to make it _fast_ keeping things as close to the database as possible.

  Features

•   Does not store updates which don't change anything

•   Supports alembic migrations

•   Can revert objects data as well as all object relations at given transaction even if the object was deleted

•   Transactions can be queried afterwards using SQLAlchemy query syntax

•   Querying for changed records at given transaction

•   Querying for versions of entity that modified given property

•   Querying for transactions, at which entities of a given class changed

•   History models give access to parent objects relations at any given point in time

  Installation

    pip install SQLAlchemy-Continuum

  Basics

In order to make your models versioned you need two things:

1.  Call make_versioned() before your models are defined.

2.  Add __versioned__ to all models you wish to add versioning to

        import sqlalchemy as sa
        from sqlalchemy_continuum import make_versioned
        make_versioned(user_cls=None)
        class Article(Base):
            __versioned__ = {}
            __tablename__ = 'article'
            id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
            name = sa.Column(sa.Unicode(255))
            content = sa.Column(sa.UnicodeText)
        # after you have defined all your models, call configure_mappers:
        sa.orm.configure_mappers()

    After this setup SQLAlchemy-Continuum does the following things:

1.  It creates ArticleHistory model that acts as version history for Article model

2.  Creates TransactionLog and TransactionChanges models for transactional history tracking

3.  Adds couple of listeners so that each Article object insert, update and delete gets recorded

    When the models have been configured either by calling configure_mappers() or by accessing some of them the first time, the following things become available:

        from sqlalchemy_continuum import version_class, parent_class
        version_class(Article)  # ArticleHistory class
        parent_class(version_class(Article))  # Article class

  Versions and transactions

At the end of each transaction SQLAlchemy-Continuum gathers all changes together and creates version objects for each changed versioned entity. Continuum also creates one TransactionLog entity and N number of TransactionChanges entities per transaction (here N is the number of affected classes per transaction). TransactionLog and TransactionChanges entities are created for transaction tracking.

    article = Article(name=u'Some article')
    session.add(article)
    session.commit()
    article.versions[0].name == u'Some article'
    article.name = u'Some updated article'
    session.commit()
    article.versions[1].name == u'Some updated article'

VERSION OBJECTS

  Operation types

When changing entities and committing results into database Continuum saves the used operations (INSERT, UPDATE or DELETE) into version entities. The operation types are stored by default to a small integer field named 'operation_type'. Class called 'Operation' holds convenient constants for these values as shown below:

    from sqlalchemy_continuum import Operation
    article = Article(name=u'Some article')
    session.add(article)
    session.commit()
    article.versions[0].operation_type == Operation.INSERT
    article.name = u'Some updated article'
    session.commit()
    article.versions[1].operation_type == Operation.UPDATE
    session.delete(article)
    session.commit()
    article.versions[2].operation_type == Operation.DELETE

  Version traversal

    first_version = article.versions[0]
    first_version.index
    # 0
    second_version = first_version.next
    assert second_version == article.versions[1]
    second_version.previous == first_version
    # True
    second_version.index
    # 1

  Changeset

Continuum provides easy way for getting the changeset of given version object. Each version contains a changeset property which holds a dict of changed fields in that version.

    article = Article(name=u'New article', content=u'Some content')
    session.add(article)
    session.commit(article)
    version = article.versions[0]
    version.changeset
    # {
    #   'id': [None, 1],
    #   'name': [None, u'New article'],
    #   'content': [None, u'Some content']
    # }
    article.name = u'Updated article'
    session.commit()
    version = article.versions[1]
    version.changeset
    # {
    #   'name': [u'New article', u'Updated article'],
    # }
    session.delete(article)
    version = article.versions[2]
    version.changeset
    # {
    #   'id': [1, None]
    #   'name': [u'Updated article', None],
    #   'content': [u'Some content', None]
    # }

SQLAlchemy-Continuum also provides a utility function called changeset. With this function you can easily check the changeset of given object in current transaction.

    from sqlalchemy_continuum import changeset
    article = Article(name=u'Some article')
    changeset(article)
    # {'name': [None, u'Some article']}

  Version relationships

Each version object reflects all parent object relationships. You can think version object relations as 'relations of parent object in given point in time'.

Lets say you have two models: Article and Category. Each Article has one Category. In the following example we first add article and category objects into database.

Continuum saves new ArticleVersion and CategoryVersion records in the background. After that we update the created article entity to use another category. Continuum creates new version objects accordingly.

Lastly we check the category relations of different article versions.

    category = Category(name=u'Some category')
    article = Article(
        name=u'Some article',
        category=category
    )
    session.add(article)
    session.commit()
    article.category = Category(name=u'Some other category')
    session.commit()
    article.versions[0].category.name  # u'Some category'
    article.versions[1].category.name  # u'Some other category'

The logic how SQLAlchemy-Continuum builds these relationships is within the RelationshipBuilder class.

  Relationships to non-versioned classes

Let's take previous example of Articles and Categories. Now consider that only Article model is versioned:

    class Article(Base):
        __tablename__ = 'article'
        __versioned__ = {}
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255), nullable=False)
    class Category(Base):
        __tablename__ = 'tag'
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255))
        article_id = sa.Column(sa.Integer, sa.ForeignKey(Article.id))
        article = sa.orm.relationship(
            Article,
            backref=sa.orm.backref('categories')
        )

Here Article versions will still reflect the relationships of Article model but they will simply return Category objects instead of CategoryVersion objects:

    category = Category(name=u'Some category')
    article = Article(
        name=u'Some article',
        category=category
    )
    session.add(article)
    session.commit()
    article.category = Category(name=u'Some other category')
    session.commit()
    version = article.versions[0]
    version.category.name                   # u'Some other category'
    isinstance(version.category, Category)  # True

  Dynamic relationships

If the parent class has a dynamic relationship it will be reflected as a property which returns a query in the associated version class.

    class Article(Base):
        __tablename__ = 'article'
        __versioned__ = {}
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255), nullable=False)
    class Tag(Base):
        __tablename__ = 'tag'
        __versioned__ = {}
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255))
        article_id = sa.Column(sa.Integer, sa.ForeignKey(Article.id))
        article = sa.orm.relationship(
            Article,
            backref=sa.orm.backref(
                'tags',
                lazy='dynamic'
            )
        )
    article = Article()
    article.name = u'Some article'
    article.content = u'Some content'
    session.add(article)
    session.commit()
    tag_query = article.versions[0].tags
    tag_query.all()  # return all tags for given version
    tag_query.count()  # return the tag count for given version

REVERTING CHANGES

One of the major benefits of SQLAlchemy-Continuum is its ability to revert changes.

  Revert update

    article = Article(name=u'New article', content=u'Some content')
    session.add(article)
    session.commit(article)
    version = article.versions[0]
    article.name = u'Updated article'
    session.commit()
    version.revert()
    session.commit()
    article.name
    # u'New article'

  Revert delete

    article = Article(name=u'New article', content=u'Some content')
    session.add(article)
    session.commit(article)
    version = article.versions[0]
    session.delete(article)
    session.commit()
    version.revert()
    session.commit()
    # article lives again!
    session.query(Article).first()

  Revert relationships

Sometimes you may have cases where you want to revert an object as well as some of its relation to certain state. Consider the following model definition:

    class Article(Base):
        __tablename__ = 'article'
        __versioned__ = {}
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255))
    class Tag(Base):
        __tablename__ = 'tag'
        __versioned__ = {}
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255))
        article_id = sa.Column(sa.Integer, sa.ForeignKey(Article.id))
        article = sa.orm.relationship(Article, backref='tags')

Now lets say some user first adds an article with couple of tags:

    article = Article(
        name=u'Some article',
        tags=[Tag(u'Good'), Tag(u'Interesting')]
    )
    session.add(article)
    session.commit()

Then lets say another user deletes one of the tags:

    tag = session.query(Tag).filter_by(name=u'Interesting')
    session.delete(tag)
    session.commit()

Now the first user wants to set the article back to its original state. It can be achieved as follows (notice how we use the relations parameter):

    article = session.get(Article, 1)
    article.versions[0].revert(relations=['tags'])
    session.commit()

QUERIES

You can query history models just like any other sqlalchemy declarative model.

    from sqlalchemy_continuum import version_class
    ArticleVersion = version_class(Article)
    session.query(ArticleVersion).filter_by(name=u'some name').all()

  How many transactions have been executed?

    from sqlalchemy_continuum import transaction_class
    Transaction = transaction_class(Article)
    Transaction.query.count()

  Querying for entities of a class at a given revision

In the following example we find all articles which were affected by transaction 33.

    session.query(ArticleVersion).filter_by(transaction_id=33)

  Querying for transactions, at which entities of a given class changed

In this example we find all transactions which affected any instance of 'Article' model. This query needs the TransactionChangesPlugin.

    TransactionChanges = Article.__versioned__['transaction_changes']
    entries = (
        session.query(Transaction)
        .innerjoin(Transaction.changes)
        .filter(
            TransactionChanges.entity_name.in_(['Article'])
        )
    )

  Querying for versions of entity that modified given property

In the following example we want to find all versions of Article class which changed the attribute 'name'. This example assumes you are using PropertyModTrackerPlugin.

    ArticleVersion = version_class(Article)
    session.query(ArticleHistory).filter(ArticleVersion.name_mod).all()

EFFICIENT VERSION QUERIES

SQLAlchemy-Continuum provides several methods for efficiently querying version history without incurring N+1 query problems.

  Querying version at a specific transaction

The version_at class method efficiently retrieves the version that was active at a specific transaction. This is much faster than iterating through versions manually.

    ArticleVersion = version_class(Article)
    # Get the version of Article #5 that was active at transaction #100
    version = ArticleVersion.version_at(
        session,
        {'id': 5},
        transaction_id=100
    )

For the validity strategy (default), this uses an efficient range query:

    WHERE transaction_id <= 100
      AND (end_transaction_id > 100 OR end_transaction_id IS NULL)

For the subquery strategy, it finds the version with the highest transaction_id <= target.

  Batch fetching all versions

When you need to iterate through version history, avoid the N+1 query problem by using all_versions instead of repeatedly accessing .previous or .next:

    ArticleVersion = version_class(Article)
    # Fetch all versions for Article #5 in a single query
    versions = ArticleVersion.all_versions(
        session,
        {'id': 5},
        limit=10,      # Optional: limit to 10 most recent
        desc=True,     # Newest first (default)
        link=True      # Pre-populate previous/next caches (default)
    )
    # Now iteration doesn't trigger additional queries
    for version in versions:
        print(version.changeset)
        print(version.previous)  # Uses cached value, no additional query!

When link=True (the default), the returned versions will have their .previous and .next properties pre-populated from the fetched results. This means accessing these properties won't trigger additional database queries.

Anti-pattern to avoid:

    # BAD: This triggers N queries for N versions!
    version = article.versions[-1]
    while version:
        process(version)
        version = version.previous  # Each call is a separate query

Recommended pattern:

    # GOOD: Single query, then iterate in memory
    versions = ArticleVersion.all_versions(
        session,
        {'id': article.id}
    )
    for version in versions:
        process(version)

INDEX RECOMMENDATIONS

SQLAlchemy-Continuum automatically creates several indexes on version tables. Understanding these indexes helps you write efficient queries.

  Automatic Indexes

The following indexes are created automatically:

•   transaction_id - Primary key index (always present)

•   end_transaction_id - For validity strategy (enables efficient range queries)

•   operation_type - For filtering INSERT/UPDATE/DELETE operations

    Starting with version 1.6.0, composite indexes are also created by default:

•   (primary_keys, transaction_id DESC) - For efficient entity version lookups

•   (primary_keys, transaction_id, end_transaction_id) - For validity strategy temporal queries

    These composite indexes dramatically speed up the most common query patterns.

  Disabling Composite Indexes

If you need to disable automatic composite index creation (e.g., for migration compatibility), you can set the create_composite_index option to False:

    make_versioned(options={'create_composite_index': False})

Or per-model:

    class Article(Base):
        __versioned__ = {
            'create_composite_index': False
        }

  Recommended Additional Indexes

Depending on your query patterns, you may want to add these additional indexes:

For queries filtering by operation type and entity:

    from sqlalchemy import Index
    Index(
        'ix_article_version_id_operation',
        ArticleVersion.id,
        ArticleVersion.operation_type,
        ArticleVersion.transaction_id
    )

For queries joining with Transaction table on issued_at:

If you frequently query versions by timestamp (e.g., "give me the version as of 2023-01-01"), ensure you have an index on Transaction.issued_at:

    Index('ix_transaction_issued_at', Transaction.issued_at.desc())

For PropertyModTrackerPlugin queries:

If you use PropertyModTrackerPlugin and frequently query for versions where specific fields changed, consider partial indexes:

    # PostgreSQL partial index example
    Index(
        'ix_article_version_name_mod',
        ArticleVersion.id,
        postgresql_where=ArticleVersion.name_mod.is_(True)
    )

  Query Performance Tips

1.  Use the validity strategy (default) for read-heavy workloads. It enables O(log N) version lookups via direct equality conditions instead of correlated subqueries.

2.  Batch fetch versions using all_versions() instead of iterating with .previous/.next.

3.  Add composite indexes on (entity_pk, transaction_id) for your most-queried version tables.

4.  Use LIMIT when you only need recent versions:

        ArticleVersion.all_versions(session, {'id': 5}, limit=10)

5.  Avoid relationship traversal on version objects when possible. Relationship queries on versions generate complex subqueries. If you need related data, fetch from the parent object first or use explicit joins.

TRANSACTIONS

  Transaction

For each committed transaction SQLAlchemy-Continuum creates a new Transaction record.

Transaction can be queried just like any other sqlalchemy declarative model.

    from sqlalchemy_continuum import transaction_class
    Transaction = transaction_class(Article)
    # find all transactions
    session.query(Transaction).all()

  UnitOfWork

For each database connection SQLAlchemy-Continuum creates an internal UnitOfWork object. Normally these objects are created at before flush phase of session workflow. However you can also force create unit of work before this phase.

    uow = versioning_manager.unit_of_work(session)

Transaction objects are normally created automatically at before flush phase. If you need access to transaction object before the flush phase begins you can do so by calling the create_transaction method of the UnitOfWork class.

    transaction = uow.create_transaction(session)

The version objects are normally created during the after flush phase but you can also force create those at any time by calling make_versions method.

    uow.make_versions(session)

  Workflow internals

Consider the following code snippet where we create a new article.

    article = Article()
    article.name = u'Some article'
    article.content = u'Some content'
    session.add(article)
    session.commit()

This would execute the following SQL queries (on PostgreSQL)

1.

INSERT INTO article (name, content) VALUES (?, ?)
    params: ('Some article', 'Some content')

2.

INSERT INTO transaction (issued_at) VALUES (?)
    params: (datetime.utcnow())

3.

INSERT INTO article_version (id, name, content, transaction_id) VALUES (?, ?, ?, ?)
    params: (<article id from query 1>, 'Some article', 'Some content', <transaction id from query 2>)

NATIVE VERSIONING

As of version 1.1 SQLAlchemy-Continuum supports native versioning for PostgreSQL dialect. Native versioning creates SQL triggers for all versioned models. These triggers keep track of changes made to versioned models. Compared to object based versioning, native versioning has

•   Much faster than regular object based versioning

•   Minimal memory footprint when used alongside create_tables=False and create_models=False configuration options.

•   More cumbersome database migrations, since triggers need to be updated also.

  Usage

For enabling native versioning you need to set native_versioning configuration option as True.

    make_versioned(options={'native_versioning': True})

  Schema migrations

When making schema migrations (for example adding new columns to version tables) you need to remember to call sync_trigger in order to keep the version trigger up-to-date.

    from sqlalchemy_continuum.dialects.postgresql import sync_trigger
    sync_trigger(conn, 'article_version')

If you don't use PropertyModTrackerPlugin, then you have to disable it:

    sync_trigger(conn, 'article_version', use_property_mod_tracking=False)

PLUGINS

  Using plugins

    from sqlalchemy_continuum.plugins import PropertyModTrackerPlugin
    versioning_manager.plugins.append(PropertyModTrackerPlugin())
    versioning_manager.plugins  # <PluginCollection [...]>
    # You can also remove plugin
    del versioning_manager.plugins[0]

  Activity

The ActivityPlugin is the most powerful plugin for tracking changes of individual entities. If you use ActivityPlugin you probably don't need to use TransactionChanges nor TransactionMeta plugins.

You can initalize the ActivityPlugin by adding it to versioning manager.

    activity_plugin = ActivityPlugin()
    make_versioned(plugins=[activity_plugin])

ActivityPlugin uses single database table for tracking activities. This table follows the data structure in activity stream specification <http://www.activitystrea.ms>, but it comes with a nice twist:
Column | Type | Description
id | BigInteger | The primary key of the activity
verb | Unicode | Verb defines the action of the activity
data | JSON | Additional data for the activity in JSON format
transaction_id | BigInteger | The transaction this activity was associated with
object_id | BigInteger | The primary key of the object. Object can be any entity which has an integer as primary key.
object_type | Unicode | The type of the object (class name as string)
object_tx_id | BigInteger | The last transaction_id associated with the object. This is used for efficiently fetching the object version associated with this activity.
target_id | BigInteger | The primary key of the target. Target can be any entity which has an integer as primary key.
target_type | Unicode | The of the target (class name as string)
target_tx_id | BigInteger | The last transaction_id associated with the target.

Each Activity has relationships to actor, object and target but it also holds information about the associated transaction and about the last associated transactions with the target and object. This allows each activity to also have object_version and target_version relationships for introspecting what those objects and targets were in given point in time. All these relationship properties use generic relationships <https://sqlalchemy-utils.readthedocs.io/en/latest/generic_relationship.html> of the SQLAlchemy-Utils package.

  Limitations

Currently all changes to parent models must be flushed or committed before creating activities. This is due to a fact that there is still no dependency processors for generic relationships. So when you create activities and assign objects / targets for those please remember to flush the session before creating an activity:

    article = Article(name=u'Some article')
    session.add(article)
    session.flush()  # <- IMPORTANT!
    first_activity = Activity(verb=u'create', object=article)
    session.add(first_activity)
    session.commit()

Targets and objects of given activity must have an integer primary key column id.

  Create activities

Once your models have been configured you can get the Activity model from the ActivityPlugin class with activity_cls property:

    Activity = activity_plugin.activity_cls

Now let's say we have model called Article and Category. Each Article has one Category. Activities should be created along with the changes you make on these models.

    article = Article(name=u'Some article')
    session.add(article)
    session.flush()
    first_activity = Activity(verb=u'create', object=article)
    session.add(first_activity)
    session.commit()

Current transaction gets automatically assigned to activity object:

    first_activity.transaction  # Transaction object

  Update activities

The object property of the Activity object holds the current object and the object_version holds the object version at the time when the activity was created.

    article.name = u'Some article updated!'
    session.flush()
    second_activity = Activity(verb=u'update', object=article)
    session.add(second_activity)
    session.commit()
    second_activity.object.name  # u'Some article updated!'
    first_activity.object.name  # u'Some article updated!'
    first_activity.object_version.name  # u'Some article'

  Delete activities

The version properties are especially useful for delete activities. Once the activity is fetched from the database the object is no longer available ( since its deleted), hence the only way we could show some information about the object the user deleted is by accessing the object_version property.

    session.delete(article)
    session.flush()
    third_activity = Activity(verb=u'delete', object=article)
    session.add(third_activity)
    session.commit()
    third_activity.object_version.name  # u'Some article updated!'

  Local version histories using targets

The target property of the Activity model offers a way of tracking changes of given related object. In the example below we create a new activity when adding a category for article and then mark the article as the target of this activity.

    session.add(Category(name=u'Fist category', article=article))
    session.flush()
    activity = Activity(
        verb=u'create',
        object=category,
        target=article
    )
    session.add(activity)
    session.commit()

Now if we wanted to find all the changes that affected given article we could do so by searching through all the activities where either the object or target is the given article.

    import sqlalchemy as sa
    activities = session.query(Activity).filter(
        sa.or_(
            Activity.object == article,
            Activity.target == article
        )
    )

  Flask

FlaskPlugin offers way of integrating Flask framework with SQLAlchemy-Continuum. Flask-Plugin adds two columns for Transaction model, namely user_id and remote_addr.

These columns are automatically populated when transaction object is created. The remote_addr column is populated with the value of the remote address that made current request. The user_id column is populated with the id of the current_user object.

    from sqlalchemy_continuum.plugins import FlaskPlugin
    from sqlalchemy_continuum import make_versioned
    make_versioned(plugins=[FlaskPlugin()])

  PropertyModTracker

The PropertyModTrackerPlugin offers a way of efficiently tracking individual property modifications. With PropertyModTrackerPlugin you can make efficient queries such as:

Find all versions of model X where user updated the property A or property B.

Find all versions of model X where user didn't update property A.

PropertyModTrackerPlugin adds separate modified tracking column for each versioned column. So for example if you have versioned model called Article with columns name and content, this plugin would add two additional boolean columns name_mod and content_mod for the version model. When user commits transactions the plugin automatically updates these boolean columns.

  TransactionChanges

TransactionChanges provides way of keeping track efficiently which declarative models were changed in given transaction. This can be useful when transactions need to be queried afterwards for problems such as:

1.  Find all transactions which affected User model.

2.  Find all transactions which didn't affect models Entity and Event.

    The plugin works in two ways. On class instrumentation phase this plugin creates a special transaction model called TransactionChanges. This model is associated with table called transaction_changes, which has only only two fields: transaction_id and entity_name. If for example transaction consisted of saving 5 new User entities and 1 Article entity, two new rows would be inserted into transaction_changes table.
    transaction_id | entity_name
    233678 | User
    233678 | Article

  TransactionMeta

TransactionMetaPlugin offers a way of saving key-value data for transations. You can use the plugin in same way as other plugins:

    meta_plugin = TransactionMetaPlugin()
    versioning_manager.plugins.append(meta_plugin)

TransactionMetaPlugin creates a simple model called TransactionMeta. This class has three columns: transaction_id, key and value. TransactionMeta plugin also creates an association proxy between TransactionMeta and Transaction classes for easy dictionary based access of key-value pairs.

You can easily 'tag' transactions with certain key value pairs by giving these keys and values to the meta property of Transaction class.

    from sqlalchemy_continuum import versioning_manager
    article = Article()
    session.add(article)
    uow = versioning_manager.unit_of_work(session)
    tx = uow.create_transaction(session)
    tx.meta = {u'some_key': u'some value'}
    session.commit()
    TransactionMeta = meta_plugin.model_class
    Transaction = versioning_manager.transaction_cls
    # find all transactions with 'article' tags
    query = (
        session.query(Transaction)
        .join(Transaction.meta_relation)
        .filter(
            db.and_(
                TransactionMeta.key == 'some_key',
                TransactionMeta.value == 'some value'
            )
        )
    )

CONFIGURATION

  Global and class level configuration

All Continuum configuration parameters can be set on global level (manager level) and on class level. Setting an option at manager level affects all classes within the scope of the manager's class instrumentation listener (by default all SQLAlchemy declarative models).

In the following example we set 'transaction_column_name' configuration option to False at the manager level.

    make_versioned(options={'transaction_column_name': 'my_tx_id'})

As the name suggests class level configuration only applies to given class. Class level configuration can be passed to __versioned__ class attribute.

    class User(Base):
        __versioned__ = {
            'transaction_column_name': 'tx_id'
        }

  Versioning strategies

Similar to Hibernate Envers SQLAlchemy-Continuum offers two distinct versioning strategies 'validity' and 'subquery'. The default strategy is 'validity'.

  Validity

The 'validity' strategy saves two columns in each history table, namely 'transaction_id' and 'end_transaction_id'. The names of these columns can be configured with configuration options transaction_column_name and end_transaction_column_name.

As with 'subquery' strategy for each inserted, updated and deleted entity Continuum creates new version in the history table. However it also updates the end_transaction_id of the previous version to point at the current version. This creates a little bit of overhead during data manipulation.

With 'validity' strategy version traversal is very fast. When accessing previous version Continuum tries to find the version record where the primary keys match and end_transaction_id is the same as the transaction_id of the given version record. When accessing the next version Continuum tries to find the version record where the primary keys match and transaction_id is the same as the end_transaction_id of the given version record.

Pros:

•   Version traversal is much faster since no correlated subqueries are needed

Cons:

•   Updates, inserts and deletes are little bit slower

  Subquery

The 'subquery' strategy uses one column in each history table, namely 'transaction_id'. The name of this column can be configured with configuration option transaction_column_name.

After each inserted, updated and deleted entity Continuum creates new version in the history table and sets the 'transaction_id' column to point at the current transaction.

With 'subquery' strategy the version traversal is slow. When accessing previous and next versions of given version object needs correlated subqueries.

Pros:

•   Updates, inserts and deletes little bit faster than in 'validity' strategy

Cons:

•   Version traversel much slower

  Column exclusion and inclusion

With exclude configuration option you can define which entity attributes you want to get versioned. By default Continuum versions all entity attributes.

    class User(Base):
        __versioned__ = {
            'exclude': ['picture']
        }
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(255))
        picture = sa.Column(sa.LargeBinary)

  Basic configuration options

Here is a full list of configuration options:

•

base_classes (default: None)
    A tuple defining history class base classes.

•

table_name (default: '%s_version')
    The name of the history table.

•

transaction_column_name (default: 'transaction_id')
    The name of the transaction column (used by history tables).

•

end_transaction_column_name (default: 'end_transaction_id')
    The name of the end transaction column in history table when using the validity versioning strategy.

•

operation_type_column_name (default: 'operation_type')
    The name of the operation type column (used by history tables).

•

strategy (default: 'validity')
    The versioning strategy to use. Either 'validity' or 'subquery'

•

create_composite_index (default: True)
    Whether to automatically create composite indexes on version tables for efficient version queries. The composite index is created on (primary_key_columns, transaction_id DESC) which dramatically speeds up common query patterns like finding a specific version of an entity. For validity strategy, an additional index on (primary_key_columns, transaction_id, end_transaction_id) is also created.

    Example

        class Article(Base):
            __versioned__ = {
                'transaction_column_name': 'tx_id'
            }
            __tablename__ = 'user'
            id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
            name = sa.Column(sa.Unicode(255))
            content = sa.Column(sa.UnicodeText)

  Customizing transaction user class

By default Continuum tries to build a relationship between 'User' class and Transaction class. If you have differently named user class you can simply pass its name to make_versioned:

    make_versioned(user_cls='MyUserClass')

If you don't want transactions to contain any user references you can also disable this feature.

    make_versioned(user_cls=None)

  Customizing versioned mappers

By default SQLAlchemy-Continuum versions all mappers. You can override this behaviour by passing the desired mapper class/object to make_versioned function.

    make_versioned(mapper=my_mapper)

  Customizing versioned sessions

By default SQLAlchemy-Continuum versions all sessions. You can override this behaviour by passing the desired session class/object to make_versioned function.

    make_versioned(session=my_session)

CONTINUUM SCHEMA

  Version tables

By default SQLAlchemy-Continuum creates a version table for each versioned entity table. The version tables are suffixed with '_version'. So for example if you have two versioned tables 'article' and 'category', SQLAlchemy-Continuum would create two version tables 'article_version' and 'category_version'.

By default the version tables contain these columns:

•   id of the original entity (this can be more then one column in the case of composite primary keys)

•   transaction_id - an integer that matches to the id number in the transaction_log table.

•   end_transaction_id - an integer that matches the next version record's transaction_id. If this is the current version record then this field is null.

•   operation_type - a small integer defining the type of the operation

•   versioned fields from the original entity

    If you are using PropertyModTracker <#property-mod-tracker> Continuum also creates one boolean field for each versioned field. By default these boolean fields are suffixed with '_mod'.

    The primary key of each version table is the combination of parent table's primary key + the transaction_id. This means there can be at most one version table entry for a given entity instance at given transaction.

  Transaction tables

By default Continuum creates one transaction table called transaction. Many continuum plugins also create additional tables for efficient transaction storage. If you wish to query efficiently transactions afterwards you should consider using some of these plugins.

The transaction table only contains two fields by default: id and issued_at.

  Using vacuum

sqlalchemy_continuum.vacuum(session, model, yield_per=1000)
    When making structural changes to version tables (for example dropping columns) there are sometimes situations where some old version records become futile.

    Vacuum deletes all futile version rows which had no changes compared to previous version.

        from sqlalchemy_continuum import vacuum
        vacuum(session, User)  # vacuums user version

Parameters

•   session -- SQLAlchemy session object

•   model -- SQLAlchemy declarative model class

•   yield_per -- how many rows to process at a time

  Schema tools

sqlalchemy_continuum.schema.update_end_tx_column(table, end_tx_column_name='end_transaction_id', tx_column_name='transaction_id', conn=None)
    Calculates end transaction columns and updates the version table with the calculated values. This function can be used for migrating between subquery versioning strategy and validity versioning strategy.

Parameters

•   table -- SQLAlchemy table object

•   end_tx_column_name -- Name of the end transaction column

•   tx_column_name -- Transaction column name

•   conn --

    Either SQLAlchemy Connection, Engine, Session or Alembic Operations object. Basically this should be an object that can execute the queries needed to update the end transaction column values.

    If no object is given then this function tries to use alembic.op for executing the queries.

sqlalchemy_continuum.schema.update_property_mod_flags(table, tracked_columns, mod_suffix='_mod', end_tx_column_name='end_transaction_id', tx_column_name='transaction_id', conn=None)
    Update property modification flags for given table and given columns. This function can be used for migrating an existing schema to use property mod flags (provided by PropertyModTracker plugin).

Parameters

•   table -- SQLAlchemy table object

•   mod_suffix -- Modification tracking columns suffix

•   end_tx_column_name -- Name of the end transaction column

•   tx_column_name -- Transaction column name

•   conn --

    Either SQLAlchemy Connection, Engine, Session or Alembic Operations object. Basically this should be an object that can execute the queries needed to update the property modification flags.

    If no object is given then this function tries to use alembic.op for executing the queries.

ALEMBIC MIGRATIONS

Each time you make changes to database structure you should also change the associated history tables. When you make changes to your models SQLAlchemy-Continuum automatically alters the history model definitions, hence you can use alembic revision --autogenerate just like before. You just need to make sure make_versioned function gets called before alembic gathers all your models and configure_mappers is called afterwards.

Pay close attention when dropping or moving data from parent tables and reflecting these changes to history tables.

  Troubleshooting

If alembic didn't detect any changes or generates reversed migration (tries to remove *_version tables from database instead of creating), make sure that configure_mappers was called by alembic command.

UTILITIES

  changeset

sqlalchemy_continuum.utils.changeset(obj)
    Return a humanized changeset for given SQLAlchemy declarative object. With this function you can easily check the changeset of given object in current transaction.

        from sqlalchemy_continuum import changeset
        article = Article(name=u'Some article')
        changeset(article)
        # {'name': [u'Some article', None]}

Parameters
    obj -- SQLAlchemy declarative model object

  count_versions

sqlalchemy_continuum.utils.count_versions(obj)
    Return the number of versions given object has. This function works even when obj has create_models and create_tables versioned settings disabled.

        article = Article(name=u'Some article')
        count_versions(article)  # 0
        session.add(article)
        session.commit()
        count_versions(article)  # 1

Parameters
    obj -- SQLAlchemy declarative model object

  get_versioning_manager

sqlalchemy_continuum.utils.get_versioning_manager(obj_or_class)
    Return the associated SQLAlchemy-Continuum VersioningManager for given SQLAlchemy declarative model class or object.

Parameters
    obj_or_class -- SQLAlchemy declarative model object or class

  is_modified

sqlalchemy_continuum.utils.is_modified(obj)
    Return whether or not the versioned properties of given object have been modified.

        article = Article()
        is_modified(article)  # False
        article.name = 'Something'
        is_modified(article)  # True

Parameters
    obj -- SQLAlchemy declarative model object

    See also: is_modified_or_deleted()

    See also: is_session_modified()

  is_modified_or_deleted

sqlalchemy_continuum.utils.is_modified_or_deleted(obj)
    Return whether or not some of the versioned properties of given SQLAlchemy declarative object have been modified or if the object has been deleted.

Parameters
    obj -- SQLAlchemy declarative model object

  is_session_modified

sqlalchemy_continuum.utils.is_session_modified(session)
    Return whether or not any of the versioned objects in given session have been either modified or deleted.

Parameters
    session -- SQLAlchemy session object

    See also: is_versioned()

    See also: versioned_objects()

  is_versioned

sqlalchemy_continuum.utils.is_versioned(obj_or_class)
    Return whether or not given object is versioned.

        is_versioned(Article)  # True
        article = Article()
        is_versioned(article)  # True

Parameters
    obj_or_class -- SQLAlchemy declarative model object or SQLAlchemy declarative model class.

    See also: versioned_objects()

  parent_class

sqlalchemy_continuum.utils.parent_class(version_cls)
    Return the parent class for given version model class.

        parent_class(ArticleVersion)  # Article class

Parameters
    model -- SQLAlchemy declarative version model class

    See also: version_class()

  transaction_class

sqlalchemy_continuum.utils.transaction_class(cls)
    Return the associated transaction class for given versioned SQLAlchemy declarative class or version class.

        from sqlalchemy_continuum import transaction_class
        transaction_class(Article)  # Transaction class

Parameters
    cls -- SQLAlchemy versioned declarative class or version model class

  version_class

sqlalchemy_continuum.utils.version_class(model)
    Return the version class for given SQLAlchemy declarative model class.

        version_class(Article)  # ArticleVersion class

Parameters
    model -- SQLAlchemy declarative model class

    See also: parent_class()

  versioned_objects

sqlalchemy_continuum.utils.versioned_objects(session)
    Return all versioned objects in given session.

Parameters
    session -- SQLAlchemy session object

    See also: is_versioned()

  version_table

sqlalchemy_continuum.utils.version_table(table)
    Return associated version table for given SQLAlchemy Table object.

Parameters
    table -- SQLAlchemy Table object

API DOCUMENTATION

sqlalchemy_continuum.make_versioned(mapper=<class 'sqlalchemy.orm.mapper.Mapper'>, session=<class 'sqlalchemy.orm.session.Session'>, manager=<sqlalchemy_continuum.manager.VersioningManager object>, plugins=None, options=None, user_cls='User')
    This is the public API function of SQLAlchemy-Continuum for making certain mappers and sessions versioned. By default this applies to all mappers and all sessions.

Parameters

•   mapper -- SQLAlchemy mapper to apply the versioning to.

•   session -- SQLAlchemy session to apply the versioning to. By default this is sa.orm.session.Session meaning it applies to all Session subclasses.

•   manager -- SQLAlchemy-Continuum versioning manager.

•   plugins -- Plugins to pass for versioning manager.

•   options -- A dictionary of VersioningManager options.

•   user_cls -- User class which the Transaction class should have relationship to. This can either be a class or string name of a class for lazy evaluation.

  Versioning Manager

class sqlalchemy_continuum.VersioningManager(unit_of_work_cls=<class 'sqlalchemy_continuum.unit_of_work.UnitOfWork'>, transaction_cls=None, user_cls=None, options=None, plugins=None, builder=None)
    VersioningManager delegates versioning configuration operations to builder classes and the actual versioning to UnitOfWork class. Manager contains configuration options that act as defaults for all versioned classes.

Parameters

•   unit_of_work_cls -- The UnitOfWork class to use for initializing UnitOfWork objects for versioning

•   transaction_cls -- Transaction class to use for versioning. If None, the default Transaction class generated by TransactionFactory will be used.

•   user_cls -- User class which Transaction class should have relationship to. This can either be a class or string name of a class for lazy evaluation.

•   options -- Versioning options

•   plugins -- Versioning plugins that listen the events invoked by the manager.

•   builder -- Builder object which handles the building of versioning tables and models.

after_flush(session, flush_context)
    After flush listener for SQLAlchemy sessions. If this manager has versioning enabled this listener gets the UnitOfWork associated with session's connections and invokes the process_after_flush method of that object.

Parameters
    session -- SQLAlchemy session

append_association_operation(conn, table_name, params, op)
    Append history association operation to pending_statements list.

apply_class_configuration_listeners(mapper)
    Applies class configuration listeners for given mapper.

    The listener work in two phases:

1.

Class instrumentation phase
    The first listeners listens to class instrumentation event and handles the collecting of versioned models and adds them to the pending_classes list.

2.

After class configuration phase
    The second listener listens to after class configuration event and handles the actual history model generation based on list that was collected during class instrumenation phase.

Parameters
    mapper -- SQLAlchemy mapper to apply the class configuration listeners to

before_flush(session, flush_context, instances)
    Before flush listener for SQLAlchemy sessions. If this manager has versioning enabled this listener invokes the process before flush of associated UnitOfWork object.

Parameters
    session -- SQLAlchemy session

clear(session)
    Simple SQLAlchemy listener that is being invoked after successful transaction commit or when transaction rollback occurs. The purpose of this listener is to reset this UnitOfWork back to its initialization state.

Parameters
    session -- SQLAlchemy session object

create_transaction_model()
    Create Transaction class but only if it doesn't already exist in declarative model registry.

is_excluded_property(model, key)
    Returns whether or not given property of given model is excluded from the associated history model.

Parameters

•   model -- SQLAlchemy declarative model object.

•   key -- Model property key

option(model, name)
    Returns the option value for given model. If the option is not found from given model falls back to default values of this manager object. If the option is not found from this manager object either this method throws a KeyError.

Parameters

•   model -- SQLAlchemy declarative object

•   name -- name of the versioning option

remove_class_configuration_listeners(mapper)
    Remove versioning class configuration listeners from specified mapper.

Parameters
    mapper -- mapper to remove class configuration listeners from

remove_operations_tracking(mapper)
    Remove listeners from specified mapper that track SQL inserts, updates and deletes.

Parameters
    mapper -- mapper to remove the SQL operations tracking listeners from

remove_session_tracking(session)
    Remove listeners that track the operations (flushing, committing and rolling back) of given session. This method should be used in conjunction with remove_operations_tracking.

Parameters
    session -- SQLAlchemy session to remove the operations tracking from

reset()
    Resets this manager's internal state.

    This method should be used in test cases that create models on the fly. Otherwise history_class_map and some other variables would be polluted by no more used model classes.

track_cloned_connections(c, opt)
    Track cloned connections from association tables.

track_deletes(uow, target)
    Track object deletion operations. Whenever object is deleted it is added to this UnitOfWork's internal operations dictionary.

track_inserts(uow, target)
    Track object insert operations. Whenever object is inserted it is added to this UnitOfWork's internal operations dictionary.

track_operations(mapper)
    Attach listeners for specified mapper that track SQL inserts, updates and deletes.

Parameters
    mapper -- mapper to track the SQL operations from

track_session(session)
    Attach listeners that track the operations (flushing, committing and rolling back) of given session. This method should be used in conjunction with track_operations.

Parameters
    session -- SQLAlchemy session to track the operations from

track_updates(uow, target)
    Track object update operations. Whenever object is updated it is added to this UnitOfWork's internal operations dictionary.

unit_of_work(session)
    Return the associated SQLAlchemy-Continuum UnitOfWork object for given SQLAlchemy session object.

    If no UnitOfWork object exists for given object then this method tries to create one.

Parameters
    session -- SQLAlchemy session object

  Builders

class sqlalchemy_continuum.table_builder.TableBuilder(versioning_manager, parent_table, model=None)
    TableBuilder handles the building of version tables based on parent table's structure and versioning configuration options.

property table_name
    Returns the version table name for current parent table.

class sqlalchemy_continuum.model_builder.ModelBuilder(versioning_manager, model)
    VersionedModelBuilder handles the building of Version models based on parent table attributes and versioning configuration.

base_classes()
    Returns all base classes for history model.

build_model(table)
    Build history model class.

build_parent_relationship()
    Builds a relationship between currently built version class and parent class (the model whose history the currently build version class represents).

build_transaction_relationship(tx_class)
    Builds a relationship between currently built version class and Transaction class.

Parameters
    tx_class -- Transaction class

inheritance_args(cls, version_table, table)
    Return mapper inheritance args for currently built history model.

class sqlalchemy_continuum.relationship_builder.RelationshipBuilder(versioning_manager, model, property_)

association_subquery(obj)
    Returns an EXISTS clause that checks if an association exists for given SQLAlchemy declarative object. This query is used by many_to_many_criteria method.

    Example query:

EXISTS (
    SELECT 1 FROM article_tag_version WHERE article_id = 3 AND tag_id = tags_version.id AND operation_type != 2 AND EXISTS ( SELECT 1 FROM article_tag_version as article_tag_version2 WHERE article_tag_version2.tag_id = article_tag_version.tag_id AND article_tag_version2.tx_id <=5 AND article_tag_version2.article_id = 3 GROUP BY article_tag_version2.tag_id HAVING MAX(article_tag_version2.tx_id) = article_tag_version.tx_id

    )

    )

Parameters
    obj -- SQLAlchemy declarative object

build_association_version_tables()
    Builds many-to-many association version table for given property. Association version tables are used for tracking change history of many-to-many associations.

many_to_many_criteria(obj)
    Returns the many-to-many query.

    Looks up remote items through associations and for each item returns returns the last version with a transaction less than or equal to the transaction of obj. This must hold true for both the association and the remote relation items.

  Example

Select all tags of article with id 3 and transaction 5

SELECT tags_version.* FROM tags_version WHERE EXISTS ( SELECT 1 FROM article_tag_version WHERE article_id = 3 AND tag_id = tags_version.id AND operation_type != 2 AND EXISTS ( SELECT 1 FROM article_tag_version as article_tag_version2 WHERE article_tag_version2.tag_id = article_tag_version.tag_id AND article_tag_version2.tx_id <= 5 GROUP BY article_tag_version2.tag_id HAVING MAX(article_tag_version2.tx_id) = article_tag_version.tx_id

)

) AND EXISTS ( SELECT 1 FROM tags_version as tags_version_2 WHERE tags_version_2.id = tags_version.id AND tags_version_2.tx_id <= 5 GROUP BY tags_version_2.id HAVING MAX(tags_version_2.tx_id) = tags_version.tx_id

) AND operation_type != 2

many_to_one_criteria(obj)
    Returns the many-to-one query.

    Returns the item on the 'one' side with the highest transaction id as long as it is less or equal to the transaction id of the obj.

  Example

Look up the Article of a Tag with article_id = 4 and transaction_id = 5

SELECT * FROM articles_version WHERE id = 4 AND transaction_id = ( SELECT max(transaction_id) FROM articles_version WHERE transaction_id <= 5 AND id = 4

) AND operation_type != 2

one_to_many_criteria(obj)
    Returns the one-to-many query.

    For each item on the 'many' side, returns its latest version as long as the transaction of that version is less than equal of the transaction of obj.

  Example

Using the Article-Tags relationship, where we look for tags of article_version with id = 3 and transaction = 5 the sql produced is

SELECT tags_version.* FROM tags_version WHERE tags_version.article_id = 3 AND tags_version.operation_type != 2 AND EXISTS ( SELECT 1 FROM tags_version as tags_version_last WHERE tags_version_last.transaction_id <= 5 AND tags_version_last.id = tags_version.id GROUP BY tags_version_last.id HAVING MAX(tags_version_last.transaction_id) = tags_version.transaction_id

)

process_query(query)
    Process given SQLAlchemy Query object depending on the associated RelationshipProperty object.

Parameters
    query -- SQLAlchemy Query object

property reflected_relationship
    Builds a reflected one-to-many, one-to-one and many-to-one relationship between two version classes.

  UnitOfWork

class sqlalchemy_continuum.unit_of_work.UnitOfWork(manager)

assign_attributes(parent_obj, version_obj)
    Assign attributes values from parent object to version object.

Parameters

•   parent_obj -- Parent object to get the attribute values from

•   version_obj -- Version object to assign the attribute values to

create_association_versions(session)
    Creates association table version records for given session.

Parameters
    session -- SQLAlchemy session object

create_transaction(session)
    Create transaction object for given SQLAlchemy session.

Parameters
    session -- SQLAlchemy session object

create_version_objects(session)
    Create version objects for given session based on operations collected by insert, update and deleted trackers.

Parameters
    session -- SQLAlchemy session object

get_or_create_version_object(target)
    Return version object for given parent object. If no version object exists for given parent object, create one.

Parameters
    target -- Parent object to create the version object for

property has_changes
    Return whether or not this unit of work has changes.

is_modified(session)
    Return whether or not given session has been modified. Session has been modified if any versioned property of any version object in given session has been modified or if any of the plugins returns that session has been modified.

Parameters
    session -- SQLAlchemy session object

make_versions(session)
    Create transaction, transaction changes records, version objects.

Parameters
    session -- SQLAlchemy session object

process_after_flush(session)
    After flush processor for given session.

    Creates version objects for all modified versioned parent objects that were affected during the flush phase.

Parameters
    session -- SQLAlchemy session object

process_before_flush(session)
    Before flush processor for given session.

    This method creates a version session which is later on used for the creation of version objects. It also creates Transaction object for the current transaction and invokes before_flush template method on all plugins.

    If the given session had no relevant modifications regarding versioned objects this method does nothing.

Parameters
    session -- SQLAlchemy session object

process_operation(operation)
    Process given operation object. The operation processing has x stages:

1.  Get or create a version object for given parent object

2.  Assign the operation type for this object

3.  Invoke listeners

4.  Update version validity in case validity strategy is used

5.  Mark operation as processed

Parameters
    operation -- Operation object

reset(session=None)
    Reset the internal state of this UnitOfWork object. Normally this is called after transaction has been committed or rolled back.

update_version_validity(parent, version_obj)
    Updates previous version object end_transaction_id based on given parent object and newly created version object.

    This method is only used when using 'validity' versioning strategy.

Parameters
    parent -- SQLAlchemy declarative parent object

Parem version_obj
    SQLAlchemy declarative version object

    See also: version_validity_subquery()

version_validity_subquery(parent, version_obj, alias=None)
    Return the subquery needed by update_version_validity().

    This method is only used when using 'validity' versioning strategy.

Parameters
    parent -- SQLAlchemy declarative parent object

Parem version_obj
    SQLAlchemy declarative version object

    See also: update_version_validity()

  History class

class sqlalchemy_continuum.version.VersionClassBase

classmethod all_versions(session, primary_key_values: Dict, limit: int | None = None, offset: int = 0, desc: bool = True, link: bool = True) -> List[VersionClassBase <#sqlalchemy_continuum.version.VersionClassBase>]
    Efficiently fetch all versions for an entity in a single query.

    This avoids N+1 queries when iterating through version history by fetching all versions at once. When link=True, the returned versions will have their .previous and .next properties pre-populated from the cache.

    Example:

        # Get all versions of Article #5, newest first
        versions = ArticleVersion.all_versions(
            session,
            {'id': 5},
            limit=10  # Only get the 10 most recent versions
        )
        # Iterate without N+1 queries
        for version in versions:
            print(version.changeset)
            print(version.previous)  # Uses cached value, no query

Parameters

•   session -- SQLAlchemy session

•   primary_key_values -- Dict mapping primary key column names to values

•   limit -- Maximum number of versions to return (None for all)

•   offset -- Number of versions to skip

•   desc -- If True, return newest versions first (default)

•   link -- If True, pre-populate previous/next caches (default)

Returns
    List of version objects

property changeset
    Return a dictionary of changed fields in this version with keys as field names and values as lists with first value as the old field value and second list value as the new value.

property index
    Return the index of this version in the version history.

property next
    Returns the next version relative to this version in the version history. If current version is the last version this method returns None.

    If versions have been pre-fetched using all_versions() with link_versions(), this will use the cached value instead of making a database query.

property previous
    Returns the previous version relative to this version in the version history. If current version is the first version this method returns None.

    If versions have been pre-fetched using all_versions() with link_versions(), this will use the cached value instead of making a database query.

classmethod version_at(session, primary_key_values: Dict, transaction_id: int) -> VersionClassBase <#sqlalchemy_continuum.version.VersionClassBase> | None
    Efficiently retrieve the version that was active at a specific transaction.

    This is more efficient than iterating through versions manually, especially when using the validity strategy which can use range conditions.

    Example:

        # Get the version of Article #5 that was active at transaction #100
        version = ArticleVersion.version_at(
            session,
            {'id': 5},
            transaction_id=100
        )

Parameters

•   session -- SQLAlchemy session

•   primary_key_values -- Dict mapping primary key column names to values

•   transaction_id -- The transaction ID to query at

Returns
    The version object active at that transaction, or None

  Changelog

Here you can see the full list of changes between each SQLAlchemy-Continuum release.

  Unreleased changes

•   None currently

  1.6.0 (2026-01-22)

•   Add version_at() class method on version objects for efficient retrieval of the version active at a specific transaction (#376)

•   Add all_versions() class method with link option to batch fetch all versions for an entity in a single query, avoiding N+1 queries by pre-populating previous/next navigation caches (#376)

•   Add automatic composite indexes on version tables for optimized version lookups: (pk_columns, transaction_id DESC) index and (pk_columns, transaction_id, end_transaction_id) for validity strategy temporal queries (#376)

•   Add create_composite_index configuration option to control composite index creation (#376)

  1.5.2 (2025-10-10)

•   Add Python 3.14 support

  1.5.1 (2025-10-01)

•   Fix utc_now() to return a naive datetime (#373, thanks to dawhalen)

•   Remove SQLAlchemy-Utils dependency by porting required functions to internal _compat module (#352) Note: if you use SQLAlchemy-Utils directly, you may need to add it as a dependency. - Port core functions: ImproperlyConfigured, get_declarative_base, naturally_equivalent - Port column utilities: get_columns, get_primary_keys, identity, get_column_key - Port advanced functionality: has_changes, JSONType, generic_relationship with full SQLAlchemy 2.x compatibility - Maintain full backward compatibility while eliminating external dependency - Reduce installation footprint and potential version conflicts

  1.5.0 (2025-08-30)

•   Migrate to ruff <https://docs.astral.sh/ruff/> for code linting and formatting, replacing flake8 with a faster Rust-based tool. (#364)

•   Add Python 3.13 support. (#364)

•   Drop Python 3.8 support. (#364)

•   Add comprehensive pre-commit hooks configuration with ruff, pyupgrade, and code quality checks

•   Add pyupgrade <https://github.com/asottile/pyupgrade> integration to automatically modernize Python 3.9+ syntax

•   Enhance tox configuration with matrix testing for multiple Python (3.9-3.13) and SQLAlchemy versions (1.4, 2.x)

•   Add dedicated ruff testing environment in tox for consistent code quality checks

•   Modernize codebase with Python 3.9+ idioms and formatting improvements

•   Migrate to modern Python packaging with pyproject.toml

•   Remove SQLAlchemy-i18n support

•   Fix SQLAlchemy 2.0 deprecation warnings: replace Query.get() with Session.get() for improved compatibility

•   Fix datetime.utcnow() deprecation warnings with cross-version compatibility function supporting Python 3.9-3.13+

•   Eliminate cartesian product warnings in many-to-many relationship queries with non-versioned classes

•   Improve code quality by modernizing mixed string formatting patterns to f-strings

  1.4.2 (2024-03-26)

•   Remove SQLAlchemy pin and require latest SQLAlchemy-Utils

  1.4.1 (2024-03-14)

•   Pin SQLAlchemy due to SQLAlchemy-Utils breakage

•   Fix docs (#335, thanks to gnu-lorien)

•   Remove use of deprecated _app_ctx_stack and _request_ctx_stack (#307, thanks to rubencho)

  1.4.0 (2023-07-12)

•   Add support for SQLAlchemy 2.0

•   Remove compatibility code for SQLAlchemy < 1.4 and Python 2

•   Fix deadlock issues in MySQL on concurrent inserts (#172, thanks to mfulgo)

•   Allow overriding of DATABASE_URL for docker compose

•   Switch to before_execute for association tracking

•   Use ORM instead of deadlock-prone validity update query

•   create_trigger and sync_trigger now take a session, not a connection

•   Removed Operation.iteritems

  1.3.15 (2023-06-07)

•   Fix docs (#329, thanks to kelvinscuesta)

•   Declare six as a dependency (#327, thanks to zupo)

  1.3.14 (2023-01-04)

•   Undo unneeded change to Flask-Login that breaks alternative IDs (#325, thanks to anthraxx)

  1.3.13 (2022-09-07)

•   Fixes for Flask 2.2 and Flask-Login 0.6.2 (#288, thanks to AbdealiJK)

•   Allow changed_entities to work without TransactionChanges plugin (#268, thanks to TomGoBravo)

•   Fix Activity plugin for non-composite primary keys not named id (#210, thanks to dryobates)

•   Allow sync_trigger to pass arguments through to create_trigger (#273, thanks to nanvel)

•   Fix association tables on Oracle (#291, thanks to AbdealiJK)

•   Fix some deprecation warnings in SA 1.4 (#269, #277, #279, #300, #302, thanks to TomGoBravo, edhaz, and indiVar0508)

  1.3.12 (2022-01-18)

•   Support SA 1.4

  1.3.11 (2020-05-24)

•   Made ModelBuilder create column aliases in version models (#246, courtesy of killthekitten)

  1.3.10 (2020-05-10)

•   Added explicit "pseudo-backref" relationships for version/parent (#240, courtesy of lgedgar)

•   Fixed m2m Bug when an unrelated change is made to a model (#242, courtesy of Andrew-Dickinson)

  1.3.9 (2019-03-19)

•   Added SA 1.3 support

•   Reverted trigger creation from 1.3.7

  1.3.8 (2019-02-27)

•   Fixed revert to ignore non-columns (#197, courtesy of mauler)

  1.3.7 (2019-01-13)

•   Fix trigger creation during alembic migrations (#209, courtesy of lyndsysimon)

  1.3.6 (2018-07-30)

•   Fixed ResourceClosedErrors from connections leaking when using an external transaction (#196, courtesy of vault)

  1.3.5 (2018-06-03)

•   Track cloned connections (#167, courtesy of netcriptus)

  1.3.4 (2018-03-07)

•   Exclude many-to-many properties from versioning if they are added in exclude parameter (#169, courtesy of fuhrysteve)

  1.3.3 (2017-11-05)

•   Fixed changeset when updating object in same transaction as inserting it (#141, courtesy of oinopion)

  1.3.2 (2017-10-12)

•   Fixed multiple schema handling (#132, courtesy of vault)

  1.3.1 (2017-06-28)

•   Fixed subclass retrieval for closest_matching_table (#163, courtesy of debonzi)

  1.3.0 (2017-01-30)

•   Dropped py2.6 support

•   Fixed memory leaks with UnitOfWork instances (#131, courtesy of quantus)

  1.2.4 (2016-01-10)

•   Added explicit sequence names for Oracle (#118, courtesy of apfeiffer1)

  1.2.3 (2016-01-10)

•   Added use_module_name configuration option (#119, courtesy of kyheo)

  1.2.2 (2015-12-08)

•   Fixed some relationship changes not counted as modifications (#116, courtesy of tvuotila)

  1.2.1 (2015-09-27)

•   Fixed deep joined table inheritance handling (#105, courtesy of piotr-dobrogost)

•   Fixed naive assumption of related User model always having id column (#107, courtesy of avilaton)

•   Fixed one-to-many relationship reverting (#102, courtesy of sdorazio)

  1.2.0 (2015-07-31)

•   Removed generated changes attribute from version classes. This attribute can be accessed through transaction.changes

•   Removed is_modified checking from insert operations

  1.1.5 (2014-12-28)

•   Added smart primary key type inspection for user class (#86, courtesy of mattupstate)

•   Added support for self-referential version relationship reflection (#88, courtesy of dtheodor)

  1.1.4 (2014-12-06)

•   Fixed One-To-Many version relationship handling (#82, courtesy of dtheodor)

•   Fixed Many-To-Many version relationship handling (#83, courtesy of dtheodor)

•   Fixed inclusion and exclusion of aliased columns

•   Removed automatic exclusion of auto-assigned datetime columns and tsvector columns (explicit is better than implicit)

  1.1.3 (2014-10-23)

•   Made FlaskPlugin accepts overriding of current_user_id_factory and remote_addr_factory

  1.1.2 (2014-10-07)

•   Fixed identifier quoting in trigger syncing

  1.1.1 (2014-10-07)

•   Fixed native versioning trigger syncing

  1.1.0 (2014-10-02)

•   Added Python 3.4 to test suite

•   Added optional native trigger based versioning for PostgreSQL dialect

•   Added create_models option

•   Added count_versions utility function

•   Fixed custom transaction column name handling with models using joined table inheritance

•   Fixed subquery strategy support for models using joined table inheritance

•   Fixed savepoint handling

•   Fixed version model building when no versioned models were found (previously threw AttributeError)

•   Replaced plugin template methods before_create_tx_object and after_create_tx_object with transaction_args to better cope with native versioning

  1.0.3 (2014-07-16)

•   Added __repr__ for Operations class

•   Fixed an issue where assigning unmodified object's attributes in user defined before flush listener would raise TypeError in UnitOfWork

  1.0.2 (2014-07-11)

•   Allowed easier overriding of PropertyModTracker column creation

•   Rewrote join table inheritance handling schematics (now working with SA 0.9.6)

•   SQLAlchemy-Utils dependency updated to 0.26.5

  1.0.1 (2014-06-18)

•   Fixed an issue where deleting an object with deferred columns would throw ObjectDeletedError.

•   Made viewonly relationships with association tables not register the association table to versioning manager registry.

  1.0 (2014-06-16)

•   Added __repr__ for Transaction class, issue #59

•   Made transaction_cls of VersioningManager configurable.

•   Removed generic relationships from transaction class to versioned classes.

•   Removed generic relationships from transaction changes class to versioned classes.

•   Removed relation_naming_function (no longer needed)

•   Moved get_bind to SQLAlchemy-Utils

•   Removed inflection package from dependencies (no longer needed)

•   SQLAlchemy-Utils dependency updated to 0.26.2

  1.0b5 (2014-05-07)

•   Added order_by mapper arg ignoring for version class reflection if other than string argument is used

•   Added support for customizing the User class which the Transaction class should have relationship to (issue #53)

•   Changed get_versioning_manager to throw ClassNotVersioned exception if first argument is not a versioned class

•   Fixed relationship reflection from versioned classes to non versioned classes (issue #52)

•   SQLAlchemy-Utils dependency updated to 0.25.4

  1.0-b4 (2014-04-20)

•   Fixed many-to-many unit of work inspection when using engine bind instead of collection bind

•   Fixed various issues if primary key aliases were used in declarative models

•   Fixed an issue where association versioning would not work with custom transaction column name

•   SQLAlchemy-Utils dependency updated to 0.25.3

  1.0-b3 (2014-04-19)

•   Added support for concrete inheritance

•   Added order_by mapper arg reflection to version classes

•   Added support for column_prefix mapper arg

•   Made model builder copy inheritance mapper args to version classes from parent classes

•   Fixed end transaction id setting for join table inheritance classes. Now end transaction id is set explicitly to all tables in inheritance hierarchy.

•   Fixed single table inheritance handling

  1.0-b2 (2014-04-09)

•   Added some schema tools to help migrating between different plugins and versioning strategies

•   Added remove_versioning utility function, see issue #45

•   Added order_by transaction_id default to versions relationship

•   Fixed PropertyModTrackerPlugin association table handling.

•   Fixed get_bind schematics (Flask-SQLAlchemy integration wasn't working)

•   Fixed a bug where committing a session without objects would result in KeyError

•   SQLAlchemy dependency updated to 0.9.4

  1.0-b1 (2014-03-14)

•   Added new plugin architecture

•   Added ActivityPlugin

•   Naming conventions change: History -> Version (to be consistent throughout Continuum)

•   Naming convention change: TransactionLog -> Transaction

•   Rewritten reflected relationship model for version classes. Only dynamic relationships are now reflected as dynamic relationships. Other relationships return either lists or scalars.

•   One-To-One relationship support for reflected version class relationships

•   Removed tx_context context manager. Transaction objects can now be created manually and user has direct access to the parameters of this object.

•   Removed tx_meta context manager. Transaction meta objects can now be created explicitly.

•   Fixed association reverting when the relationship uses uselist=False

•   Fixed one-to-many directed relationship reverting when the relationship uses uselist=False

•   Fixed many-to-many relationship handling when multiple links were created during the same transaction

•   Added indexes to operation_type, transaction_id and end_transaction_id columns of version classes

•   Deprecated extensions

•   SQLAlchemy-Utils dependency updated to 0.25.0

  0.10.3 (2014-02-27)

•   Fixed version next / previous handling

•   SQLAlchemy dependency updated to 0.9.3

•   Fixed column onupdate to history table reflection (issue #47)

  0.10.2 (2014-02-10)

•   Fixed MySQL support (issue #36)

•   Added SQLite and MySQL to testing matrix

  0.10.1 (2013-10-18)

•   Added vacuum function

  0.10.0 (2013-10-09)

•   Validity versioning strategy

•   Changeset supports custom transaction column names

•   Reify -> Revert

•   Fixed revert to support class level column exclusion

  0.9.0 (2013-09-12)

•   Ability to track property modifications

•   New configuration options: track_property_modifications and modified_flag_suffix

  0.8.7 (2013-09-04)

•   Only autoincremented columns marked as autoincrement=False for history tables. This enables alembic migrations to generate without annoying explicit autoincrement=False args.

  0.8.6 (2013-08-21)

•   Custom database schema support added

  0.8.5 (2013-08-01)

•   TSVectorType columns not versioned by default (in order to avoid massive version histories)

  0.8.4 (2013-07-31)

•   Full MySQL and SQLite support added

  0.8.3 (2013-07-29)

•   Fixed UnitOfWork changed entities handling (now checks only for versioned attributes not all object attributes)

•   Fixed UnitOfWork TransactionMeta object creation (now checks if actual modifications were made)

  0.8.2 (2013-07-26)

•   Fixed MySQL history table primary key generation (autoincrement=False now forced for transaction_id column)

  0.8.1 (2013-07-25)

•   Added support for SQLAlchemy-i18n

  0.8.0 (2013-07-25)

•   Added database independent transaction meta parameter handling (formerly supported postgres only)

  0.7.13 (2013-07-24)

•   Smarter is_modified handling for UnitOfWork (now understands excluded properties)

  0.7.12 (2013-07-23)

•   Fixed FlaskVersioningManager schematics when working outside of request context (again)

•   Added possibility to use custom UnitOfWork class

  0.7.11 (2013-07-23)

•   Fixed FlaskVersioningManager schematics when working outside of request context

  0.7.10 (2013-07-23)

•   Fixed is_auto_assigned_date_column (again)

•   Moved some core utility functions to SQLAlchemy-Utils

  0.7.9 (2013-07-23)

•   Fixed is_auto_assigned_date_column

•   Inflection added to requirements

  0.7.8 (2013-07-03)

•   Removed Versioned base class (adding __versioned__ attribute and calling make_versioned() is sufficient for making declarative class versioned)

  0.7.7 (2013-07-03)

•   DateTime columns with defaults excluded by default from history classes

•   Column inclusion added as option

  0.7.6 (2013-07-03)

•   Smarter changeset handling

  0.7.5 (2013-07-03)

•   Improved reify() speed

  0.7.4 (2013-07-03)

•   Fixed changeset when parent contains more columns than version class.

  0.7.3 (2013-06-27)

•   Transaction log and transaction changes records only created if actual net changes were made during transaction.

  0.7.2 (2013-06-27)

•   Removed last references for old revision versioning

  0.7.1 (2013-06-27)

•   Added is_versioned utility function

•   Fixed before operation listeners

  0.7.0 (2013-06-27)

•   Version tables no longer have revision column

•   Parent tables no longer need revision column

•   Version tables primary key is now (parent table pks + transaction_id)

  0.6.8 (2013-06-26)

•   Make versioned join table inherited classes support multiple consecutive flushes per transaction

  0.6.7 (2013-06-26)

•   Fixed association versioning when using executemany

  0.6.6 (2013-06-26)

•   Improved transaction log changed_entities schematics

  0.6.5 (2013-06-26)

•   Added possibility to add lazy values in transaction context meta

  0.6.4 (2013-06-25)

•   Version tables no longer generated when versioning attribute of model set to False

  0.6.3 (2013-06-25)

•   Revision column not nullable in version classes

  0.6.2 (2013-06-25)

•   Fixed relationship building for non-versioned classes

  0.6.1 (2013-06-25)

•   Parent table primary keys remain not nullable in generated version table

  0.6.0 (2013-06-25)

•   Added database agnostic versioning (no need for PostgreSQL specific triggers anymore)

•   Fixed version object relationships (never worked properly in previous versions)

•   New configuration option versioning allows setting the versioning on and off per child class.

•   Added column exclusion

  0.5.1 (2013-06-20)

•   Added improved context managing capabilities for transactions via VersioningManager.tx_context

  0.5.0 (2013-06-20)

•   Removed Versioned base class, versioned objects only need to have __versioned__ defined.

•   Session versioning now part of make_versioned function

•   Added meta parameter in TransactionLog

•   TransactionChanges model for tracking changed entities in given transaction

•   Added Flask extension

  0.4.2 (2013-06-18)

•   Alembic trigger syncing fixed for drop column and add column

  0.4.1 (2013-06-18)

•   Alembic trigger syncing fixed

  0.4.0 (2013-06-18)

•   Added support for multiple updates for same row within single transaction

•   History tables have now own revision column

  0.3.12 (2013-06-18)

•   Not null constraints removed from all reflected columns

•   Fixed reify when parent has not null constraints

•   Added support for reifying deletion

  0.3.11 (2013-06-18)

•   Single table inheritance support added

  0.3.10 (2013-06-18)

•   Generated operation_type column not nullable by default

  0.3.9 (2013-06-18)

•   Added drop_table trigger synchronization

  0.3.8 (2013-06-18)

•   Autoincrementation automatically removed from reflected primary keys

  0.3.7 (2013-06-18)

•   Added identifier quoting for all column names

  0.3.6 (2013-06-18)

•   Identifier quoting for create_trigger_sql

  0.3.5 (2013-06-12)

•   Added alembic operations proxy class

  0.3.4 (2013-06-12)

•   VersioningManager now added in __versioned__ dict of each versioned class

  0.3.3 (2013-06-12)

•   Creating TransactionLog now checks if it already exists.

  0.3.2 (2013-06-12)

•   Added operation_type column to version tables.

  0.3.1 (2013-06-12)

•   Versioned mixin no longer holds lists of pending objects

•   Added VersioningManager for more customizable versioning syntax

  0.3.0 (2013-06-10)

•   Model changesets

•   Fixed previous and next accessors

•   Updates generate versions only if actual changes occur

  0.2.1 (2013-06-10)

•   Added sanity check in all_affected_entities

  0.2.0 (2013-06-10)

•   Added backref relations to TransactionLog

•   Added all_affected_entities property to TransactionLog

  0.1.9 (2013-06-10)

•   Renamed internal attribute __pending__ to __pending_versioned__ in order to avoid variable naming collisions.

  0.1.8 (2013-06-10)

•   Better checking of model table name in scenarios where model does not have __tablename__ defined.

  0.1.7 (2013-06-07)

•   Added make_versioned for more robust declaration of versioned mappers

  0.1.6 (2013-06-07)

•   Added PostgreSQLAdapter class

  0.1.5 (2013-06-07)

•   Made trigger procedures table specific to allow more fine-grained control.

  0.1.4 (2013-06-06)

•   Added column order inspection.

  0.1.3 (2013-06-06)

•   Removed foreign key dependency from version table and transaction table

  0.1.2 (2013-06-06)

•   Fixed packaging

  0.1.1 (2013-06-06)

•   Initial support for join table inheritance

  0.1.0 (2013-06-05)

•   Initial release

LICENSE

Copyright (c) 2012, Konsta Vesterinen

All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

•   Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

•   Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

•   Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Author
Konsta Vesterinen

Copyright
2013, Konsta Vesterinen