chrono_model 0.5.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.rspec +1 -1
 - data/.travis.yml +7 -0
 - data/Gemfile +10 -1
 - data/LICENSE +3 -1
 - data/README.md +239 -136
 - data/README.sql +108 -94
 - data/chrono_model.gemspec +5 -4
 - data/lib/active_record/connection_adapters/chronomodel_adapter.rb +42 -0
 - data/lib/chrono_model.rb +0 -8
 - data/lib/chrono_model/adapter.rb +346 -212
 - data/lib/chrono_model/patches.rb +21 -8
 - data/lib/chrono_model/railtie.rb +1 -13
 - data/lib/chrono_model/time_gate.rb +2 -2
 - data/lib/chrono_model/time_machine.rb +153 -87
 - data/lib/chrono_model/utils.rb +35 -8
 - data/lib/chrono_model/version.rb +1 -1
 - data/spec/adapter_spec.rb +154 -14
 - data/spec/config.yml.example +1 -0
 - data/spec/json_ops_spec.rb +48 -0
 - data/spec/support/connection.rb +4 -9
 - data/spec/support/helpers.rb +27 -2
 - data/spec/support/matchers/column.rb +5 -2
 - data/spec/support/matchers/index.rb +4 -0
 - data/spec/support/matchers/schema.rb +4 -0
 - data/spec/support/matchers/table.rb +94 -21
 - data/spec/time_machine_spec.rb +62 -28
 - data/spec/time_query_spec.rb +227 -0
 - data/sql/json_ops.sql +56 -0
 - data/sql/uninstall-json_ops.sql +24 -0
 - metadata +44 -18
 - data/lib/chrono_model/compatibility.rb +0 -31
 
    
        data/lib/chrono_model/patches.rb
    CHANGED
    
    | 
         @@ -14,11 +14,11 @@ module ChronoModel 
     | 
|
| 
       14 
14 
     | 
    
         
             
                class Association < ActiveRecord::Associations::Association
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
       16 
16 
     | 
    
         
             
                  # If the association class or the through association are ChronoModels,
         
     | 
| 
       17 
     | 
    
         
            -
                  # then fetches the records from a virtual table using a subquery  
     | 
| 
      
 17 
     | 
    
         
            +
                  # then fetches the records from a virtual table using a subquery scope
         
     | 
| 
       18 
18 
     | 
    
         
             
                  # to a specific timestamp.
         
     | 
| 
       19 
     | 
    
         
            -
                  def  
     | 
| 
       20 
     | 
    
         
            -
                     
     | 
| 
       21 
     | 
    
         
            -
                    return  
     | 
| 
      
 19 
     | 
    
         
            +
                  def scope
         
     | 
| 
      
 20 
     | 
    
         
            +
                    scope = super
         
     | 
| 
      
 21 
     | 
    
         
            +
                    return scope unless _chrono_record?
         
     | 
| 
       22 
22 
     | 
    
         | 
| 
       23 
23 
     | 
    
         
             
                    klass = reflection.options[:polymorphic] ?
         
     | 
| 
       24 
24 
     | 
    
         
             
                      owner.public_send(reflection.foreign_type).constantize :
         
     | 
| 
         @@ -28,23 +28,36 @@ module ChronoModel 
     | 
|
| 
       28 
28 
     | 
    
         
             
                      # For standard associations, replace the table name with the virtual
         
     | 
| 
       29 
29 
     | 
    
         
             
                      # as-of table name at the owner's as-of-time
         
     | 
| 
       30 
30 
     | 
    
         
             
                      #
         
     | 
| 
       31 
     | 
    
         
            -
                       
     | 
| 
      
 31 
     | 
    
         
            +
                      scope = scope.readonly.from(klass.history.virtual_table_at(owner.as_of_time))
         
     | 
| 
       32 
32 
     | 
    
         
             
                    elsif respond_to?(:through_reflection) && through_reflection.klass.chrono?
         
     | 
| 
       33 
33 
     | 
    
         | 
| 
       34 
34 
     | 
    
         
             
                      # For through associations, replace the joined table name instead.
         
     | 
| 
       35 
35 
     | 
    
         
             
                      #
         
     | 
| 
       36 
     | 
    
         
            -
                       
     | 
| 
      
 36 
     | 
    
         
            +
                      scope.join_sources.each do |join|
         
     | 
| 
       37 
37 
     | 
    
         
             
                        if join.left.name == through_reflection.klass.table_name
         
     | 
| 
       38 
38 
     | 
    
         
             
                          v_table = through_reflection.klass.history.virtual_table_at(
         
     | 
| 
       39 
39 
     | 
    
         
             
                            owner.as_of_time, join.left.table_alias || join.left.table_name)
         
     | 
| 
       40 
40 
     | 
    
         | 
| 
       41 
     | 
    
         
            -
                           
     | 
| 
      
 41 
     | 
    
         
            +
                          # avoid problems in Rails when code down the line expects the
         
     | 
| 
      
 42 
     | 
    
         
            +
                          # join.left to respond to the following methods. we modify
         
     | 
| 
      
 43 
     | 
    
         
            +
                          # the instance of SqlLiteral to do just that.
         
     | 
| 
      
 44 
     | 
    
         
            +
                          table_name  = join.left.table_name
         
     | 
| 
      
 45 
     | 
    
         
            +
                          table_alias = join.left.table_alias
         
     | 
| 
      
 46 
     | 
    
         
            +
                          join.left   = Arel::Nodes::SqlLiteral.new(v_table)
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                          class << join.left
         
     | 
| 
      
 49 
     | 
    
         
            +
                            attr_accessor :name, :table_name, :table_alias
         
     | 
| 
      
 50 
     | 
    
         
            +
                          end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                          join.left.name        = table_name
         
     | 
