module Sequel::Plugins::Bitemporal

Constants

THREAD_NOW_KEY
THREAD_POINT_IN_TIME_KEY

Public Class Methods

as_we_knew_it(time) { || ... } click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 7
def self.as_we_knew_it(time)
  previous = Thread.current[THREAD_POINT_IN_TIME_KEY]
  raise ArgumentError, "requires a block" unless block_given?
  Thread.current[THREAD_POINT_IN_TIME_KEY] = time.to_datetime
  yield
ensure
  Thread.current[THREAD_POINT_IN_TIME_KEY] = previous
end
at(time) { || ... } click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 21
def self.at(time)
  previous = Thread.current[THREAD_NOW_KEY]
  raise ArgumentError, "requires a block" unless block_given?
  Thread.current[THREAD_NOW_KEY] = time.to_datetime
  yield
ensure
  Thread.current[THREAD_NOW_KEY] = previous
end
bitemporal_excluded_columns(master = nil) click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 44
def self.bitemporal_excluded_columns(master = nil)
  [:id, *bitemporal_version_columns(master)]
end
bitemporal_version_columns(master = nil) click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 40
def self.bitemporal_version_columns(master = nil)
  [*version_foreign_keys(master), :valid_from, :valid_to, :created_at, :expired_at]
end
configure(master, opts = {}) click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 48
      def self.configure(master, opts = {})
        version = opts[:version_class]
        raise Error, "please specify version class to use for bitemporal plugin" unless version
        return version.db.log_info("Version table does not exist for #{version.name}") unless version.db.table_exists?(version.table_name)
        missing = bitemporal_version_columns(master) - version.columns
        raise Error, "bitemporal plugin requires the following missing column#{"s" if missing.size>1} on version class: #{missing.join(", ")}" unless missing.empty?

        if Sequel::Plugins::Bitemporal.jdbc?(master.db)
          master.plugin :typecast_on_load, *master.columns
        end

        # Sequel::Model.def_dataset_method has been deprecated and moved to the
        # def_dataset_method plugin in Sequel 4.46.0, it was not loaded by
        # default from 5.0
        begin
          master.plugin :def_dataset_method
        rescue LoadError
        end
        # Sequel::Model#set_all has been deprecated and moved to the whitelist
        # security plugin in Sequel 4.46.0, it was not loaded by default from 5.0
        begin
          master.plugin :whitelist_security
          version.plugin :whitelist_security
        rescue LoadError
        end

        master.instance_eval do
          @version_class = version
          base_alias = opts.fetch :base_alias do
            name ? underscore(demodulize(name)) : table_name
          end
          @versions_alias = "#{base_alias}_versions".to_sym
          @current_version_alias = "#{base_alias}_current_version".to_sym
          @audit_class = opts[:audit_class]
          @audit_updated_by_method = opts.fetch(:audit_updated_by_method){ :updated_by }
          @propagate_per_column = opts.fetch(:propagate_per_column, false)
          @version_uses_string_nilifier = version.plugins.map(&:to_s).include? "Sequel::Plugins::StringNilifier"
          @excluded_columns = Sequel::Plugins::Bitemporal.bitemporal_excluded_columns(master)
          @excluded_columns += Array opts[:excluded_columns] if opts[:excluded_columns]
          @use_ranges = if opts[:ranges]
            db = self.db
            unless db.database_type==:postgres && db.server_version >= 90200
              raise "Ranges require PostgreSQL 9.2"
            end
            true
          else
            false
          end
        end
        master.class_eval do
          def self.current_versions_dataset
            t = ::Sequel::Plugins::Bitemporal.point_in_time
            n = ::Sequel::Plugins::Bitemporal.now
            version_class.where do
              (created_at <= t) &
              (Sequel.|({expired_at=>nil}, expired_at > t)) &
              (valid_from <= n) &
              (valid_to > n)
            end
          end
        end
        master.one_to_many :versions, class: version, key: version_foreign_keys(master), graph_alias_base: master.versions_alias
        master.one_to_one :current_version, class: version, key: version_foreign_keys(master), graph_alias_base: master.current_version_alias, :graph_block=>(proc do |j, lj, js|
          t = Sequel.delay{ ::Sequel::Plugins::Bitemporal.point_in_time }
          n = Sequel.delay{ ::Sequel::Plugins::Bitemporal.now }
          if master.use_ranges
            master.existence_range_contains(t, j) &
            master.validity_range_contains(n, j)
          else
            e = Sequel.qualify j, :expired_at
            (Sequel.qualify(j, :created_at) <= t) &
            (Sequel.|({e=>nil}, e > t)) &
            (Sequel.qualify(j, :valid_from) <= n) &
            (Sequel.qualify(j, :valid_to) > n)
          end
        end) do |ds|
          t = Sequel.delay{ ::Sequel::Plugins::Bitemporal.point_in_time }
          n = Sequel.delay{ ::Sequel::Plugins::Bitemporal.now }
          if master.use_ranges
            ds.where(master.existence_range_contains(t) & master.validity_range_contains(n))
          else
            ds.where do
              (created_at <= t) &
              (Sequel.|({expired_at=>nil}, expired_at > t)) &
              (valid_from <= n) &
              (valid_to > n)
            end
          end
        end
        master.def_dataset_method :with_current_version do
          eager_graph(:current_version).where(
            Sequel.negate(
              Sequel.qualify(model.current_version_alias, :id) => nil
            )
          )
        end
        master.one_to_many :current_or_future_versions, class: version, key: version_foreign_keys(master), :graph_block=>(proc do |j, lj, js|
          t = Sequel.delay{ ::Sequel::Plugins::Bitemporal.point_in_time }
          n = Sequel.delay{ ::Sequel::Plugins::Bitemporal.now }
          if master.use_ranges
            master.existence_range_contains(t, j) &
            (Sequel.qualify(j, :valid_to) > n) &
            (Sequel.qualify(j, :valid_from) != Sequel.qualify(j, :valid_to))
          else
            e = Sequel.qualify j, :expired_at
            (Sequel.qualify(j, :created_at) <= t) &
            Sequel.|({e=>nil}, e > t) &
            (Sequel.qualify(j, :valid_to) > n) &
            (Sequel.qualify(j, :valid_from) != Sequel.qualify(j, :valid_to))
          end
        end) do |ds|
          t = Sequel.delay{ ::Sequel::Plugins::Bitemporal.point_in_time }
          n = Sequel.delay{ ::Sequel::Plugins::Bitemporal.now }
          if master.use_ranges
            existence_conditions = master.existence_range_contains t
            ds.where{ existence_conditions & (:valid_to > n) & (:valid_from != :valid_to) }
          else
            ds.where do
              (created_at <= t) &
              Sequel.|({expired_at=>nil}, expired_at > t) &
              (valid_to > n) &
              (valid_from != valid_to)
            end
          end
        end
        master.def_dataset_method :with_current_or_future_versions do
          eager_graph(:current_or_future_versions).where(
            Sequel.negate(Sequel.qualify(:current_or_future_versions, :id) => nil)
          )
        end
        version.many_to_one :master, class: master, key: version_foreign_keys(master)
        version.class_eval do
          if Sequel::Plugins::Bitemporal.jdbc?(master.db)
            plugin :typecast_on_load, *columns
          end

          def current?
            t = ::Sequel::Plugins::Bitemporal.point_in_time
            n = ::Sequel::Plugins::Bitemporal.now
            !new? &&
            created_at.to_datetime<=t &&
            (expired_at.nil? || expired_at.to_datetime>t) &&
            valid_from.to_datetime<=n &&
            valid_to.to_datetime>n
          end
          def destroy(opts={})
            expand_previous_version = opts.fetch(:expand_previous_version){
              valid_from.to_datetime>::Sequel::Plugins::Bitemporal.now
            }
            master.destroy_version self, expand_previous_version
          end
        end
        unless opts[:delegate]==false
          (version.columns-master.columns-master.excluded_columns).each do |column|
            master.class_eval <<-EOS, __FILE__, __LINE__ + 1
              def #{column}
                pending_or_current_version.#{column} if pending_or_current_version
              end
            EOS
          end
        end
        if opts[:writers]
          (version.columns-master.columns-master.excluded_columns).each do |column|
            master.class_eval <<-EOS, __FILE__, __LINE__ + 1
              def #{column}=(value)
                self.attributes = {"#{column}" => value}
              end
            EOS
          end
        end
      end
