<commit_before>require 'active_record'

module ChronoModel

  module TimeMachine
    extend ActiveSupport::Concern

    included do
      if table_exists? && !chrono?
        puts "WARNING: #{table_name} is not a temporal table. " \
          "Please use change_table :#{table_name}, :temporal => true"
      end

      history = TimeMachine.define_history_model_for(self)
      TimeMachine.chrono_models[table_name] = history

      # STI support. TODO: more thorough testing
      #
      def self.inherited(subclass)
        super

        # Do not smash stack: as the below method is defining a
        # new anonymous class, without this check this leads to
        # infinite recursion.
        unless subclass.name.nil?
          TimeMachine.define_inherited_history_model_for(subclass)
        end
      end
    end

    # Returns an Hash keyed by table name of ChronoModels
    #
    def self.chrono_models
      (@chrono_models ||= {})
    end

    def self.define_history_model_for(model)
      history = Class.new(model) do
        self.table_name = [Adapter::HISTORY_SCHEMA, model.table_name].join('.')

        extend TimeMachine::HistoryMethods

        # The history id is `hid`, but this cannot set as primary key
        # or temporal assocations will break. Solutions are welcome.
        def id
          hid
        end

        # Referenced record ID.
        #
        def rid
          attributes[self.class.primary_key]
        end

        # HACK to make ActiveAdmin work properly. This will be surely
        # better written in the future.
        #
        def self.find(*args)
          old = self.primary_key
          self.primary_key = :hid
          super
        ensure
          self.primary_key = old
        end

        # Returns the previous history entry, or nil if this
        # is the first one.
        #
        def pred
          return if self.valid_from.nil?

          if self.class.timeline_associations.empty?
            self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first
          else
            super(:id => rid, :before => valid_from, :table => self.class.superclass.quoted_table_name)
          end
        end

        # Returns the next history entry, or nil if this is the
        # last one.
        #
        def succ
          return if self.valid_to.nil?

          if self.class.timeline_associations.empty?
            self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first
          else
            super(:id => rid, :after => valid_to, :table => self.class.superclass.quoted_table_name)
          end
        end
        alias :next :succ

        # Returns the first history entry
        #
        def first
          self.class.where(:id => rid).order('lower(validity)').first
        end

        # Returns the last history entry
        #
        def last
          self.class.where(:id => rid).order('lower(validity)').last
        end

        # Returns this history entry's current record
        #
        def record
          self.class.superclass.find(rid)
        end

        def valid_from
          validity.first
        end

        def valid_to
          validity.last
        end
      end

      model.singleton_class.instance_eval do
        define_method(:history) { history }
      end

      history.singleton_class.instance_eval do
        define_method(:sti_name) { model.sti_name }
      end

      model.const_set :History, history

      return history
    end

    def self.define_inherited_history_model_for(subclass)
      # Define history model for the subclass
      history = Class.new(subclass.superclass.history)
      history.table_name = subclass.superclass.history.table_name

      # Override the STI name on the history subclass
      history.singleton_class.instance_eval do
        define_method(:sti_name) { subclass.sti_name }
      end

      # Return the subclass history via the .history method
      subclass.singleton_class.instance_eval do
        define_method(:history) { history }
      end

      # Define the History constant inside the subclass
      subclass.const_set :History, history
    end

    # Returns a read-only representation of this record as it was +time+ ago.
    # Returns nil if no record is found.
    #
    def as_of(time)
      _as_of(time).first
    end

    # Returns a read-only representation of this record as it was +time+ ago.
    # Raises ActiveRecord::RecordNotFound if no record is found.
    #
    def as_of!(time)
      _as_of(time).first!
    end

    # Delegates to +HistoryMethods.as_of+ to fetch this instance as it was on
    # +time+. Used both by +as_of+ and +as_of!+ for performance reasons, to
    # avoid a `rescue` (@lleirborras).
    #
    def _as_of(time)
      self.class.as_of(time).where(:id => self.id)
    end
    protected :_as_of

    # Return the complete read-only history of this instance.
    #
    def history
      self.class.history.of(self)
    end

    # Returns an Array of timestamps for which this instance has an history
    # record. Takes temporal associations into account.
    #
    def timeline(options = {})
      self.class.history.timeline(self, options)
    end

    # Returns a boolean indicating whether this record is an history entry.
    #
    def historical?
      self.attributes.key?('as_of_time') || self.kind_of?(self.class.history)
    end

    # Hack around AR timezone support. These timestamps are recorded
    # by the chrono triggers in UTC, but AR reads them as they were
    # stored in the local timezone - thus here we bypass type casting
    # to force creation of UTC timestamps.
    #
    %w( valid_from valid_to recorded_at as_of_time ).each do |attr|
      define_method(attr) do
        Conversions.string_to_utc_time attributes_before_type_cast[attr]
      end
    end

    # Inhibit destroy of historical records
    #
    def destroy
      raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records' if historical?
      super
    end

    # Returns the previous record in the history, or nil if this is the only
    # recorded entry.
    #
    def pred(options = {})
      if self.class.timeline_associations.empty?
        history.order('upper(validity) DESC').offset(1).first
      else
        return nil unless (ts = pred_timestamp(options))
        self.class.as_of(ts).order(%[ #{options[:table] || self.class.quoted_table_name}."hid" DESC ]).find(options[:id] || id)
      end
    end

    # Returns the previous timestamp in this record's timeline. Includes
    # temporal associations.
    #
    def pred_timestamp(options = {})
      if historical?
        options[:before] ||= as_of_time
        timeline(options.merge(:limit => 1, :reverse => true)).first
      else
        timeline(options.merge(:limit => 2, :reverse => true)).second
      end
    end

    # Returns the next record in the history timeline.
    #
    def succ(options = {})
      unless self.class.timeline_associations.empty?
        return nil unless (ts = succ_timestamp(options))
        self.class.as_of(ts).order(%[ #{options[:table] || self.class.quoted_table_name}."hid" DESC ]).find(options[:id] || id)
      end
    end

    # Returns the next timestamp in this record's timeline. Includes temporal
    # associations.
    #
    def succ_timestamp(options = {})
      return nil unless historical?

      options[:after] ||= as_of_time
      timeline(options.merge(:limit => 1, :reverse => false)).first
    end

    # Returns the differences between this entry and the previous history one.
    # See: +changes_against+.
    #
    def last_changes
      pred = self.pred
      changes_against(pred) if pred
    end

    # Returns the differences between this record and an arbitrary reference
    # record. The changes representation is an hash keyed by attribute whose
    # values are arrays containing previous and current attributes values -
    # the same format used by ActiveModel::Dirty.
    #
    def changes_against(ref)
      self.class.attribute_names_for_history_changes.inject({}) do |changes, attr|
        old, new = ref.public_send(attr), self.public_send(attr)

        changes.tap do |c|
          changed = old.respond_to?(:history_eql?) ?
            !old.history_eql?(new) : old != new

          c[attr] = [old, new] if changed
        end
      end
    end

    module ClassMethods
      # Returns an ActiveRecord::Relation on the history of this model as
      # it was +time+ ago.
      def as_of(time)
        history.as_of(time, current_scope)
      end

      def attribute_names_for_history_changes
        @attribute_names_for_history_changes ||= attribute_names -
          %w( id hid validity recorded_at as_of_time )
      end

      def has_timeline(options)
        changes = options.delete(:changes)
        assocs  = history.has_timeline(options)

        attributes = changes.present? ?
          Array.wrap(changes) : assocs.map(&:name)

        attribute_names_for_history_changes.concat(attributes.map(&:to_s))
      end

      delegate :timeline_associations, :to => :history
    end

    module TimeQuery
      OPERATORS = {
        :at       => '<@',
        :at_range => '&&',
        :before   => '<<',
        :after    => '>>',
      }.freeze

      def time_query(match, time, options)
        range = options[:on]

        time = if time.kind_of?(Array)
          time.map! {|t| time_for_time_query(t)}
        else
          time_for_time_query(time)
        end

        if match == :not
          where(%[
            #{build_time_query(:before, time, range)} OR
            #{build_time_query(:after,  time, range)}
          ])
        else
          where(build_time_query(match, time, range))
        end
      end

      private
        def time_for_time_query(t)
          t = Conversions.time_to_utc_string(t.utc) if t.kind_of?(Time)
          t == :now ? "timezone('UTC', now())" : "#{connection.quote(t)}::timestamp"
        end

        def build_time_query(match, time, range)
          if time.kind_of?(Array)
            match = :at_range if match == :at
            %[ tsrange(#{time.first}, #{time.last}) #{OPERATORS.fetch(match)} #{range} ]
          else
            %[ #{time} #{OPERATORS.fetch(match)} #{range} ]
          end
        end
    end

    # Methods that make up the history interface of the companion History
    # model, automatically built for each Model that includes TimeMachine<commit_msg>Add ability to use the TimeQuery module also with daterange columns w:@lleirborras<commit_after>require 'active_record'

module ChronoModel

  module TimeMachine
    extend ActiveSupport::Concern

    included do
      if table_exists? && !chrono?
        puts "WARNING: #{table_name} is not a temporal table. " \
          "Please use change_table :#{table_name}, :temporal => true"
      end

      history = TimeMachine.define_history_model_for(self)
      TimeMachine.chrono_models[table_name] = history

      # STI support. TODO: more thorough testing
      #
      def self.inherited(subclass)
        super

        # Do not smash stack: as the below method is defining a
        # new anonymous class, without this check this leads to
        # infinite recursion.
        unless subclass.name.nil?
          TimeMachine.define_inherited_history_model_for(subclass)
        end
      end
    end

    # Returns an Hash keyed by table name of ChronoModels
    #
    def self.chrono_models
      (@chrono_models ||= {})
    end

    def self.define_history_model_for(model)
      history = Class.new(model) do
        self.table_name = [Adapter::HISTORY_SCHEMA, model.table_name].join('.')

        extend TimeMachine::HistoryMethods

        # The history id is `hid`, but this cannot set as primary key
        # or temporal assocations will break. Solutions are welcome.
        def id
          hid
        end

        # Referenced record ID.
        #
        def rid
          attributes[self.class.primary_key]
        end

        # HACK to make ActiveAdmin work properly. This will be surely
        # better written in the future.
        #
        def self.find(*args)
          old = self.primary_key
          self.primary_key = :hid
          super
        ensure
          self.primary_key = old
        end

        # Returns the previous history entry, or nil if this
        # is the first one.
        #
        def pred
          return if self.valid_from.nil?

          if self.class.timeline_associations.empty?
            self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first
          else
            super(:id => rid, :before => valid_from, :table => self.class.superclass.quoted_table_name)
          end
        end

        # Returns the next history entry, or nil if this is the
        # last one.
        #
        def succ
          return if self.valid_to.nil?

          if self.class.timeline_associations.empty?
            self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first
          else
            super(:id => rid, :after => valid_to, :table => self.class.superclass.quoted_table_name)
          end
        end
        alias :next :succ

        # Returns the first history entry
        #
        def first
          self.class.where(:id => rid).order('lower(validity)').first
        end

        # Returns the last history entry
        #
        def last
          self.class.where(:id => rid).order('lower(validity)').last
        end

        # Returns this history entry's current record
        #
        def record
          self.class.superclass.find(rid)
        end

        def valid_from
          validity.first
        end

        def valid_to
          validity.last
        end
      end

      model.singleton_class.instance_eval do
        define_method(:history) { history }
      end

      history.singleton_class.instance_eval do
        define_method(:sti_name) { model.sti_name }
      end

      model.const_set :History, history

      return history
    end

    def self.define_inherited_history_model_for(subclass)
      # Define history model for the subclass
      history = Class.new(subclass.superclass.history)
      history.table_name = subclass.superclass.history.table_name

      # Override the STI name on the history subclass
      history.singleton_class.instance_eval do
        define_method(:sti_name) { subclass.sti_name }
      end

      # Return the subclass history via the .history method
      subclass.singleton_class.instance_eval do
        define_method(:history) { history }
      end

      # Define the History constant inside the subclass
      subclass.const_set :History, history
    end

    # Returns a read-only representation of this record as it was +time+ ago.
    # Returns nil if no record is found.
    #
    def as_of(time)
      _as_of(time).first
    end

    # Returns a read-only representation of this record as it was +time+ ago.
    # Raises ActiveRecord::RecordNotFound if no record is found.
    #
    def as_of!(time)
      _as_of(time).first!
    end

    # Delegates to +HistoryMethods.as_of+ to fetch this instance as it was on
    # +time+. Used both by +as_of+ and +as_of!+ for performance reasons, to
    # avoid a `rescue` (@lleirborras).
    #
    def _as_of(time)
      self.class.as_of(time).where(:id => self.id)
    end
    protected :_as_of

    # Return the complete read-only history of this instance.
    #
    def history
      self.class.history.of(self)
    end

    # Returns an Array of timestamps for which this instance has an history
    # record. Takes temporal associations into account.
    #
    def timeline(options = {})
      self.class.history.timeline(self, options)
    end

    # Returns a boolean indicating whether this record is an history entry.
    #
    def historical?
      self.attributes.key?('as_of_time') || self.kind_of?(self.class.history)
    end

    # Hack around AR timezone support. These timestamps are recorded
    # by the chrono triggers in UTC, but AR reads them as they were
    # stored in the local timezone - thus here we bypass type casting
    # to force creation of UTC timestamps.
    #
    %w( valid_from valid_to recorded_at as_of_time ).each do |attr|
      define_method(attr) do
        Conversions.string_to_utc_time attributes_before_type_cast[attr]
      end
    end

    # Inhibit destroy of historical records
    #
    def destroy
      raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records' if historical?
      super
    end

    # Returns the previous record in the history, or nil if this is the only
    # recorded entry.
    #
    def pred(options = {})
      if self.class.timeline_associations.empty?
        history.order('upper(validity) DESC').offset(1).first
      else
        return nil unless (ts = pred_timestamp(options))
        self.class.as_of(ts).order(%[ #{options[:table] || self.class.quoted_table_name}."hid" DESC ]).find(options[:id] || id)
      end
    end

    # Returns the previous timestamp in this record's timeline. Includes
    # temporal associations.
    #
    def pred_timestamp(options = {})
      if historical?
        options[:before] ||= as_of_time
        timeline(options.merge(:limit => 1, :reverse => true)).first
      else
        timeline(options.merge(:limit => 2, :reverse => true)).second
      end
    end

    # Returns the next record in the history timeline.
    #
    def succ(options = {})
      unless self.class.timeline_associations.empty?
        return nil unless (ts = succ_timestamp(options))
        self.class.as_of(ts).order(%[ #{options[:table] || self.class.quoted_table_name}."hid" DESC ]).find(options[:id] || id)
      end
    end

    # Returns the next timestamp in this record's timeline. Includes temporal
    # associations.
    #
    def succ_timestamp(options = {})
      return nil unless historical?

      options[:after] ||= as_of_time
      timeline(options.merge(:limit => 1, :reverse => false)).first
    end

    # Returns the differences between this entry and the previous history one.
    # See: +changes_against+.
    #
    def last_changes
      pred = self.pred
      changes_against(pred) if pred
    end

    # Returns the differences between this record and an arbitrary reference
    # record. The changes representation is an hash keyed by attribute whose
    # values are arrays containing previous and current attributes values -
    # the same format used by ActiveModel::Dirty.
    #
    def changes_against(ref)
      self.class.attribute_names_for_history_changes.inject({}) do |changes, attr|
        old, new = ref.public_send(attr), self.public_send(attr)

        changes.tap do |c|
          changed = old.respond_to?(:history_eql?) ?
            !old.history_eql?(new) : old != new

          c[attr] = [old, new] if changed
        end
      end
    end

    module ClassMethods
      # Returns an ActiveRecord::Relation on the history of this model as
      # it was +time+ ago.
      def as_of(time)
        history.as_of(time, current_scope)
      end

      def attribute_names_for_history_changes
        @attribute_names_for_history_changes ||= attribute_names -
          %w( id hid validity recorded_at as_of_time )
      end

      def has_timeline(options)
        changes = options.delete(:changes)
        assocs  = history.has_timeline(options)

        attributes = changes.present? ?
          Array.wrap(changes) : assocs.map(&:name)

        attribute_names_for_history_changes.concat(attributes.map(&:to_s))
      end

      delegate :timeline_associations, :to => :history
    end

    module TimeQuery
      OPERATORS = {
        :at       => '<@',
        :at_range => '&&',
        :before   => '<<',
        :after    => '>>',
      }.freeze

      def time_query(match, time, options)
        range = options[:on]
        type  = options[:type] || :tsrange

        time = if time.kind_of?(Array)
          time.map! {|t| time_for_time_query(t)}
        else
          time_for_time_query(time)
        end

        if match == :not
          where(%[
            #{build_time_query(:before, time, range, type)} OR
            #{build_time_query(:after,  time, range, type)}
          ])
        else
          where(build_time_query(match, time, range, type))
        end
      end

      private
        def time_for_time_query(t)
          if t == :now
            "timezone('UTC', now())"

          elsif t == :today
            "current_date"

          elsif t.try(:acts_like_date?)
            "#{connection.quote(t.to_s)}::date"

          else

            if t.try(:acts_like_time?)
              t = Conversions.time_to_utc_string(t.utc)
            end

            "#{connection.quote(t)}::timestamp"

          end
        end

        def build_time_query(match, time, range, type)
          if time.kind_of?(Array)
            match = :at_range if match == :at
            %[ #{type}(#{time.first}, #{time.last}) #{OPERATORS.fetch(match)} #{range} ]
          else
            %[ #{time} #{OPERATORS.fetch(match)} #{range} ]
          end
        end
    end

    # Methods that make up the history interface of the companion History
    # model, automatically built for each Model that includes TimeMachine
{'fraction_non_alphanumeric': 0.0895255988727102, 'fraction_numerical': 0.00037576326914044153, 'mean_word_length': 2.2059930733323294, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 49, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3929936', 'n_tokens_mistral': 6179, 'n_tokens_neox': 6013, 'n_words': 1760}