| 
      
 53 
     | 
    
         
            +
                          join.left.table_name  = table_name
         
     | 
| 
      
 54 
     | 
    
         
            +
                          join.left.table_alias = table_alias
         
     | 
| 
       42 
55 
     | 
    
         
             
                        end
         
     | 
| 
       43 
56 
     | 
    
         
             
                      end
         
     | 
| 
       44 
57 
     | 
    
         | 
| 
       45 
58 
     | 
    
         
             
                    end
         
     | 
| 
       46 
59 
     | 
    
         | 
| 
       47 
     | 
    
         
            -
                    return  
     | 
| 
      
 60 
     | 
    
         
            +
                    return scope
         
     | 
| 
       48 
61 
     | 
    
         
             
                  end
         
     | 
| 
       49 
62 
     | 
    
         | 
| 
       50 
63 
     | 
    
         
             
                  private
         
     | 
    
        data/lib/chrono_model/railtie.rb
    CHANGED
    
    | 
         @@ -1,21 +1,9 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module ChronoModel
         
     | 
| 
       2 
2 
     | 
    
         
             
              class Railtie < ::Rails::Railtie
         
     | 
| 
       3 
     | 
    
         
            -
                 
     | 
| 
       4 
     | 
    
         
            -
                  ActiveRecord::Base.connection.chrono_create_schemas!
         
     | 
| 
       5 
     | 
    
         
            -
                end
         
     | 
| 
      
 3 
     | 
    
         
            +
                ActiveRecord::Tasks::DatabaseTasks.register_task /chronomodel/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks
         
     | 
| 
       6 
4 
     | 
    
         | 
| 
       7 
5 
     | 
    
         
             
                rake_tasks do
         
     | 
| 
       8 
6 
     | 
    
         
             
                  load 'chrono_model/schema_format.rake'
         
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
                  namespace :db do
         
     | 
| 
       11 
     | 
    
         
            -
                    namespace :chrono do
         
     | 
| 
       12 
     | 
    
         
            -
                      task :create_schemas do
         
     | 
| 
       13 
     | 
    
         
            -
                        ActiveRecord::Base.connection.chrono_create_schemas!
         
     | 
| 
       14 
     | 
    
         
            -
                      end
         
     | 
| 
       15 
     | 
    
         
            -
                    end
         
     | 
| 
       16 
     | 
    
         
            -
                  end
         
     | 
| 
       17 
     | 
    
         
            -
             
     | 
| 
       18 
     | 
    
         
            -
                  task 'db:schema:load' => 'db:chrono:create_schemas'
         
     | 
| 
       19 
7 
     | 
    
         
             
                end
         
     | 
| 
       20 
8 
     | 
    
         
             
              end
         
     | 
| 
       21 
9 
     | 
    
         
             
            end
         
     | 
| 
         @@ -11,10 +11,10 @@ module ChronoModel 
     | 
|
| 
       11 
11 
     | 
    
         
             
                    time = Conversions.time_to_utc_string(time.utc) if time.kind_of? Time
         
     | 
| 
       12 
12 
     | 
    
         | 
| 
       13 
