def get_or_create_entity_history(entity, field, clock): """Gets an existing or creates a new entity history model for an entity temporal field. """ new_value = getattr(entity, field) history_cls = entity._history[field] history_entity = session.query(history_cls).filter( history_cls.entity_id == entity.rid, history_cls.clock_id == clock.rid, history_cls.ticks.contains(clock.ticks), ).one_or_none() if not new_value: return history_entity, False if history_entity: if history_entity.value == new_value: return history_entity, False history_entity.ticks = pg_extras.NumericRange( history_entity.ticks.lower, clock.ticks, '[]') new_history_entity = history_cls() new_history_entity.entity = entity new_history_entity.clock = clock new_history_entity.ticks = pg_extras.NumericRange(clock.ticks + 1) new_history_entity.value = new_value session.add(new_history_entity) return new_history_entity, True
def int_range(lower=0, upper=None, nullable=False): """Postgres Int4Range column mapping. """ return sa.Column( postgresql.INT4RANGE(), default=pg_extras.NumericRange(lower, upper), nullable=nullable)
def create_field_history(self, entity, field, clock, value): """creates a new entity field history. """ history = entity._history[field]() history.entity = entity history.clock = clock history.ticks = pg_extras.NumericRange(clock.ticks + 1) history.value = value # self.add(history) return history
def create_field_history_bulk(self, entity, field, clock, value): """creates a new entity field history. """ history = entity._history[field]() history.rid = uuid4() history.entity_id = entity.rid history.clock_id = clock.rid history.ticks = pg_extras.NumericRange(clock.ticks + 1) history.value = getattr(entity, field) return history, True
def make_clock(effective_lower: dt.datetime, vclock_lower: int, **kwargs) -> _ClockSet: """construct a clock set tuple""" effective_upper = kwargs.get('effective_upper', None) vclock_upper = kwargs.get('vclock_upper', None) effective = psql_extras.DateTimeTZRange(effective_lower, effective_upper) vclock = psql_extras.NumericRange(vclock_lower, vclock_upper) return _ClockSet(effective, vclock)
def get_or_create_field_history(self, entity, field, clock): """gets an existing or creates a new entity history field for a temporal model. """ new_value = getattr(entity, field) #if not new_value: # return None, False if entity.rid: history = self.query(entity.__class__).field_history( entity, field, clock) if history: if history.value == new_value: return history, False history.ticks = pg_extras.NumericRange(history.ticks.lower, clock.ticks, '[]') new_history = self.create_field_history(entity, field, clock, new_value) return new_history, True
def _record_history(self, clocked: Clocked): """ Record all history for a given clocked object Args: clocked (Clocked): instance of clocked object """ # # Check for activity misuse # if clocked.temporal_options.activity_model is not None and clocked.activity is None: raise ValueError('An activity is required when saving a %s' % type(clocked).__name__) if clocked.temporal_options.activity_model is None and clocked.activity is not None: raise ValueError( 'There is no activity model for %s; you cannot supply an activity' % type(clocked).__name__) # # Determine which fields have changed # changed_fields = {} for field, history_model in self.history_models.items(): new_val = clocked._meta.get_field(field).value_from_object(clocked) prev_val = clocked._state._django_temporal_previous[field] if new_val != prev_val or clocked._state._django_temporal_add: changed_fields[(field, history_model)] = new_val if not changed_fields: return # # Increment the clock and build the effective and vclock ranges for the next tick # timestamp = timezone.now() clocked.vclock += 1 new_tick = clocked.vclock # # Create the EntityClock for this tick # clock_model = type(clocked).temporal_options.clock_model if clocked.temporal_options.activity_model is not None: clock = clock_model(entity=clocked, activity=clocked.activity, tick=new_tick) else: clock = clock_model(entity=clocked, tick=new_tick) clock.save() # # Create the field history for this tick # for (field, history_model), new_val in changed_fields.items(): if new_tick > 1: # This is an update, not a create, so update the upper bounds of the previous tick. # # This cannot be done with F expressions because of the range updates, unless we write our # own implementations of int4range/tstzrange. # # Instead use .raw and force execution by wrapping with list(). This approach ensures that # the update happens in the correct connection/transaction. list( history_model.objects.raw( """ UPDATE {table_name} SET vclock = int4range(lower(vclock), %s), effective = tstzrange(lower(effective), %s) WHERE entity_id = %s AND upper(vclock) IS NULL RETURNING id; """.format(table_name=connection.ops.quote_name( history_model._meta.db_table)), [new_tick, timestamp, clocked.pk])) hist = history_model(**{field: new_val}, entity=clocked, vclock=psql_extras.NumericRange( new_tick, None), effective=psql_extras.DateTimeTZRange( timestamp, None)) hist.save() # Update the stored state for this field to detect future changes clocked._state._django_temporal_previous[field] = new_val # Reset the activity so it can't be accidentally reused easily clocked.activity = None # Update the vclock value without triggering a recursive `record_history` type(clocked).objects \ .filter(**{clocked._meta.pk.name: getattr(clocked, clocked._meta.pk.name)}) \ .update(vclock=new_tick)