Commits

Rufus Pollock committed 52245a6

[sqlalchemy][l]: start of a working changeset setup with sqlachemy model (but *much* still to do).

* Much still to do - lots of tests disabled, m2m does not work properly, do not use ChangeObject.OperationType ...
* sqlachemy/model.py: core code where ChangeObject creation gets done using a SessionExtension
* test/sqlalchemy/demo.py, test/sqlalchemy/test_demo.py: major reworking (lots of tests diabled atm)
* sqlachemy/tools.py: a few tweaks (no proper work - just enough to get things working)
* test/sqlachemy/test_changeset: minor change to avoid duplicate mapping of Changeset, ChangeObject.

  • Participants
  • Parent commits 08f57cb
  • Branches changeset

Comments (0)

Files changed (7)

File vdm/sqlalchemy/__init__.py

     * support for state of revision (active, deleted (spam), in-progress etc)
 
 2. Support for composite primary keys.
+'''
+from .base import SQLAlchemySession
+from .tools import Repository
+from .changeset import Changeset, ChangeObject, setup_changeset
+from .model import VersionedListener
+from .sqla import SQLAlchemyMixin
 
-4. Support for m2m collections other than lists.
-'''
-from base import *
-from tools import Repository
-
-__all__ = [
-        'set_revision', 'get_revision',
-        'make_state_table', 'make_revision_table',
-        'make_table_stateful', 'make_table_revisioned',
-        'make_State', 'make_Revision',
-        'StatefulObjectMixin', 'RevisionedObjectMixin',
-        'Revisioner', 'modify_base_object_mapper', 'create_object_version',
-        'add_stateful_versioned_m2m', 'add_stateful_versioned_m2m_on_version',
-        'Repository'
-        ]
-

File vdm/sqlalchemy/changeset.py

 from sqlalchemy.orm.collections import column_mapped_collection
 
 from vdm.changeset import Changeset as _Changeset, ChangeObject as _ChangeObject
+from .sqla import SQLAlchemyMixin
 
 class JsonType(types.TypeDecorator):
     '''Store data as JSON serializing on save and unserializing on use.
         return JsonTypeTuple(self.impl.length)
 
 
-class Changeset(_Changeset):
+class Changeset(_Changeset, SQLAlchemyMixin):
     @classmethod
     def youngest(self, session):
         '''Get the youngest (most recent) changeset.
         q = session.query(self)
         return q.first()
 
-class ChangeObject(_ChangeObject):
+class ChangeObject(_ChangeObject, SQLAlchemyMixin):
     pass
 
