Changeset 358
- Timestamp:
- 07/11/08 10:55:58 (5 years ago)
- Location:
- elixir/trunk/elixir
- Files:
-
- 2 modified
-
ext/versioned.py (modified) (11 diffs)
-
properties.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
elixir/trunk/elixir/ext/versioned.py
r336 r358 2 2 A versioning plugin for Elixir. 3 3 4 Entities that are marked as versioned with the `acts_as_versioned` statement 4 Entities that are marked as versioned with the `acts_as_versioned` statement 5 5 will automatically have a history table created and a timestamp and version 6 column added to their tables. In addition, versioned entities are provided 7 with four new methods: revert, revert_to, compare_with and get_as_of, and one 6 column added to their tables. In addition, versioned entities are provided 7 with four new methods: revert, revert_to, compare_with and get_as_of, and one 8 8 new attribute: versions. Entities with compound primary keys are supported. 9 9 … … 32 32 33 33 Also included in the module is a `after_revert` decorator that can be used to 34 decorate methods on the versioned entity that will be called following that 34 decorate methods on the versioned entity that will be called following that 35 35 instance being reverted. 36 36 37 The acts_as_versioned statement also accepts an optional `ignore` argument 38 that consists of a list of strings, specifying names of fields. Changes in 37 The acts_as_versioned statement also accepts an optional `ignore` argument 38 that consists of a list of strings, specifying names of fields. Changes in 39 39 those fields will not result in a version increment. In addition, you can 40 40 pass in an optional `check_concurrent` argument, which will use SQLAlchemy's 41 built-in optimistic concurrency mechanisms. 41 built-in optimistic concurrency mechanisms. 42 42 43 43 Note that relationships that are stored in mapping tables will not be included … … 55 55 from elixir import Integer, DateTime 56 56 from elixir.statements import Statement 57 from elixir.properties import EntityBuilder 57 58 58 59 __all__ = ['acts_as_versioned', 'after_revert'] … … 90 91 instance.timestamp = datetime.now() 91 92 return EXT_CONTINUE 92 93 93 94 def before_update(self, mapper, connection, instance): 94 values = instance.table.select(get_entity_where(instance)).execute().fetchone() 95 old_values = instance.table.select(get_entity_where(instance)) \ 96 .execute().fetchone() 95 97 96 98 # SA might've flagged this for an update even though it didn't change. … … 103 105 if key in ignored: 104 106 continue 105 if getattr(instance, key) != values[key]:107 if getattr(instance, key) != old_values[key]: 106 108 # the instance was really updated, so we create a new version 107 colvalues = dict(values.items()) 108 connection.execute(instance.__class__.__history_table__.insert(), colvalues) 109 dict_values = dict(old_values.items()) 110 connection.execute( 111 instance.__class__.__history_table__.insert(), dict_values) 109 112 instance.version = instance.version + 1 110 113 instance.timestamp = datetime.now() … … 112 115 113 116 return EXT_CONTINUE 114 117 115 118 def before_delete(self, mapper, connection, instance): 116 119 connection.execute(instance.__history_table__.delete( … … 127 130 # 128 131 129 class VersionedEntityBuilder( object):130 132 class VersionedEntityBuilder(EntityBuilder): 133 131 134 def __init__(self, entity, ignore=[], check_concurrent=False): 132 entity._descriptor.add_mapper_extension(versioned_mapper_extension)133 135 self.entity = entity 136 self.add_mapper_extension(versioned_mapper_extension) 137 #TODO: we should rather check that the version_id_col isn't set 138 # externally 134 139 self.check_concurrent = check_concurrent 135 140 136 141 # Changes in these fields will be ignored 142 ignore.extend(['version', 'timestamp']) 137 143 entity.__ignored_fields__ = ignore 138 entity.__ignored_fields__.extend(['version', 'timestamp']) 139 144 140 145 def create_non_pk_cols(self): 141 146 # add a version column to the entity, along with a timestamp 142 version_col = Column('version', Integer) 143 timestamp_col = Column('timestamp', DateTime) 144 self.entity._descriptor.add_column(version_col) 145 self.entity._descriptor.add_column(timestamp_col) 146 147 self.add_table_column(Column('version', Integer)) 148 self.add_table_column(Column('timestamp', DateTime)) 149 147 150 # add a concurrent_version column to the entity, if required 148 151 if self.check_concurrent: 149 152 self.entity._descriptor.version_id_col = 'concurrent_version' 150 153 151 154 # we copy columns from the main entity table, so we need it to exist first 152 155 def after_table(self): 153 156 entity = self.entity 154 157 155 158 # look for events 156 159 after_revert_events = [] … … 158 161 if getattr(func, '_elixir_after_revert', False): 159 162 after_revert_events.append(func) 160 163 161 164 # create a history table for the entity 162 165 #TODO: fail more noticeably in case there is a version col 163 166 columns = [ 164 column.copy() for column in entity.table.c 167 column.copy() for column in entity.table.c 165 168 if column.name not in ('version', 'concurrent_version') 166 169 ] 167 170 columns.append(Column('version', Integer, primary_key=True)) 168 table = Table(entity.table.name + '_history', entity.table.metadata, 171 table = Table(entity.table.name + '_history', entity.table.metadata, 169 172 *columns 170 173 ) 171 174 entity.__history_table__ = table 172 175 173 176 # create an object that represents a version of this entity 174 177 class Version(object): 175 178 pass 176 179 177 180 # map the version class to the history table for this entity 178 181 Version.__name__ = entity.__name__ + 'Version' 179 182 Version.__versioned_entity__ = entity 180 183 mapper(Version, entity.__history_table__) 181 184 182 185 # attach utility methods and properties to the entity 183 186 def get_versions(self): … … 190 193 v.append(self) 191 194 return v 192 195 193 196 def get_as_of(self, dt): 194 197 # if the passed in timestamp is older than our current version's … … 196 199 if self.timestamp < dt: 197 200 return self 198 201 199 202 # otherwise, we need to look to the history table to get our 200 203 # older version 201 query = object_session(self).query(Version) 202 query = query.filter(and_(get_history_where(self), 203 Version.timestamp <= dt)) 204 query = query.order_by(desc(Version.timestamp)).limit(1) 204 sess = object_session(self) 205 query = sess.query(Version) \ 206 .filter(and_(get_history_where(self), 207 Version.timestamp <= dt)) \ 208 .order_by(desc(Version.timestamp)).limit(1) 205 209 return query.first() 206 210 207 211 def revert_to(self, to_version): 208 212 if isinstance(to_version, Version): 209 213 to_version = to_version.version 210 214 211 215 hist = entity.__history_table__ 212 216 old_version = hist.select(and_( 213 get_history_where(self), 217 get_history_where(self), 214 218 hist.c.version == to_version 215 219 )).execute().fetchone() 216 220 217 221 entity.table.update(get_entity_where(self)).execute( 218 222 dict(old_version.items()) 219 223 ) 220 221 hist.delete(and_(get_history_where(self), 224 225 hist.delete(and_(get_history_where(self), 222 226 hist.c.version >= to_version)).execute() 223 227 self.expire() 224 for event in after_revert_events: 228 for event in after_revert_events: 225 229 event(self) 226 230 227 231 def revert(self): 228 232 assert self.version > 1 229 233 self.revert_to(self.version - 1) 230 234 231 235 def compare_with(self, version): 232 236 differences = {} … … 239 243 differences[column.name] = (this, that) 240 244 return differences 241 245 242 246 entity.versions = property(get_versions) 243 247 entity.get_as_of = get_as_of -
elixir/trunk/elixir/properties.py
r350 r358 78 78 79 79 # helper methods 80 def add_table_column(self, column): 81 self.entity._descriptor.add_column(column) 82 80 83 def add_mapper_property(self, name, prop): 81 84 self.entity._descriptor.add_property(name, prop) 82 85 83 def add_ table_column(self, column):84 self.entity._descriptor.add_ column(column)86 def add_mapper_extension(self, ext): 87 self.entity._descriptor.add_mapper_extension(ext) 85 88 86 89