13 
     | 
    
         
             
                    virtual_table = select(%[
         
     | 
| 
       14 
     | 
    
         
            -
                      #{quoted_table_name}.*, #{connection.quote(time)} AS "as_of_time"]
         
     | 
| 
      
 14 
     | 
    
         
            +
                      #{quoted_table_name}.*, #{connection.quote(time)}::timestamp AS "as_of_time"]
         
     | 
| 
       15 
15 
     | 
    
         
             
                    ).to_sql
         
     | 
| 
       16 
16 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
                    as_of =  
     | 
| 
      
 17 
     | 
    
         
            +
                    as_of = all.from("(#{virtual_table}) #{quoted_table_name}")
         
     | 
| 
       18 
18 
     | 
    
         | 
| 
       19 
19 
     | 
    
         
             
                    as_of.instance_variable_set(:@temporal, time)
         
     | 
| 
       20 
20 
     | 
    
         | 
| 
         @@ -6,11 +6,6 @@ module ChronoModel 
     | 
|
| 
       6 
6 
     | 
    
         
             
                extend ActiveSupport::Concern
         
     | 
| 
       7 
7 
     | 
    
         | 
| 
       8 
8 
     | 
    
         
             
                included do
         
     | 
| 
       9 
     | 
    
         
            -
                  unless supports_chrono?
         
     | 
| 
       10 
     | 
    
         
            -
                    raise Error, "Your database server is not supported by ChronoModel. "\
         
     | 
| 
       11 
     | 
    
         
            -
                      "Currently, only PostgreSQL >= 9.0 is supported."
         
     | 
| 
       12 
     | 
    
         
            -
                  end
         
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
9 
     | 
    
         
             
                  if table_exists? && !chrono?
         
     | 
| 
       15 
10 
     | 
    
         
             
                    puts "WARNING: #{table_name} is not a temporal table. " \
         
     | 
| 
       16 
11 
     | 
    
         
             
                      "Please use change_table :#{table_name}, :temporal => true"
         
     | 
| 
         @@ -51,7 +46,7 @@ module ChronoModel 
     | 
|
| 
       51 
46 
     | 
    
         
             
                      hid
         
     | 
| 
       52 
47 
     | 
    
         
             
                    end
         
     | 
| 
       53 
48 
     | 
    
         | 
| 
       54 
     | 
    
         
            -
                    # Referenced record ID
         
     | 
| 
      
 49 
     | 
    
         
            +
                    # Referenced record ID.
         
     | 
| 
       55 
50 
     | 
    
         
             
                    #
         
     | 
| 
       56 
51 
     | 
    
         
             
                    def rid
         
     | 
| 
       57 
52 
     | 
    
         
             
                      attributes[self.class.primary_key]
         
     | 
| 
         @@ -72,12 +67,12 @@ module ChronoModel 
     | 
|
| 
       72 
67 
     | 
    
         
             
                    # is the first one.
         
     | 
| 
       73 
68 
     | 
    
         
             
                    #
         
     | 
| 
       74 
69 
     | 
    
         
             
                    def pred
         
     | 
| 
       75 
     | 
    
         
            -
                      return  
     | 
| 
      
 70 
     | 
    
         
            +
                      return if self.valid_from.nil?
         
     | 
| 
       76 
71 
     | 
    
         | 
| 
       77 
72 
     | 
    
         
             
                      if self.class.timeline_associations.empty?
         
     | 
| 
       78 
     | 
    
         
            -
                        self.class.where( 
     | 
| 
      
 73 
     | 
    
         
            +
                        self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first
         
     | 
| 
       79 
74 
     | 
    
         
             
                      else
         
     | 
| 
       80 
     | 
    
         
            -
                        super(:id => rid, :before => valid_from)
         
     | 
| 
      
 75 
     | 
    
         
            +
                        super(:id => rid, :before => valid_from, :table => self.class.superclass.quoted_table_name)
         
     | 
| 
       81 
76 
     | 
    
         
             
                      end
         
     | 
| 
       82 
77 
     | 
    
         
             
                    end
         
     | 
| 
       83 
78 
     | 
    
         | 
| 
         @@ -85,12 +80,12 @@ module ChronoModel 
     | 
|
| 
       85 
80 
     | 
    
         
             
                    # last one.
         
     | 
| 
       86 
81 
     | 
    
         
             
                    #
         
     | 
| 
       87 
82 
     | 
    
         
             
                    def succ
         
     | 
| 
       88 
     | 
    
         
            -
                      return  
     | 
| 
      
 83 
     | 
    
         
            +
                      return if self.valid_to.nil?
         
     | 
| 
       89 
84 
     | 
    
         | 
| 
       90 
85 
     | 
    
         
             
                      if self.class.timeline_associations.empty?
         
     | 
| 
       91 
     | 
    
         
            -
                        self.class.where( 
     | 
| 
      
 86 
     | 
    
         
            +
                        self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first
         
     | 
| 
       92 
87 
     | 
    
         
             
                      else
         
     | 
| 
       93 
     | 
    
         
            -
                        super(:id => rid, :after => valid_to)
         
     | 
| 
      
 88 
     | 
    
         
            +
                        super(:id => rid, :after => valid_to, :table => self.class.superclass.quoted_table_name)
         
     | 
| 
       94 
89 
     | 
    
         
             
                      end
         
     | 
| 
       95 
90 
     | 
    
         
             
                    end
         
     | 
| 
       96 
91 
     | 
    
         
             
                    alias :next :succ
         
     | 
| 
         @@ -98,13 +93,13 @@ module ChronoModel 
     | 
|
| 
       98 
93 
     | 
    
         
             
                    # Returns the first history entry
         
     | 
| 
       99 
94 
     | 
    
         
             
                    #
         
     | 
| 
       100 
95 
     | 
    
         
             
                    def first
         
     | 
| 
       101 
     | 
    
         
            -
                      self.class.where(:id => rid).order( 
     | 
| 
      
 96 
     | 
    
         
            +
                      self.class.where(:id => rid).order('lower(validity)').first
         
     | 
| 
       102 
97 
     | 
    
         
             
                    end
         
     | 
| 
       103 
98 
     | 
    
         | 
| 
       104 
99 
     | 
    
         
             
                    # Returns the last history entry
         
     | 
| 
       105 
100 
     | 
    
         
             
                    #
         
     | 
| 
       106 
101 
     | 
    
         
             
                    def last
         
     | 
| 
       107 
     | 
    
         
            -
                      self.class.where(:id => rid).order( 
     | 
| 
      
 102 
     | 
    
         
            +
                      self.class.where(:id => rid).order('lower(validity)').last
         
     | 
| 
       108 
103 
     | 
    
         
             
                    end
         
     | 
| 
       109 
104 
     | 
    
         | 
| 
       110 
105 
     | 
    
         
             
                    # Returns this history entry's current record
         
     | 
| 
         @@ -112,6 +107,18 @@ module ChronoModel 
     | 
|
| 
       112 
107 
     | 
    
         
             
                    def record
         
     | 
| 
       113 
108 
     | 
    
         
             
                      self.class.superclass.find(rid)
         
     | 
| 
       114 
109 
     | 
    
         
             
                    end
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                    def valid_from
         
     | 
| 
      
 112 
     | 
    
         
            +
                      validity.first
         
     | 
| 
      
 113 
     | 
    
         
            +
                    end
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                    def valid_to
         
     | 
| 
      
 116 
     | 
    
         
            +
                      validity.last
         
     | 
| 
      
 117 
     | 
    
         
            +
                    end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                    def recorded_at
         
     | 
| 
      
 120 
     | 
    
         
            +
                      Conversions.string_to_utc_time attributes_before_type_cast['recorded_at']
         
     | 
| 
      
 121 
     | 
    
         
            +
                    end
         
     | 
| 
       115 
122 
     | 
    
         
             
                  end
         
     | 
| 
       116 
123 
     | 
    
         | 
| 
       117 
124 
     | 
    
         
             
                  model.singleton_class.instance_eval do
         
     | 
| 
         @@ -188,15 +195,10 @@ module ChronoModel 
     | 
|
| 
       188 
195 
     | 
    
         
             
                  self.attributes.key?('as_of_time') || self.kind_of?(self.class.history)
         
     | 
| 
       189 
196 
     | 
    
         
             
                end
         
     | 
| 
       190 
197 
     | 
    
         | 
| 
       191 
     | 
    
         
            -
                #  
     | 
| 
       192 
     | 
    
         
            -
                # by the chrono rewrite rules in UTC, but AR reads them as they
         
     | 
| 
       193 
     | 
    
         
            -
                # were stored in the local timezone - thus here we bypass type
         
     | 
| 
       194 
     | 
    
         
            -
                # casting to force creation of UTC timestamps.
         
     | 
| 
      
 198 
     | 
    
         
            +
                # Read the virtual 'as_of_time' attribute and return it as an UTC timestamp.
         
     | 
| 
       195 
199 
     | 
    
         
             
                #
         
     | 
| 
       196 
     | 
    
         
            -
                 
     | 
| 
       197 
     | 
    
         
            -
                   
     | 
| 
       198 
     | 
    
         
            -
                    Conversions.string_to_utc_time attributes_before_type_cast[attr]
         
     | 
| 
       199 
     | 
    
         
            -
                  end
         
     | 
| 
      
 200 
     | 
    
         
            +
                def as_of_time
         
     | 
| 
      
 201 
     | 
    
         
            +
                  Conversions.string_to_utc_time attributes_before_type_cast['as_of_time']
         
     | 
| 
       200 
202 
     | 
    
         
             
                end
         
     | 
| 
       201 
203 
     | 
    
         | 
| 
       202 
204 
     | 
    
         
             
                # Inhibit destroy of historical records
         
     | 
| 
         @@ -211,10 +213,10 @@ module ChronoModel 
     | 
|
| 
       211 
213 
     | 
    
         
             
                #
         
     | 
| 
       212 
214 
     | 
    
         
             
                def pred(options = {})
         
     | 
| 
       213 
215 
     | 
    
         
             
                  if self.class.timeline_associations.empty?
         
     | 
| 
       214 
     | 
    
         
            -
                    history.order(' 
     | 
| 
      
 216 
     | 
    
         
            +
                    history.order('upper(validity) DESC').offset(1).first
         
     | 
| 
       215 
217 
     | 
    
         
             
                  else
         
     | 
| 
       216 
218 
     | 
    
         
             
                    return nil unless (ts = pred_timestamp(options))
         
     | 
| 
       217 
     | 
    
         
            -
                    self.class.as_of(ts).order( 
     | 
| 
      
 219 
     | 
    
         
            +
                    self.class.as_of(ts).order(%[ #{options[:table] || self.class.quoted_table_name}."hid" DESC ]).find(options[:id] || id)
         
     | 
| 
       218 
220 
     | 
    
         
             
                  end
         
     | 
| 
       219 
221 
     | 
    
         
             
                end
         
     | 
| 
       220 
222 
     | 
    
         | 
| 
         @@ -235,7 +237,7 @@ module ChronoModel 
     | 
|
| 
       235 
237 
     | 
    
         
             
                def succ(options = {})
         
     | 
| 
       236 
238 
     | 
    
         
             
                  unless self.class.timeline_associations.empty?
         
     | 
| 
       237 
239 
     | 
    
         
             
                    return nil unless (ts = succ_timestamp(options))
         
     | 
| 
       238 
     | 
    
         
            -
                    self.class.as_of(ts).order( 
     | 
| 
      
 240 
     | 
    
         
            +
                    self.class.as_of(ts).order(%[ #{options[:table] || self.class.quoted_table_name}."hid" DESC ]).find(options[:id] || id)
         
     | 
| 
       239 
241 
     | 
    
         
             
                  end
         
     | 
| 
       240 
242 
     | 
    
         
             
                end
         
     | 
| 
       241 
243 
     | 
    
         | 
| 
         @@ -275,13 +277,6 @@ module ChronoModel 
     | 
|
| 
       275 
277 
     | 
    
         
             
                  end
         
     | 
| 
       276 
278 
     | 
    
         
             
                end
         
     | 
| 
       277 
279 
     | 
    
         | 
| 
       278 
     | 
    
         
            -
                # Wraps AR::Base#attributes by removing the __xid internal attribute
         
     | 
| 
       279 
     | 
    
         
            -
                # used to squash together changes made in the same transaction.
         
     | 
| 
       280 
     | 
    
         
            -
                #
         
     | 
| 
       281 
     | 
    
         
            -
                %w( attributes attribute_names ).each do |name|
         
     | 
| 
       282 
     | 
    
         
            -
                  define_method(name) { super().tap {|x| x.delete('__xid')} }
         
     | 
| 
       283 
     | 
    
         
            -
                end
         
     | 
| 
       284 
     | 
    
         
            -
             
     | 
| 
       285 
280 
     | 
    
         
             
                module ClassMethods
         
     | 
| 
       286 
281 
     | 
    
         
             
                  # Returns an ActiveRecord::Relation on the history of this model as
         
     | 
| 
       287 
282 
     | 
    
         
             
                  # it was +time+ ago.
         
     | 
| 
         @@ -291,7 +286,7 @@ module ChronoModel 
     | 
|
| 
       291 
286 
     | 
    
         | 
| 
       292 
287 
     | 
    
         
             
                  def attribute_names_for_history_changes
         
     | 
| 
       293 
288 
     | 
    
         
             
                    @attribute_names_for_history_changes ||= attribute_names -
         
     | 
| 
       294 
     | 
    
         
            -
                      %w( id hid  
     | 
| 
      
 289 
     | 
    
         
            +
                      %w( id hid validity recorded_at as_of_time )
         
     | 
| 
       295 
290 
     | 
    
         
             
                  end
         
     | 
| 
       296 
291 
     | 
    
         | 
| 
       297 
292 
     | 
    
         
             
                  def has_timeline(options)
         
     | 
| 
         @@ -308,48 +303,76 @@ module ChronoModel 
     | 
|
| 
       308 
303 
     | 
    
         
             
                end
         
     | 
| 
       309 
304 
     | 
    
         | 
| 
       310 
305 
     | 
    
         
             
                module TimeQuery
         
     | 
| 
       311 
     | 
    
         
            -
                   
     | 
| 
       312 
     | 
    
         
            -
             
     | 
| 
       313 
     | 
    
         
            -
             
     | 
| 
       314 
     | 
    
         
            -
                    : 
     | 
| 
       315 
     | 
    
         
            -
                  }.freeze
         
     | 
| 
      
 306 
     | 
    
         
            +
                  # TODO Documentation
         
     | 
| 
      
 307 
     | 
    
         
            +
                  #
         
     | 
| 
      
 308 
     | 
    
         
            +
                  def time_query(match, time, options)
         
     | 
| 
      
 309 
     | 
    
         
            +
                    range = columns_hash.fetch(options[:on].to_s)
         
     | 
| 
       316 
310 
     | 
    
         | 
| 
       317 
     | 
    
         
            -
             
     | 
| 
       318 
     | 
    
         
            -
                     
     | 
| 
      
 311 
     | 
    
         
            +
                    query = case match
         
     | 
| 
      
 312 
     | 
    
         
            +
                    when :at
         
     | 
| 
      
 313 
     | 
    
         
            +
                      build_time_query_at(time, range)
         
     | 
| 
       319 
314 
     | 
    
         | 
| 
       320 
     | 
    
         
            -
                     
     | 
| 
       321 
     | 
    
         
            -
                       
     | 
| 
       322 
     | 
    
         
            -
             
     | 
| 
       323 
     | 
    
         
            -
             
     | 
| 
       324 
     | 
    
         
            -
             
     | 
| 
      
 315 
     | 
    
         
            +
                    when :not
         
     | 
| 
      
 316 
     | 
    
         
            +
                      "NOT (#{build_time_query_at(time, range)})"
         
     | 
| 
      
 317 
     | 
    
         
            +
             
     | 
| 
      
 318 
     | 
    
         
            +
                    when :before
         
     | 
| 
      
 319 
     | 
    
         
            +
                      build_time_query(['NULL', time_for_time_query(time, range)], range)
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
      
 321 
     | 
    
         
            +
                    when :after
         
     | 
| 
      
 322 
     | 
    
         
            +
                      build_time_query([time_for_time_query(time, range), 'NULL'], range)
         
     | 
| 
       325 
323 
     | 
    
         | 
| 
       326 
     | 
    
         
            -
                    if match == :not
         
     | 
| 
       327 
     | 
    
         
            -
                      where(%[
         
     | 
| 
       328 
     | 
    
         
            -
                        #{build_time_query(:before, from_t, from_f, to_t, to_f)} OR
         
     | 
| 
       329 
     | 
    
         
            -
                        #{build_time_query(:after,  from_t, from_f, to_t, to_f)}
         
     | 
| 
       330 
     | 
    
         
            -
                      ])
         
     | 
| 
       331 
324 
     | 
    
         
             
                    else
         
     | 
| 
       332 
     | 
    
         
            -
                       
     | 
| 
      
 325 
     | 
    
         
            +
                      raise ArgumentError, "Invalid time_query: #{match}"
         
     | 
| 
       333 
326 
     | 
    
         
             
                    end
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                    where(query)
         
     | 
| 
       334 
329 
     | 
    
         
             
                  end
         
     | 
| 
       335 
330 
     | 
    
         | 
| 
       336 
331 
     | 
    
         
             
                  private
         
     | 
| 
       337 
     | 
    
         
            -
             
     | 
| 
       338 
     | 
    
         
            -
             
     | 
| 
       339 
     | 
    
         
            -
                      t == :now  
     | 
| 
      
 332 
     | 
    
         
            +
             
     | 
| 
      
 333 
     | 
    
         
            +
                    def time_for_time_query(t, column)
         
     | 
| 
      
 334 
     | 
    
         
            +
                      if t == :now || t == :today
         
     | 
| 
      
 335 
     | 
    
         
            +
                        now_for_column(column)
         
     | 
| 
      
 336 
     | 
    
         
            +
                      else
         
     | 
| 
      
 337 
     | 
    
         
            +
                        [connection.quote(t, column),
         
     | 
| 
      
 338 
     | 
    
         
            +
                         primitive_type_for_column(column)
         
     | 
| 
      
 339 
     | 
    
         
            +
                        ].join('::')
         
     | 
| 
      
 340 
     | 
    
         
            +
                      end
         
     | 
| 
       340 
341 
     | 
    
         
             
                    end
         
     | 
| 
       341 
342 
     | 
    
         | 
| 
       342 
     | 
    
         
            -
                    def  
     | 
| 
       343 
     | 
    
         
            -
                       
     | 
| 
       344 
     | 
    
         
            -
             
     | 
| 
       345 
     | 
    
         
            -
             
     | 
| 
       346 
     | 
    
         
            -
             
     | 
| 
       347 
     | 
    
         
            -
             
     | 
| 
       348 
     | 
    
         
            -
             
     | 
| 
       349 
     | 
    
         
            -
             
     | 
| 
       350 
     | 
    
         
            -
             
     | 
| 
       351 
     | 
    
         
            -
             
     | 
| 
       352 
     | 
    
         
            -
                       
     | 
| 
      
 343 
     | 
    
         
            +
                    def now_for_column(column)
         
     | 
| 
      
 344 
     | 
    
         
            +
                      case column.type
         
     | 
| 
      
 345 
     | 
    
         
            +
                      when :tsrange, :tstzrange then "timezone('UTC', current_timestamp)"
         
     | 
| 
      
 346 
     | 
    
         
            +
                      when :daterange           then "current_date"
         
     | 
| 
      
 347 
     | 
    
         
            +
                      else raise "Cannot generate 'now()' for #{column.type} column #{column.name}"
         
     | 
| 
      
 348 
     | 
    
         
            +
                      end
         
     | 
| 
      
 349 
     | 
    
         
            +
                    end
         
     | 
| 
      
 350 
     | 
    
         
            +
             
     | 
| 
      
 351 
     | 
    
         
            +
                    def primitive_type_for_column(column)
         
     | 
| 
      
 352 
     | 
    
         
            +
                      case column.type
         
     | 
| 
      
 353 
     | 
    
         
            +
                      when :tsrange   then :timestamp
         
     | 
| 
      
 354 
     | 
    
         
            +
                      when :tstzrange then :timestamptz
         
     | 
| 
      
 355 
     | 
    
         
            +
                      when :daterange then :date
         
     | 
| 
      
 356 
     | 
    
         
            +
                      else raise "Don't know how to map #{column.type} column #{column.name} to a primitive type"
         
     | 
| 
      
 357 
     | 
    
         
            +
                      end
         
     | 
| 
      
 358 
     | 
    
         
            +
                    end
         
     | 
| 
      
 359 
     | 
    
         
            +
             
     | 
| 
      
 360 
     | 
    
         
            +
                    def build_time_query_at(time, range)
         
     | 
| 
      
 361 
     | 
    
         
            +
                      time = if time.kind_of?(Array)
         
     | 
| 
      
 362 
     | 
    
         
            +
                        time.map! {|t| time_for_time_query(t, range)}
         
     | 
| 
      
 363 
     | 
    
         
            +
                      else
         
     | 
| 
      
 364 
     | 
    
         
            +
                        time_for_time_query(time, range)
         
     | 
| 
      
 365 
     | 
    
         
            +
                      end
         
     | 
| 
      
 366 
     | 
    
         
            +
             
     | 
| 
      
 367 
     | 
    
         
            +
                      build_time_query(time, range)
         
     | 
| 
      
 368 
     | 
    
         
            +
                    end
         
     | 
| 
      
 369 
     | 
    
         
            +
             
     | 
| 
      
 370 
     | 
    
         
            +
                    def build_time_query(time, range)
         
     | 
| 
      
 371 
     | 
    
         
            +
                      if time.kind_of?(Array)
         
     | 
| 
      
 372 
     | 
    
         
            +
                        %[ #{range.type}(#{time.first}, #{time.last}) && #{table_name}.#{range.name} ]
         
     | 
| 
      
 373 
     | 
    
         
            +
                      else
         
     | 
| 
      
 374 
     | 
    
         
            +
                        %[ #{time} <@ #{table_name}.#{range.name} ]
         
     | 
| 
      
 375 
     | 
    
         
            +
                      end
         
     | 
| 
       353 
376 
     | 
    
         
             
                    end
         
     | 
| 
       354 
377 
     | 
    
         
             
                end
         
     | 
| 
       355 
378 
     | 
    
         | 
| 
         @@ -358,10 +381,41 @@ module ChronoModel 
     | 
|
| 
       358 
381 
     | 
    
         
             
                module HistoryMethods
         
     | 
| 
       359 
382 
     | 
    
         
             
                  include TimeQuery
         
     | 
| 
       360 
383 
     | 
    
         | 
| 
      
 384 
     | 
    
         
            +
                  # In the History context, pre-fill the :on options with the validity interval.
         
     | 
| 
      
 385 
     | 
    
         
            +
                  #
         
     | 
| 
      
 386 
     | 
    
         
            +
                  def time_query(match, time, options = {})
         
     | 
| 
      
 387 
     | 
    
         
            +
                    options[:on] ||= :validity
         
     | 
| 
      
 388 
     | 
    
         
            +
                    super
         
     | 
| 
      
 389 
     | 
    
         
            +
                  end
         
     | 
| 
      
 390 
     | 
    
         
            +
             
     | 
| 
      
 391 
     | 
    
         
            +
                  def past
         
     | 
| 
      
 392 
     | 
    
         
            +
                    time_query(:before, :now).where('NOT upper_inf(validity)')
         
     | 
| 
      
 393 
     | 
    
         
            +
                  end
         
     | 
| 
      
 394 
     | 
    
         
            +
             
     | 
| 
      
 395 
     | 
    
         
            +
                  # To identify this class as the History subclass
         
     | 
| 
      
 396 
     | 
    
         
            +
                  def history?
         
     | 
| 
      
 397 
     | 
    
         
            +
                    true
         
     | 
| 
      
 398 
     | 
    
         
            +
                  end
         
     | 
| 
      
 399 
     | 
    
         
            +
             
     | 
| 
      
 400 
     | 
    
         
            +
                  # Getting the correct quoted_table_name can be tricky when
         
     | 
| 
      
 401 
     | 
    
         
            +
                  # STI is involved. If Orange < Fruit, then Orange::History < Fruit::History
         
     | 
| 
      
 402 
     | 
    
         
            +
                  # (see define_inherited_history_model_for).
         
     | 
| 
      
 403 
     | 
    
         
            +
                  # This means that the superclass method returns Fruit::History, which
         
     | 
| 
      
 404 
     | 
    
         
            +
                  # will give us the wrong table name. What we actually want is the
         
     | 
| 
      
 405 
     | 
    
         
            +
                  # superclass of Fruit::History, which is Fruit. So, we use
         
     | 
| 
      
 406 
     | 
    
         
            +
                  # non_history_superclass instead. -npj
         
     | 
| 
      
 407 
     | 
    
         
            +
                  def non_history_superclass(klass = self)
         
     | 
| 
      
 408 
     | 
    
         
            +
                    if klass.superclass.respond_to?(:history?) && klass.superclass.history?
         
     | 
| 
      
 409 
     | 
    
         
            +
                      non_history_superclass(klass.superclass)
         
     | 
| 
      
 410 
     | 
    
         
            +
                    else
         
     | 
| 
      
 411 
     | 
    
         
            +
                      klass.superclass
         
     | 
| 
      
 412 
     | 
    
         
            +
                    end
         
     | 
| 
      
 413 
     | 
    
         
            +
                  end
         
     | 
| 
      
 414 
     | 
    
         
            +
             
     | 
| 
       361 
415 
     | 
    
         
             
                  # Fetches as of +time+ records.
         
     | 
| 
       362 
416 
     | 
    
         
             
                  #
         
     | 
| 
       363 
417 
     | 
    
         
             
                  def as_of(time, scope = nil)
         
     | 
| 
       364 
     | 
    
         
            -
                    as_of =  
     | 
| 
      
 418 
     | 
    
         
            +
                    as_of = non_history_superclass.unscoped.readonly.from(virtual_table_at(time))
         
     | 
| 
       365 
419 
     | 
    
         | 
| 
       366 
420 
     | 
    
         
             
                    # Add default scopes back if we're passed nil or a
         
     | 
| 
       367 
421 
     | 
    
         
             
                    # specific scope, because we're .unscopeing above.
         
     | 
| 
         @@ -383,7 +437,7 @@ module ChronoModel 
     | 
|
| 
       383 
437 
     | 
    
         | 
| 
       384 
438 
     | 
    
         
             
                  def virtual_table_at(time, name = nil)
         
     | 
| 
       385 
439 
     | 
    
         
             
                    name = name ? connection.quote_table_name(name) :
         
     | 
| 
       386 
     | 
    
         
            -
                       
     | 
| 
      
 440 
     | 
    
         
            +
                      non_history_superclass.quoted_table_name
         
     | 
| 
       387 
441 
     | 
    
         | 
| 
       388 
442 
     | 
    
         
             
                    "(#{at(time).to_sql}) #{name}"
         
     | 
| 
       389 
443 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -391,23 +445,29 @@ module ChronoModel 
     | 
|
| 
       391 
445 
     | 
    
         
             
                  # Fetches history record at the given time
         
     | 
| 
       392 
446 
     | 
    
         
             
                  #
         
     | 
| 
       393 
447 
     | 
    
         
             
                  def at(time)
         
     | 
| 
       394 
     | 
    
         
            -
                    time = Conversions.time_to_utc_string(time.utc) if time.kind_of?(Time)
         
     | 
| 
      
 448 
     | 
    
         
            +
                    time = Conversions.time_to_utc_string(time.utc) if time.kind_of?(Time) && !time.utc?
         
     | 
| 
       395 
449 
     | 
    
         | 
| 
       396 
450 
     | 
    
         
             
                    unscoped.
         
     | 
| 
       397 
     | 
    
         
            -
                      select("#{quoted_table_name}.*, #{connection.quote(time)} AS as_of_time").
         
     | 
| 
       398 
     | 
    
         
            -
                      time_query(:at, time 
     | 
| 
      
 451 
     | 
    
         
            +
                      select("#{quoted_table_name}.*, #{connection.quote(time)}::timestamp AS as_of_time").
         
     | 
| 
      
 452 
     | 
    
         
            +
                      time_query(:at, time)
         
     | 
| 
       399 
453 
     | 
    
         
             
                  end
         
     | 
| 
       400 
454 
     | 
    
         | 
| 
       401 
455 
     | 
    
         
             
                  # Returns the whole history as read only.
         
     | 
| 
       402 
456 
     | 
    
         
             
                  #
         
     | 
| 
       403 
457 
     | 
    
         
             
                  def all
         
     | 
| 
       404 
     | 
    
         
            -
                    readonly 
     | 
| 
       405 
     | 
    
         
            -
             
     | 
| 
      
 458 
     | 
    
         
            +
                    super.readonly
         
     | 
| 
      
 459 
     | 
    
         
            +
                  end
         
     | 
| 
      
 460 
     | 
    
         
            +
             
     | 
| 
      
 461 
     | 
    
         
            +
                  # Returns the history sorted by recorded_at
         
     | 
| 
      
 462 
     | 
    
         
            +
                  #
         
     | 
| 
      
 463 
     | 
    
         
            +
                  def sorted
         
     | 
| 
      
 464 
     | 
    
         
            +
                    all.order(%[ #{quoted_table_name}."recorded_at", #{quoted_table_name}."hid" ])
         
     | 
| 
       406 
465 
     | 
    
         
             
                  end
         
     | 
| 
       407 
466 
     | 
    
         | 
| 
       408 
467 
     | 
    
         
             
                  # Fetches the given +object+ history, sorted by history record time
         
     | 
| 
       409 
468 
     | 
    
         
             
                  # by default. Always includes an "as_of_time" column that is either
         
     | 
| 
       410 
     | 
    
         
            -
                  # the  
     | 
| 
      
 469 
     | 
    
         
            +
                  # the upper bound of the validity range or now() if history validity
         
     | 
| 
      
 470 
     | 
    
         
            +
                  # is maximum.
         
     | 
| 
       411 
471 
     | 
    
         
             
                  #
         
     | 
| 
       412 
472 
     | 
    
         
             
                  def of(object)
         
     | 
| 
       413 
473 
     | 
    
         
             
                    readonly.where(:id => object).extend(HistorySelect)
         
     | 
| 
         @@ -425,11 +485,11 @@ module ChronoModel 
     | 
|
| 
       425 
485 
     | 
    
         
             
                      return super if has_aggregate
         
     | 
| 
       426 
486 
     | 
    
         | 
| 
       427 
487 
     | 
    
         
             
                      if order_values.blank?
         
     | 
| 
       428 
     | 
    
         
            -
                        self.order_values += [ 
     | 
| 
      
 488 
     | 
    
         
            +
                        self.order_values += [ %[#{quoted_table_name}."recorded_at", #{quoted_table_name}."hid"] ]
         
     | 
| 
       429 
489 
     | 
    
         
             
                      end
         
     | 
| 
       430 
490 
     | 
    
         | 
| 
       431 
491 
     | 
    
         
             
                      super.tap do |rel|
         
     | 
| 
       432 
     | 
    
         
            -
                        rel.project("LEAST( 
     | 
| 
      
 492 
     | 
    
         
            +
                        rel.project("LEAST(upper(validity), timezone('UTC', now())) AS as_of_time")
         
     | 
| 
       433 
493 
     | 
    
         
             
                      end
         
     | 
| 
       434 
494 
     | 
    
         
             
                    end
         
     | 
| 
       435 
495 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -451,11 +511,15 @@ module ChronoModel 
     | 
|
| 
       451 
511 
     | 
    
         
             
                      return [] if models.empty?
         
     | 
| 
       452 
512 
     | 
    
         | 
| 
       453 
513 
     | 
    
         
             
                      fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
         
     | 
| 
       454 
     | 
    
         
            -
                      fields.map! {|f| "#{f} + INTERVAL '2 usec'"}
         
     | 
| 
       455 
514 
     | 
    
         | 
| 
       456 
515 
     | 
    
         
             
                      relation = self.
         
     | 
| 
       457 
     | 
    
         
            -
                         
     | 
| 
       458 
     | 
    
         
            -
             
     | 
| 
      
 516 
     | 
    
         
            +
                        select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
         
     | 
| 
      
 517 
     | 
    
         
            +
             
     | 
| 
      
 518 
     | 
    
         
            +
                      if assocs.present?
         
     | 
| 
      
 519 
     | 
    
         
            +
                        relation = relation.joins(*assocs.map(&:name))
         
     | 
| 
      
 520 
     | 
    
         
            +
                      end
         
     | 
| 
      
 521 
     | 
    
         
            +
             
     | 
| 
      
 522 
     | 
    
         
            +
                      relation = relation.
         
     | 
| 
       459 
523 
     | 
    
         
             
                        order('ts ' << (options[:reverse] ? 'DESC' : 'ASC'))
         
     | 
| 
       460 
524 
     | 
    
         | 
| 
       461 
525 
     | 
    
         
             
                      relation = relation.from(%["public".#{quoted_table_name}]) unless self.chrono?
         
     | 
| 
         @@ -471,11 +535,10 @@ module ChronoModel 
     | 
|
| 
       471 
535 
     | 
    
         
             
                        sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after ])}'"
         
     | 
| 
       472 
536 
     | 
    
         
             
                      end
         
     | 
| 
       473 
537 
     | 
    
         | 
| 
       474 
     | 
    
         
            -
                      if rid
         
     | 
| 
       475 
     | 
    
         
            -
                        sql << (self.chrono? ? % 
     | 
| 
       476 
     | 
    
         
            -
                          AND ts  
     | 
| 
       477 
     | 
    
         
            -
             
     | 
| 
       478 
     | 
    
         
            -
                        ] : %[ AND ts < NOW() ])
         
     | 
| 
      
 538 
     | 
    
         
            +
                      if rid && !options[:with]
         
     | 
| 
      
 539 
     | 
    
         
            +
                        sql << (self.chrono? ? %{
         
     | 
| 
      
 540 
     | 
    
         
            +
                          AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} )
         
     | 
| 
      
 541 
     | 
    
         
            +
                        } : %[ AND ts < NOW() ])
         
     | 
| 
       479 
542 
     | 
    
         
             
                      end
         
     | 
| 
       480 
543 
     | 
    
         | 
| 
       481 
544 
     | 
    
         
             
                      sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)
         
     | 
| 
         @@ -510,10 +573,13 @@ module ChronoModel 
     | 
|
| 
       510 
573 
     | 
    
         
             
                  end)
         
     | 
| 
       511 
574 
     | 
    
         | 
| 
       512 
575 
     | 
    
         
             
                  def quoted_history_fields
         
     | 
| 
       513 
     | 
    
         
            -
                    @quoted_history_fields ||=  
     | 
| 
       514 
     | 
    
         
            -
                       
     | 
| 
       515 
     | 
    
         
            -
             
     | 
| 
       516 
     | 
    
         
            -
             
     | 
| 
      
 576 
     | 
    
         
            +
                    @quoted_history_fields ||= begin
         
     | 
| 
      
 577 
     | 
    
         
            +
                      validity =
         
     | 
| 
      
 578 
     | 
    
         
            +
                        [connection.quote_table_name(table_name),
         
     | 
| 
      
 579 
     | 
    
         
            +
                         connection.quote_column_name('validity')
         
     | 
| 
      
 580 
     | 
    
         
            +
                        ].join('.')
         
     | 
| 
      
 581 
     | 
    
         
            +
             
     | 
| 
      
 582 
     | 
    
         
            +
                      [:lower, :upper].map! {|func| "#{func}(#{validity})"}
         
     | 
| 
       517 
583 
     | 
    
         
             
                    end
         
     | 
| 
       518 
584 
     | 
    
         
             
                  end
         
     | 
| 
       519 
585 
     | 
    
         
             
                end
         
     |