+
 def make_tables(metadata):
     changeset_table = Table('changeset', metadata,
             Column('id', String(40), primary_key=True),
 
     return (changeset_table, change_object_table)
 
+
 def setup_changeset(metadata, mapper):
+    '''Map Changeset and ChangeObject domain objects to associated tables.
+
+    :return: None.
+    '''
     changeset_table, change_object_table = make_tables(metadata)
 
     mapper(Changeset, changeset_table, properties={

File vdm/sqlalchemy/model.py

+"""Versioning (revisioning) for sqlalchemy model objects.
+
+Based partially on:
+
+http://www.sqlalchemy.org/trac/browser/examples/versioning/history_meta.py
+"""
+import logging
+logger = logging.getLogger('vdm')
+
+from sqlalchemy.orm import mapper, class_mapper, attributes, object_mapper
+from sqlalchemy.orm.exc import UnmappedClassError, UnmappedColumnError
+from sqlalchemy import Table, Column, ForeignKeyConstraint, Integer
+from sqlalchemy.orm.interfaces import SessionExtension
+from sqlalchemy.orm.properties import RelationshipProperty
+from sqlalchemy.ext.declarative import DeclarativeMeta
+
+from vdm import json
+from .changeset import ChangeObject
+
+def versioned_objects(iter):
+    for obj in iter:
+        if hasattr(obj, '__history_mapper__'):
+            yield obj
+
+class VersionedMeta(DeclarativeMeta):
+    def __init__(cls, classname, bases, dict_):
+        DeclarativeMeta.__init__(cls, classname, bases, dict_)
+        try:
+            mapper = class_mapper(cls)
+            cls.__history_mapper__ = True
+            set_revisioned_attributes(mapper)
+        except UnmappedClassError:
+            pass
+
+def set_revisioned_attributes(local_mapper):
+    cls = local_mapper.class_
+    # Do the simplest thing possible
+    # TODO: inherited attributes etc
+    # TODO: work out primary key etc
+    # TODO: allow for exclude attributes
+    cols = []
+    for column in local_mapper.local_table.c:
+        col = column.copy()
+        col.unique = False
+        cols.append(col)
+    cls.__revisioned_attributes__ = [ col.key for col in cols ]
+    return cols
+
+def get_object_id(obj):
+    obj_mapper = object_mapper(obj)
+    object_id = [obj.__class__.__name__]
+    for om in obj_mapper.iterate_to_root():
+        for col in om.local_table.c:
+            if col.primary_key:
+                prop = obj_mapper.get_property_by_column(col)
+                val = getattr(obj, prop.key)
+                object_id.append(val)
+    object_id = tuple(object_id)
+    return object_id
+
+# Questions: when does this create the first version
+
+def create_version(obj, session, deleted=False, created=False):
+    obj_mapper = object_mapper(obj)
+    # very inefficient ...
+    if not hasattr(obj, '__revisioned_attributes__'):
+        set_revisioned_attributes(obj_mapper)
+
+    obj_state = attributes.instance_state(obj)
+
+    attr = {}
+
+    obj_changed = False
+
+    for om in obj_mapper.iterate_to_root():
+        for col_key in obj.__revisioned_attributes__:
+
+            obj_col = om.local_table.c[col_key]
+
+            # get the value of the
+            # attribute based on the MapperProperty related to the
+            # mapped column.  this will allow usage of MapperProperties
+            # that have a different keyname than that of the mapped column.
+            try:
+                prop = obj_mapper.get_property_by_column(obj_col)
+            except UnmappedColumnError:
+                # in the case of single table inheritance, there may be 
+                # columns on the mapped table intended for the subclass only.
+                # the "unmapped" status of the subclass column on the 
+                # base class is a feature of the declarative module as of sqla 0.5.2.
+                continue
+
+            # expired object attributes and also deferred cols might not be in the
+            # dict.  force it to load no matter what by using getattr().
+            if prop.key not in obj_state.dict:
+                getattr(obj, prop.key)
+
+            a, u, d = attributes.get_history(obj, prop.key)
+
+            if d:
+                attr[col_key] = d[0]
+                obj_changed = True
+            elif u:
+                attr[col_key] = u[0]
+            else:
+                # if the attribute had no value.
+                attr[col_key] = a[0]
+                obj_changed = True
+
+    if not obj_changed:
+        # not changed, but we have relationships.  OK
+        # check those too
+        for prop in obj_mapper.iterate_properties:
+            if isinstance(prop, RelationshipProperty) and \
+                attributes.get_history(obj, prop.key).has_changes():
+                obj_changed = True
+                break
+
+    if not obj_changed and not deleted and not created:
+        return
+
+    co = ChangeObject()
+    session.add(co)
+
+    ## TODO: address worry that iterator over columns may mean we get pkids in
+    ## different order ...
+    co.object_id = get_object_id(obj)
+
+    ## TODO: set the OperationType properly
+    ## self.operation_type = self.OperationType.CREATE
+    co.data = attr
+    session.revision.manifest.append(co)
+    return attr
+
+
+class VersionedListener(SessionExtension):
+    '''
+
+    Notes
+    =====
+
+    Use after_flush rather than before_flush (as in sqlalchemy example)
+    because:
+
+    In before_flush pks will not be set on objects which have values autoset
+    (e.g. int autoincrement). This in turn will mean that ChangeObject
+    object_id will not be unique.
+    Original sqlalchemy versioning code avoids this by *only* copying objects
+    that are updated or deleted and hence object pks are set (i.e. it does not
+    do anything for creation)
+
+    TODO: is there a danger here that things will not work if we are not using
+    commit (and only flush).
+    '''
+
+    # def before_commit(self, session):
+    # def before_flush(self, session, flush_context, instances):
+    def after_flush(self, session, flush_context):
+        for obj in versioned_objects(session.dirty):
+            create_version(obj, session)
+        for obj in versioned_objects(session.deleted):
+            create_version(obj, session, deleted=True)
+        for obj in versioned_objects(session.new):
+            create_version(obj, session, created=True)
+

File vdm/sqlalchemy/tools.py

 from sqlalchemy.orm import object_session
 from sqlalchemy import __version__ as sqla_version
 
-from base import SQLAlchemySession, State, Revision
+from .base import SQLAlchemySession
+from .changeset import Changeset
 
 class Repository(object):
     '''Manage repository-wide type changes for versioned domain models.
         as transactional (every commit is paired with a begin)
         <http://groups.google.com/group/sqlalchemy/browse_thread/thread/a54ce150b33517db/17587ca675ab3674>
         '''
-        rev = Revision()
+        rev = Changeset()
         self.session.add(rev)
         SQLAlchemySession.set_revision(self.session, rev)             
         return rev
     def youngest_revision(self):
         '''Get the youngest (most recent) revision.'''
         q = self.history()
-        q = q.order_by(Revision.timestamp.desc())
+        q = q.order_by(Changeset.timestamp.desc())
         return q.first()
         
     def history(self):
         
         @return: sqlalchemy query object.
         '''
-        return self.session.query(Revision).filter_by(state=State.ACTIVE)
+        return self.session.query(Changeset).filter_by(state=State.ACTIVE)
 
     def list_changes(self, revision):
         '''List all objects changed by this `revision`.
         Summary of the Algorithm
         ------------------------
 
-        1. list all RevisionObjects affected by this revision
+        1. list all ChangesetObjects affected by this revision
         2. check continuity objects and cascade on everything else ?
             1. crudely get all object revisions associated with this
             2. then check whether this is the only revision and delete the
                 if continuity.revision == revision: # need to change continuity
                     trevobjs = self.session.query(revobj).join('revision').  filter(
                             revobj.continuity==continuity
-                            ).order_by(Revision.timestamp.desc()).limit(2).all()
+                            ).order_by(Changeset.timestamp.desc()).limit(2).all()
                     if len(trevobjs) == 0:
                         raise Exception('Should have at least one revision.')
                     if len(trevobjs) == 1:

File vdm/test/sqlalchemy/demo.py

 from datetime import datetime
 import logging
 logger = logging.getLogger('vdm')
+import uuid
+def uuidstr(): return str(uuid.uuid4())
 
 from sqlalchemy import *
 from sqlalchemy import __version__ as sqla_version
+from sqlalchemy.ext.associationproxy import association_proxy
 # from sqlalchemy import create_engine
 
 import vdm.sqlalchemy
+from vdm.sqlalchemy import Changeset, ChangeObject, VersionedListener
 
 TEST_ENGINE = "postgres"  # or "sqlite"
 
 
 metadata = MetaData(bind=engine)
 
-## VDM-specific tables
-
-revision_table = vdm.sqlalchemy.make_revision_table(metadata)
-
 ## Demo tables
 
 license_table = Table('license', metadata,
         Column('open', Boolean),
         )
 
-import uuid
-def uuidstr(): return str(uuid.uuid4())
 package_table = Table('package', metadata,
         # Column('id', Integer, primary_key=True),
         Column('id', String(36), default=uuidstr, primary_key=True),
         )
 
 
-vdm.sqlalchemy.make_table_stateful(license_table)
-vdm.sqlalchemy.make_table_stateful(package_table)
-vdm.sqlalchemy.make_table_stateful(tag_table)
-vdm.sqlalchemy.make_table_stateful(package_tag_table)
-license_revision_table = vdm.sqlalchemy.make_revisioned_table(license_table)
-package_revision_table = vdm.sqlalchemy.make_revisioned_table(package_table)
-# TODO: this has a composite primary key ...
-package_tag_revision_table = vdm.sqlalchemy.make_revisioned_table(package_tag_table)
-
-
-
 ## -------------------
 ## Mapped classes
 
         
-class License(vdm.sqlalchemy.RevisionedObjectMixin,
-    vdm.sqlalchemy.StatefulObjectMixin,
-    vdm.sqlalchemy.SQLAlchemyMixin
-    ):
-    def __init__(self, **kwargs):
-        for k,v in kwargs.items():
-            setattr(self, k, v)
+class License(vdm.sqlalchemy.SQLAlchemyMixin):
+    __history_mapper__ = True
 
-class Package(vdm.sqlalchemy.RevisionedObjectMixin,
-        vdm.sqlalchemy.StatefulObjectMixin,
-        vdm.sqlalchemy.SQLAlchemyMixin
-        ):
+class Package(vdm.sqlalchemy.SQLAlchemyMixin):
+    __history_mapper__ = True
 
-    def __init__(self, **kwargs):
-        for k,v in kwargs.items():
-            setattr(self, k, v)
+    # TODO: reinstate m2m tests ...
+    # tags = association_proxy('package_tags', 'tag')
 
 
 class Tag(vdm.sqlalchemy.SQLAlchemyMixin):
         self.name = name
 
 
-class PackageTag(vdm.sqlalchemy.RevisionedObjectMixin,
-        vdm.sqlalchemy.StatefulObjectMixin,
-        vdm.sqlalchemy.SQLAlchemyMixin
-        ):
-    def __init__(self, package=None, tag=None, state=None, **kwargs):
-        logger.debug('PackageTag.__init__: %s, %s' % (package, tag))
-        self.package = package
+class PackageTag(vdm.sqlalchemy.SQLAlchemyMixin):
+    def __init__(self, tag=None, **kwargs):
+        logger.debug('PackageTag.__init__: %s' % (tag))
         self.tag = tag
-        self.state = state
         for k,v in kwargs.items():
             setattr(self, k, v)
 
 Session = scoped_session(
             sessionmaker(autoflush=True,
             expire_on_commit=False,
-            autocommit=False
+            autocommit=False,
+            # Where we introduced the revisioning/versioning
+            extension=VersionedListener()
             ))
 
 # mapper = Session.mapper
 from sqlalchemy.orm import mapper
 
-# VDM-specific domain objects
-State = vdm.sqlalchemy.State
-Revision = vdm.sqlalchemy.make_Revision(mapper, revision_table)
+# Sets up tables and maps ChangeObject and Changeset
+vdm.sqlalchemy.setup_changeset(metadata, mapper)
+
 
 mapper(License, license_table, properties={
-    },
-    extension=vdm.sqlalchemy.Revisioner(license_revision_table)
-    )
+    })
 
 mapper(Package, package_table, properties={
     'license':relation(License),
     # do we want lazy=False here? used in:
     # <http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/examples/association/proxied_association.py>
     'package_tags':relation(PackageTag, backref='package', cascade='all'), #, delete-orphan'),
-    },
-    extension = vdm.sqlalchemy.Revisioner(package_revision_table)
-    )
+    })
 
 mapper(Tag, tag_table)
 
 mapper(PackageTag, package_tag_table, properties={
     'tag':relation(Tag),
-    },
-    extension = vdm.sqlalchemy.Revisioner(package_tag_revision_table)
-    )
-
-vdm.sqlalchemy.modify_base_object_mapper(Package, Revision, State)
-vdm.sqlalchemy.modify_base_object_mapper(License, Revision, State)
-vdm.sqlalchemy.modify_base_object_mapper(PackageTag, Revision, State)
-PackageRevision = vdm.sqlalchemy.create_object_version(mapper, Package,
-        package_revision_table)
-LicenseRevision = vdm.sqlalchemy.create_object_version(mapper, License,
-        license_revision_table)
-PackageTagRevision = vdm.sqlalchemy.create_object_version(mapper, PackageTag,
-        package_tag_revision_table)
-
-from vdm.sqlalchemy import add_stateful_versioned_m2m 
-vdm.sqlalchemy.add_stateful_versioned_m2m(Package, PackageTag, 'tags', 'tag',
-        'package_tags')
-vdm.sqlalchemy.add_stateful_versioned_m2m_on_version(PackageRevision, 'tags')
+    })
 
 ## ------------------------
 ## Repository helper object