current_versions_dataset() click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 98
def self.current_versions_dataset
  t = ::Sequel::Plugins::Bitemporal.point_in_time
  n = ::Sequel::Plugins::Bitemporal.now
  version_class.where do
    (created_at <= t) &
    (Sequel.|({expired_at=>nil}, expired_at > t)) &
    (valid_from <= n) &
    (valid_to > n)
  end
end
jdbc?(db) click to toggle source
# File lib/sequel_bitemporal.rb, line 10
def self.jdbc?(db)
  db.adapter_scheme==:jdbc
end
jruby?() click to toggle source
# File lib/sequel_bitemporal.rb, line 6
def self.jruby?
  (defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby') || defined?(JRUBY_VERSION)
end
now() click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 30
def self.now
  Thread.current[THREAD_NOW_KEY] || DateTime.now
end
point_in_time() click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 16
def self.point_in_time
  Thread.current[THREAD_POINT_IN_TIME_KEY] || DateTime.now
end
version_foreign_keys(master = nil) click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 34
def self.version_foreign_keys(master = nil)
  return :master_id unless master
  primary_key = [*master.primary_key]
  primary_key.size > 1 ? primary_key : :master_id
end

Public Instance Methods

current?() click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 184
def current?
  t = ::Sequel::Plugins::Bitemporal.point_in_time
  n = ::Sequel::Plugins::Bitemporal.now
  !new? &&
  created_at.to_datetime<=t &&
  (expired_at.nil? || expired_at.to_datetime>t) &&
  valid_from.to_datetime<=n &&
  valid_to.to_datetime>n
end
destroy(opts={}) click to toggle source
# File lib/sequel/plugins/bitemporal.rb, line 193
def destroy(opts={})
  expand_previous_version = opts.fetch(:expand_previous_version){
    valid_from.to_datetime>::Sequel::Plugins::Bitemporal.now
  }
  master.destroy_version self, expand_previous_version
end