lib/sequel/plugins/bitemporal.rb in sequel_bitemporal-0.6.11 vs lib/sequel/plugins/bitemporal.rb in sequel_bitemporal-0.6.12

- old
+ new

@@ -51,37 +51,84 @@ @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" + @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.one_to_many :versions, class: version, key: :master_id, graph_alias_base: master.versions_alias master.one_to_one :current_version, class: version, key: :master_id, graph_alias_base: master.current_version_alias, :graph_block=>(proc do |j, lj, js| t = ::Sequel::Plugins::Bitemporal.point_in_time n = ::Sequel::Plugins::Bitemporal.now - e = :expired_at.qualify(j) - (:created_at.qualify(j) <= t) & ({e=>nil} | (e > t)) & (:valid_from.qualify(j) <= n) & (:valid_to.qualify(j) > n) + 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::Plugins::Bitemporal.point_in_time n = ::Sequel::Plugins::Bitemporal.now - ds.where{(created_at <= t) & ({expired_at=>nil} | (expired_at > t)) & (valid_from <= n) & (valid_to > n)} + if model.use_ranges + ds.where(model.existence_range_contains(t) & model.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({:id.qualify(model.current_version_alias) => nil}.sql_negate) + 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: :master_id, :graph_block=>(proc do |j, lj, js| t = ::Sequel::Plugins::Bitemporal.point_in_time n = ::Sequel::Plugins::Bitemporal.now - e = :expired_at.qualify(j) - (:created_at.qualify(j) <= t) & ({e=>nil} | (e > t)) & (:valid_to.qualify(j) > n) + if master.use_ranges + master.existence_range_contains(t, j) & + (Sequel.qualify(j, :valid_to) > n) + else + e = Sequel.qualify j, :expired_at + (Sequel.qualify(j, :created_at) <= t) & + Sequel.|({e=>nil}, e > t) & + (Sequel.qualify(j, :valid_to) > n) + end end) do |ds| t = ::Sequel::Plugins::Bitemporal.point_in_time n = ::Sequel::Plugins::Bitemporal.now - ds.where{(created_at <= t) & ({expired_at=>nil} | (expired_at > t)) & (valid_to > n)} + if model.use_ranges + existence_conditions = model.existence_range_contains t, j + ds.where{ existence_conditions & (:valid_to > n) } + else + ds.where do + (created_at <= t) & + Sequel.|({expired_at=>nil}, expired_at > t) & + (valid_to > n) + end + end end master.def_dataset_method :with_current_or_future_versions do - eager_graph(:current_or_future_versions).where({current_or_future_versions__id: nil}.sql_negate) + eager_graph(:current_or_future_versions).where( + Sequel.negate(current_or_future_versions__id: nil) + ) end version.many_to_one :master, class: master, key: :master_id version.class_eval do def current? t = ::Sequel::Plugins::Bitemporal.point_in_time @@ -110,11 +157,79 @@ end end module ClassMethods attr_reader :version_class, :versions_alias, :current_version_alias, :propagate_per_column, :audit_class, :audit_updated_by_method, - :version_uses_string_nilifier + :version_uses_string_nilifier, :use_ranges + + def validity_range_type + @validity_range_type ||= begin + valid_from_infos = db.schema( + version_class.table_name + ).detect do |column_name, _| + column_name==:valid_from + end + unless valid_from_infos + raise "Could not find valid_from column in #{version_class.table_name}" + end + case valid_from_infos.last[:db_type] + when "date" + :daterange + when "timestamp without time zone" + :tsrange + when "timestamp with time zone" + :tstzrange + else + raise "Don't know how to handle ranges for type: #{valid_from_infos[:db_type]}" + end + end + end + + def validity_cast_type + case validity_range_type + when :daterange + :date + when :tsrange, :tstzrange + :timestamp + else + raise "Don't know how to handle cast for range type: #{validity_range_type}" + end + end + + def existence_range(qualifier=nil) + created_at_column = :created_at + created_at_column = Sequel.qualify qualifier, created_at_column if qualifier + expired_at_column = :expired_at + expired_at_column = Sequel.qualify qualifier, expired_at_column if qualifier + Sequel.function( + :tsrange, created_at_column, expired_at_column, "[)" + ).pg_range + end + + def existence_range_contains(point_in_time, qualifier=nil) + existence_range(qualifier).contains( + Sequel.cast(point_in_time, :timestamp) + ) + end + + def validity_range(qualifier=nil) + valid_from_column = :valid_from + valid_from_column = Sequel.qualify qualifier, valid_from_column if qualifier + valid_to_column = :valid_to + valid_to_column = Sequel.qualify qualifier, valid_to_column if qualifier + + Sequel.function( + validity_range_type, valid_from_column, valid_to_column, "[)" + ).pg_range + end + + def validity_range_contains(now, qualifier=nil) + validity_range(qualifier).contains( + Sequel.cast(now, validity_cast_type) + ) + end + end module DatasetMethods end module InstanceMethods attr_reader :pending_version @@ -227,13 +342,20 @@ def last_version @last_version ||= begin return if new? t = ::Sequel::Plugins::Bitemporal.point_in_time n = ::Sequel::Plugins::Bitemporal.now + if use_ranges = self.class.use_ranges + range_conditions = self.class.existence_range_contains t + end versions_dataset.where do - (created_at <= t) & ({expired_at=>nil} | (expired_at > t)) & - (valid_from <= n) - end.order(:valid_to.desc, :created_at.desc).first + if use_ranges + range_conditions + else + (created_at <= t) & + Sequel.|({expired_at=>nil}, expired_at > t) + end & (valid_from <= n) + end.order(Sequel.desc(:valid_to), Sequel.desc(:created_at)).first end end def restore(attrs={}) return false unless deleted?