File vdm/test/sqlalchemy/test_changeset.py

 import json
-from sqlalchemy import *
-from sqlalchemy.orm import scoped_session, sessionmaker, create_session, mapper
+from sqlalchemy import MetaData, create_engine
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper, object_mapper
+from sqlalchemy.orm.exc import UnmappedInstanceError
 
-from vdm.sqlalchemy.changeset import *
+from vdm.sqlalchemy.changeset import Changeset, ChangeObject, setup_changeset
 Session = scoped_session(
     sessionmaker(autoflush=True, expire_on_commit=False, autocommit=False)
     )
     def setup_class(self):
         engine = create_engine('sqlite://')
         metadata = MetaData(bind=engine)
-        setup_changeset(metadata, mapper)
+        # to avoid conflicts due to having already called setup_changeset in e.g. demo.py
+        try:
+            object_mapper(Changeset())
+        except UnmappedInstanceError:
+            setup_changeset(metadata, mapper)
         metadata.create_all()
 
     def test_01(self):

File vdm/test/sqlalchemy/test_demo.py

 from sqlalchemy.orm import object_session, class_mapper
 
 import vdm.sqlalchemy
+from vdm.sqlalchemy import Changeset, ChangeObject
+from vdm.sqlalchemy.model import get_object_id
 from demo import *
 
 _clear = Session.expunge_all
+
+def all_revisions(obj):
+    objid = get_object_id(obj)
+    alldata = Session.query(ChangeObject).all()
+    out = Session.query(ChangeObject).filter_by(object_id=objid
+            ).join('changeset'
+                ).order_by(Changeset.timestamp.desc())
+    return list(out)
     
 class Test_01_SQLAlchemySession:
     @classmethod
     def test_1(self):
         assert not hasattr(Session, 'revision')
         assert vdm.sqlalchemy.SQLAlchemySession.at_HEAD(Session)
-        rev = Revision()
+        rev = Changeset()
         vdm.sqlalchemy.SQLAlchemySession.set_revision(Session, rev)
         assert vdm.sqlalchemy.SQLAlchemySession.at_HEAD(Session)
         assert Session.revision is not None
 
         logger.debug('===== STARTING REV 1')
         session = Session()
-        rev1 = Revision()
+        rev1 = Changeset()
         session.add(rev1)
         vdm.sqlalchemy.SQLAlchemySession.set_revision(session, rev1)
 
         self.notes1 = u'Here\nare some\nnotes'
         self.notes2 = u'Here\nare no\nnotes'
         lic1 = License(name='blah', open=True)
-        lic1.revision = rev1
         lic2 = License(name='foo', open=True)
         p1 = Package(name=self.name1, title=self.title1, license=lic1, notes=self.notes1)
         p2 = Package(name=self.name2, title=self.title1, license=lic1)
 
         logger.debug('===== STARTING REV 2')
         session = Session()
-        rev2 = Revision()
+        rev2 = Changeset()
         session.add(rev2)
         vdm.sqlalchemy.SQLAlchemySession.set_revision(session, rev2)
         outlic1 = Session.query(License).filter_by(name='blah').first()
         t1 = Tag(name='geo')
         session.add_all([outp1,outp2,t1])
         outp1.tags = [t1]
-        outp2.delete()
+        Session.delete(outp2)
         # session.flush()
         session.commit()
         # must do this after flush as timestamp not set until then
 
     @classmethod
     def teardown_class(self):
+        repo.rebuild_db()
         Session.remove()
 
     def test_01_revisions_exist(self):
-        revs = Session.query(Revision).all()
+        revs = Session.query(Changeset).all()
         assert len(revs) == 2
         # also check order (youngest first)
+        print [ rev.timestamp for rev in revs ]
         assert revs[0].timestamp > revs[1].timestamp
 
     def test_02_revision_youngest(self):
-        rev = Revision.youngest(Session)
+        rev = Changeset.youngest(Session)
         assert rev.timestamp == self.ts2
 
     def test_03_basic(self):
         assert Session.query(License).count() == 2, Session.query(License).count()
-        assert Session.query(Package).count() == 2, Session.query(Package).count()
-        assert hasattr(LicenseRevision, 'revision_id')
-        assert Session.query(LicenseRevision).count() == 3, Session.query(LicenseRevision).count()
-        assert Session.query(PackageRevision).count() == 4, Session.query(PackageRevision).count()
+        assert Session.query(Package).count() == 1, Session.query(Package).count()
 
     def test_04_all_revisions(self):
         p1 = Session.query(Package).filter_by(name=self.name1).one()
-        assert len(p1.all_revisions) == 2
+        assert len(all_revisions(p1)) == 2
         # problem here is that it might pass even if broken because ordering of
         # uuid ids is 'right' 
-        revs = [ pr.revision for pr in p1.all_revisions ]
+        revs = [ pr.changeset for pr in all_revisions(p1) ]
         assert revs[0].timestamp > revs[1].timestamp, revs
 
     def test_05_basic_2(self):
         # should be at HEAD (i.e. rev2) by default 
         p1 = Session.query(Package).filter_by(name=self.name1).one()
         assert p1.license.open == False
-        assert p1.revision.timestamp == self.ts2
-        # assert p1.tags == []
-        assert len(p1.tags) == 1
+        # TODO: reinstate tags tests ...
+        # assert len(p1.tags) == 1
 
-    def test_06_basic_continuity(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        pr1 = Session.query(PackageRevision).filter_by(name=self.name1).first()
-        table = class_mapper(PackageRevision).mapped_table
-        print table.c.keys()
-        print pr1.continuity_id
-        assert pr1.continuity == p1
+#    def test_07_basic_state(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        p2 = Session.query(Package).filter_by(name=self.name2).one()
+#        changeobjects = all_revisions(p1)
+#        assert changeobjects[-1]
+#        assert p1.state
+#        assert p1.state == State.ACTIVE
+#        assert p2.state == State.DELETED
+#
+#    def test_08_versioning_0(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        p1r1 = p1.get_as_of(rev1)
+#        assert p1r1.continuity == p1
+#
+#    def test_09_versioning_1(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        p1r1 = p1.get_as_of(rev1)
+#        assert p1r1.name == self.name1
+#        assert p1r1.title == self.title1
+#
+#    def test_10_traversal_normal_fks_and_state_at_same_time(self):
+#        p2 = Session.query(Package).filter_by(name=self.name2).one()
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        p2r1 = p2.get_as_of(rev1)
+#        assert p2r1.state == State.ACTIVE
+#
+#    def test_11_versioning_traversal_fks(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        p1r1 = p1.get_as_of(rev1)
+#        assert p1r1.license.open == True
+#
+#    def test_12_versioning_m2m_1(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        ptag = p1.package_tags[0]
+#        # does not exist
+#        assert ptag.get_as_of(rev1) == None
+#
+#    def test_13_versioning_m2m(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        p1r1 = p1.get_as_of(rev1)
+#        assert len(p1.tags_active) == 0
+#        # NB: deleted includes tags that were non-existent
+#        assert len(p1.tags_deleted) == 1
+#        assert len(p1.tags) == 0
+#        assert len(p1r1.tags) == 0
+#    
+#    def test_14_revision_has_state(self):
+#        rev1 = Session.query(Revision).get(self.rev1_id)
+#        assert rev1.state == State.ACTIVE
+#
+#    def test_15_diff(self):
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        pr2, pr1 = all_revisions(p1)
+#        # pr1, pr2 = prs[::-1]
+#        
+#        diff = p1.diff_revisioned_fields(pr2, pr1, Package)
+#        assert diff['title'] == '- XYZ\n+ ABC', diff['title']
+#        assert diff['notes'] == '  Here\n- are some\n+ are no\n  notes', diff['notes']
+#        assert diff['license_id'] == '- 1\n+ 2', diff['license_id']
+#
+#        diff1 = p1.diff(pr2.changeset, pr1.changeset)
+#        assert diff1 == diff, (diff1, diff)
+#
+#        diff2 = p1.diff()
+#        assert diff2 == diff, (diff2, diff)
+#
+#    def test_16_diff_2(self):
+#        '''Test diffing at a revision where just created.'''
+#        p1 = Session.query(Package).filter_by(name=self.name1).one()
+#        pr2, pr1 = all_revisions(p1)
+#
+#        diff1 = p1.diff(to_revision=pr1.changeset)
+#        assert diff1['title'] == u'- None\n+ XYZ', diff1
 
-    def test_07_basic_state(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        p2 = Session.query(Package).filter_by(name=self.name2).one()
-        assert p1.state
-        assert p1.state == State.ACTIVE
-        assert p2.state == State.DELETED
 
-    def test_08_versioning_0(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        p1r1 = p1.get_as_of(rev1)
-        assert p1r1.continuity == p1
-
-    def test_09_versioning_1(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        p1r1 = p1.get_as_of(rev1)
-        assert p1r1.name == self.name1
-        assert p1r1.title == self.title1
-
-    def test_10_traversal_normal_fks_and_state_at_same_time(self):
-        p2 = Session.query(Package).filter_by(name=self.name2).one()
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        p2r1 = p2.get_as_of(rev1)
-        assert p2r1.state == State.ACTIVE
-
-    def test_11_versioning_traversal_fks(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        p1r1 = p1.get_as_of(rev1)
-        assert p1r1.license.open == True
-
-    def test_12_versioning_m2m_1(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        ptag = p1.package_tags[0]
-        # does not exist
-        assert ptag.get_as_of(rev1) == None
-
-    def test_13_versioning_m2m(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        p1r1 = p1.get_as_of(rev1)
-        assert len(p1.tags_active) == 0
-        # NB: deleted includes tags that were non-existent
-        assert len(p1.tags_deleted) == 1
-        assert len(p1.tags) == 0
-        assert len(p1r1.tags) == 0
-    
-    def test_14_revision_has_state(self):
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        assert rev1.state == State.ACTIVE
-
-    def test_15_diff(self):
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        pr2, pr1 = p1.all_revisions
-        # pr1, pr2 = prs[::-1]
-        
-        diff = p1.diff_revisioned_fields(pr2, pr1, Package)
-        assert diff['title'] == '- XYZ\n+ ABC', diff['title']
-        assert diff['notes'] == '  Here\n- are some\n+ are no\n  notes', diff['notes']
-        assert diff['license_id'] == '- 1\n+ 2', diff['license_id']
-
-        diff1 = p1.diff(pr2.revision, pr1.revision)
-        assert diff1 == diff, (diff1, diff)
-
-        diff2 = p1.diff()
-        assert diff2 == diff, (diff2, diff)
-
-    def test_16_diff_2(self):
-        '''Test diffing at a revision where just created.'''
-        p1 = Session.query(Package).filter_by(name=self.name1).one()
-        pr2, pr1 = p1.all_revisions
-
-        diff1 = p1.diff(to_revision=pr1.revision)
-        assert diff1['title'] == u'- None\n+ XYZ', diff1
-
-
-class Test_03_StatefulVersioned:
+class _Test_03_StatefulVersioned:
     @classmethod
     def setup_class(self):
         repo.rebuild_db()
         Session.remove()
         
         # now remove those tags
-        logger.debug('====== start Revision 2')
+        logger.debug('====== start Changeset 2')
         rev2 = repo.new_revision()
         newp1 = Session.query(Package).filter_by(name=self.name1).one()
         # either one works
         Session.remove()
 
         # now add one of them back
-        logger.debug('====== start Revision 3')
+        logger.debug('====== start Changeset 3')
         rev3 = repo.new_revision()
         newp1 = Session.query(Package).filter_by(name=self.name1).one()
         self.tagname1 = 'geo'
         Session.remove()
 
     def test_1_underlying_is_right(self):
-        rev1 = Session.query(Revision).get(self.rev1_id)
-        ptrevs = Session.query(PackageTagRevision).filter_by(revision_id=rev1.id).all()
+        rev1 = Session.query(Changeset).get(self.rev1_id)
+        ptrevs = Session.query(PackageTagChangeset).filter_by(revision_id=rev1.id).all()
         assert len(ptrevs) == 2
         for pt in ptrevs:
             assert pt.state == State.ACTIVE
 
-        rev2 = Session.query(Revision).get(self.rev2_id)
-        ptrevs = Session.query(PackageTagRevision).filter_by(revision_id=rev2.id).all()
+        rev2 = Session.query(Changeset).get(self.rev2_id)
+        ptrevs = Session.query(PackageTagChangeset).filter_by(revision_id=rev2.id).all()
         assert len(ptrevs) == 2
         for pt in ptrevs:
             assert pt.state == State.DELETED
     # show up
     def test_2_get_as_of(self):
         p1 = Session.query(Package).filter_by(name=self.name1).one()
-        rev2 = Session.query(Revision).get(self.rev2_id)
+        rev2 = Session.query(Changeset).get(self.rev2_id)
         # should be 2 deleted and 1 as None
         ptrevs = [ pt.get_as_of(rev2) for pt in p1.package_tags ]
         print ptrevs
-        print Session.query(PackageTagRevision).all()
-        assert ptrevs[0].revision_id == rev2.id
+        print Session.query(PackageTagChangeset).all()
+        assert ptrevs[0].changeset_id == rev2.id
 
     def test_3_remove_and_readd_m2m_2(self):
         num_package_tags = 2
-        rev1 = Session.query(Revision).get(self.rev1_id)
+        rev1 = Session.query(Changeset).get(self.rev1_id)
         p1 = Session.query(Package).filter_by(name=self.name1).one()
         p1rev = p1.get_as_of(rev1)
         # NB: relations on revision object proxy to continuity
         assert len(p1rev.tags) == 2
         Session.remove()
 
-        rev2 = Session.query(Revision).get(self.rev2_id)
+        rev2 = Session.query(Changeset).get(self.rev2_id)
         p1 = Session.query(Package).filter_by(name=self.name1).one()
         p2rev = p1.get_as_of(rev2)
-        assert p2rev.__class__ == PackageRevision
+        assert p2rev.__class__ == PackageChangeset
         assert len(p2rev.package_tags) == num_package_tags
         print rev2.id
         print p2rev.tags_active
         assert len(p2rev.tags) == 0
 
 
-class Test_04_StatefulVersioned2:
+class _Test_04_StatefulVersioned2:
     '''Similar to previous but setting m2m list using existing objects'''
 
     def setup(self):
         self._test_tags()
 
 
-class Test_05_RevertAndPurge:
+class _Test_05_RevertAndPurge:
 
     @classmethod
     def setup_class(self):
         Session.remove()
         repo.rebuild_db()
 
-        rev1 = Revision()
+        rev1 = Changeset()
         Session.add(rev1)
         vdm.sqlalchemy.SQLAlchemySession.set_revision(Session, rev1)
         
         repo.rebuild_db()
 
     def test_basics(self):
-        revs = Session.query(Revision).all()
+        revs = Session.query(Changeset).all()
         assert len(revs) == 2
         p1 = Session.query(Package).filter_by(name=self.name2).one()
         assert p1.name == self.name2
         assert len(Session.query(Package).all()) == 2
 
     def test_list_changes(self):
-        rev2 = Session.query(Revision).get(self.rev2id)
+        rev2 = Session.query(Changeset).get(self.rev2id)
         out = repo.list_changes(rev2)
         assert len(out) == 3
         assert len(out[Package]) == 1, out
     def test_purge_revision(self):
         logger.debug('BEGINNING PURGE REVISION')
         Session.remove()
-        rev2 = Session.query(Revision).get(self.rev2id)
+        rev2 = Session.query(Changeset).get(self.rev2id)
         repo.purge_revision(rev2)
-        revs = Session.query(Revision).all()
+        revs = Session.query(Changeset).all()
         assert len(revs) == 1
         p1 = Session.query(Package).filter_by(name=self.name1).first()
         assert p1 is not None
         assert len(Session.query(License).all()) == 0
         pkgs = Session.query(Package).all()
         assert len(pkgs) == 2, pkgrevs
-        pkgrevs = Session.query(PackageRevision).all()
+        pkgrevs = Session.query(PackageChangeset).all()
         assert len(pkgrevs) == 2, pkgrevs