chrono_model 1.2.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +19 -20
  3. data/README.md +73 -62
  4. data/lib/active_record/connection_adapters/chronomodel_adapter.rb +14 -14
  5. data/lib/active_record/tasks/chronomodel_database_tasks.rb +40 -39
  6. data/lib/chrono_model/adapter/ddl.rb +168 -153
  7. data/lib/chrono_model/adapter/indexes.rb +99 -94
  8. data/lib/chrono_model/adapter/migrations.rb +81 -104
  9. data/lib/chrono_model/adapter/migrations_modules/stable.rb +41 -0
  10. data/lib/chrono_model/adapter/tsrange.rb +20 -5
  11. data/lib/chrono_model/adapter/upgrade.rb +89 -91
  12. data/lib/chrono_model/adapter.rb +59 -31
  13. data/lib/chrono_model/chrono.rb +17 -0
  14. data/lib/chrono_model/conversions.rb +14 -8
  15. data/lib/chrono_model/db_console.rb +5 -0
  16. data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
  17. data/lib/chrono_model/patches/as_of_time_relation.rb +3 -13
  18. data/lib/chrono_model/patches/association.rb +15 -12
  19. data/lib/chrono_model/patches/batches.rb +13 -0
  20. data/lib/chrono_model/patches/db_console.rb +20 -4
  21. data/lib/chrono_model/patches/join_node.rb +4 -4
  22. data/lib/chrono_model/patches/preloader.rb +41 -11
  23. data/lib/chrono_model/patches/relation.rb +51 -8
  24. data/lib/chrono_model/patches.rb +3 -1
  25. data/lib/chrono_model/railtie.rb +13 -27
  26. data/lib/chrono_model/time_gate.rb +3 -3
  27. data/lib/chrono_model/time_machine/history_model.rb +65 -31
  28. data/lib/chrono_model/time_machine/time_query.rb +65 -49
  29. data/lib/chrono_model/time_machine/timeline.rb +52 -28
  30. data/lib/chrono_model/time_machine.rb +57 -25
  31. data/lib/chrono_model/utilities.rb +3 -3
  32. data/lib/chrono_model/version.rb +3 -1
  33. data/lib/chrono_model.rb +31 -36
  34. metadata +24 -263
  35. data/.gitignore +0 -21
  36. data/.rspec +0 -2
  37. data/.travis.yml +0 -41
  38. data/Gemfile +0 -4
  39. data/README.sql +0 -161
  40. data/Rakefile +0 -25
  41. data/chrono_model.gemspec +0 -33
  42. data/gemfiles/rails_5.0.gemfile +0 -6
  43. data/gemfiles/rails_5.1.gemfile +0 -6
  44. data/gemfiles/rails_5.2.gemfile +0 -6
  45. data/lib/chrono_model/json.rb +0 -28
  46. data/spec/aruba/dbconsole_spec.rb +0 -25
  47. data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
  48. data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
  49. data/spec/aruba/fixtures/empty_structure.sql +0 -27
  50. data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
  51. data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
  52. data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
  53. data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
  54. data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
  55. data/spec/aruba/migrations_spec.rb +0 -48
  56. data/spec/aruba/rake_task_spec.rb +0 -71
  57. data/spec/chrono_model/adapter/base_spec.rb +0 -157
  58. data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
  59. data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
  60. data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
  61. data/spec/chrono_model/conversions_spec.rb +0 -43
  62. data/spec/chrono_model/history_models_spec.rb +0 -32
  63. data/spec/chrono_model/json_ops_spec.rb +0 -59
  64. data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
  65. data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
  66. data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
  67. data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
  68. data/spec/chrono_model/time_machine/history_spec.rb +0 -104
  69. data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
  70. data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
  71. data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
  72. data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
  73. data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
  74. data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
  75. data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
  76. data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
  77. data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
  78. data/spec/config.travis.yml +0 -5
  79. data/spec/config.yml.example +0 -9
  80. data/spec/spec_helper.rb +0 -33
  81. data/spec/support/adapter/helpers.rb +0 -53
  82. data/spec/support/adapter/structure.rb +0 -44
  83. data/spec/support/aruba.rb +0 -44
  84. data/spec/support/connection.rb +0 -70
  85. data/spec/support/matchers/base.rb +0 -56
  86. data/spec/support/matchers/column.rb +0 -99
  87. data/spec/support/matchers/function.rb +0 -79
  88. data/spec/support/matchers/index.rb +0 -69
  89. data/spec/support/matchers/schema.rb +0 -39
  90. data/spec/support/matchers/table.rb +0 -275
  91. data/spec/support/time_machine/helpers.rb +0 -47
  92. data/spec/support/time_machine/structure.rb +0 -111
  93. data/sql/json_ops.sql +0 -56
  94. data/sql/uninstall-json_ops.sql +0 -24
@@ -1,15 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  module TimeMachine
3
-
4
5
  module HistoryModel
5
6
  extend ActiveSupport::Concern
6
7
 
7
8
  included do
8
- self.table_name = [Adapter::HISTORY_SCHEMA, superclass.table_name].join('.')
9
+ self.table_name = "#{Adapter::HISTORY_SCHEMA}.#{superclass.table_name}"
9
10
 
10
11
  scope :chronological, -> { order(Arel.sql('lower(validity) ASC')) }
11
12
  end
12
13
 
14
+ # ACTIVE RECORD 7 does not call `class.find` but a new internal method called `_find_record`
15
+ def _find_record(options)
16
+ if options && options[:lock]
17
+ self.class.preload(strict_loaded_associations).lock(options[:lock]).find_by!(hid: hid)
18
+ else
19
+ self.class.preload(strict_loaded_associations).find_by!(hid: hid)
20
+ end
21
+ end
22
+
13
23
  # Methods that make up the history interface of the companion History
14
24
  # model, automatically built for each Model that includes TimeMachine
15
25
  #
@@ -21,11 +31,11 @@ module ChronoModel
21
31
  # setting it now and ensuring to reset it to the original one after
22
32
  # execution completes. FIXME
23
33
  #
24
- def with_hid_pkey(&block)
25
- old = self.primary_key
34
+ def with_hid_pkey
35
+ old = primary_key
26
36
  self.primary_key = :hid
27
37
 
28
- block.call
38
+ yield
29
39
  ensure
30
40
  self.primary_key = old
31
41
  end
@@ -61,9 +71,12 @@ module ChronoModel
61
71
  end
62
72
 
63
73
  def virtual_table_at(time, table_name: nil)
64
- virtual_name = table_name ?
65
- connection.quote_table_name(table_name) :
66
- superclass.quoted_table_name
74
+ virtual_name =
75
+ if table_name
76
+ connection.quote_table_name(table_name)
77
+ else
78
+ superclass.quoted_table_name
79
+ end
67
80
 
68
81
  "(#{at(time).to_sql}) #{virtual_name}"
69
82
  end
@@ -77,7 +90,7 @@ module ChronoModel
77
90
  # Returns the history sorted by recorded_at
78
91
  #
79
92
  def sorted
80
- all.order(Arel.sql(%[ #{quoted_table_name}."recorded_at" ASC, #{quoted_table_name}."hid" ASC ]))
93
+ all.order(Arel.sql(%( #{quoted_table_name}."recorded_at" ASC, #{quoted_table_name}."hid" ASC )))
81
94
  end
82
95
 
83
96
  # Fetches the given +object+ history, sorted by history record time
@@ -96,9 +109,7 @@ module ChronoModel
96
109
  #
97
110
  # As such it is overriden here to return the same contents that
98
111
  # the parent would have returned.
99
- def sti_name
100
- superclass.sti_name
101
- end
112
+ delegate :sti_name, to: :superclass
102
113
 
103
114
  # For STI to work, the history model needs to have the exact same
104
115
  # semantics as the model it inherits from. However given it is
@@ -107,18 +118,17 @@ module ChronoModel
107
118
  # same exact hierarchy location as its parent, thus this is defined in
108
119
  # this override.
109
120
  #
110
- def descends_from_active_record?
111
- superclass.descends_from_active_record?
112
- end
121
+ delegate :descends_from_active_record?, to: :superclass
113
122
 
114
123
  private
115
- # STI fails when a Foo::History record has Foo as type in the
116
- # inheritance column; AR expects the type to be an instance of the
117
- # current class or a descendant (or self).
118
- #
119
- def find_sti_class(type_name)
120
- super(type_name + "::History")
121
- end
124
+
125
+ # STI fails when a Foo::History record has Foo as type in the
126
+ # inheritance column; AR expects the type to be an instance of the
127
+ # current class or a descendant (or self).
128
+ #
129
+ def find_sti_class(type_name)
130
+ super("#{type_name}::History")
131
+ end
122
132
  end
123
133
 
124
134
  # The history id is `hid`, but this cannot set as primary key
@@ -134,15 +144,15 @@ module ChronoModel
134
144
  end
135
145
 
136
146
  def save(*)
137
- self.class.with_hid_pkey { super }
147
+ with_hid_pkey { super }
138
148
  end
139
149
 
140
150
  def save!(*)
141
- self.class.with_hid_pkey { super }
151
+ with_hid_pkey { super }
142
152
  end
143
153
 
144
154
  def update_columns(*)
145
- self.class.with_hid_pkey { super }
155
+ with_hid_pkey { super }
146
156
  end
147
157
 
148
158
  def historical?
@@ -153,7 +163,7 @@ module ChronoModel
153
163
  # is the first one.
154
164
  #
155
165
  def pred
156
- return if self.valid_from.nil?
166
+ return if valid_from.nil?
157
167
 
158
168
  if self.class.timeline_associations.empty?
159
169
  self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first
@@ -166,7 +176,7 @@ module ChronoModel
166
176
  # last one.
167
177
  #
168
178
  def succ
169
- return if self.valid_to.nil?
179
+ return if valid_to.nil?
170
180
 
171
181
  if self.class.timeline_associations.empty?
172
182
  self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first
@@ -174,7 +184,7 @@ module ChronoModel
174
184
  super(id: rid, after: valid_to, table: self.class.superclass.quoted_table_name)
175
185
  end
176
186
  end
177
- alias :next :succ
187
+ alias next succ
178
188
 
179
189
  # Returns the first history entry
180
190
  #
@@ -194,9 +204,9 @@ module ChronoModel
194
204
  self.class.superclass.find(rid)
195
205
  end
196
206
 
197
- def record #:nodoc:
207
+ def record # :nodoc:
198
208
  ActiveSupport::Deprecation.warn '.record is deprecated in favour of .current_version'
199
- self.current_version
209
+ current_version
200
210
  end
201
211
 
202
212
  def valid_from
@@ -211,7 +221,31 @@ module ChronoModel
211
221
  def recorded_at
212
222
  ChronoModel::Conversions.string_to_utc_time attributes_before_type_cast['recorded_at']
213
223
  end
214
- end
215
224
 
225
+ # Starting from Rails 6.0, `.read_attribute` will use the memoized
226
+ # `primary_key` if it detects that the attribute name is `id`.
227
+ #
228
+ # Since the `primary key` may have been changed to `hid` because of
229
+ # `.find` overload, the new behavior may break relations where `id` is
230
+ # still the correct attribute to read
231
+ #
232
+ # Ref: ifad/chronomodel#181
233
+ def read_attribute(attr_name, &block)
234
+ return super unless attr_name.to_s == 'id' && @primary_key.to_s == 'hid'
235
+
236
+ _read_attribute('id', &block)
237
+ end
238
+
239
+ private
240
+
241
+ def with_hid_pkey(&block)
242
+ old_primary_key = @primary_key
243
+ @primary_key = :hid
244
+
245
+ self.class.with_hid_pkey(&block)
246
+ ensure
247
+ @primary_key = old_primary_key
248
+ end
249
+ end
216
250
  end
217
251
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  module TimeMachine
3
-
4
5
  #
5
6
  # TODO Documentation
6
7
  #
@@ -12,75 +13,90 @@ module ChronoModel
12
13
  end
13
14
 
14
15
  private
15
- def time_query_sql(match, time, range, options)
16
- case match
17
- when :at
18
- build_time_query_at(time, range)
19
16
 
20
- when :not
21
- "NOT (#{build_time_query_at(time, range)})"
17
+ def time_query_sql(match, time, range, options)
18
+ case match
19
+ when :at
20
+ build_time_query_at(time, range)
22
21
 
23
- when :before
24
- op = options.fetch(:inclusive, true) ? '&&' : '@>'
25
- build_time_query(['NULL', time_for_time_query(time, range)], range, op)
22
+ when :not
23
+ "NOT (#{build_time_query_at(time, range)})"
26
24
 
27
- when :after
28
- op = options.fetch(:inclusive, true) ? '&&' : '@>'
29
- build_time_query([time_for_time_query(time, range), 'NULL'], range, op)
25
+ when :before
26
+ op =
27
+ if options.fetch(:inclusive, true)
28
+ '&&'
29
+ else
30
+ '@>'
31
+ end
32
+ build_time_query(['NULL', time_for_time_query(time, range)], range, op)
30
33
 
31
- else
32
- raise ChronoModel::Error, "Invalid time_query: #{match}"
33
- end
34
+ when :after
35
+ op =
36
+ if options.fetch(:inclusive, true)
37
+ '&&'
38
+ else
39
+ '@>'
40
+ end
41
+ build_time_query([time_for_time_query(time, range), 'NULL'], range, op)
42
+
43
+ else
44
+ raise ChronoModel::Error, "Invalid time_query: #{match}"
34
45
  end
46
+ end
35
47
 
36
- def time_for_time_query(t, column)
37
- if t == :now || t == :today
38
- now_for_column(column)
39
- else
40
- quoted_t = connection.quote(connection.quoted_date(t))
41
- [quoted_t, primitive_type_for_column(column)].join('::')
42
- end
48
+ def time_for_time_query(t, column)
49
+ if t == :now || t == :today
50
+ now_for_column(column)
51
+ else
52
+ quoted_t = connection.quote(connection.quoted_date(t))
53
+ "#{quoted_t}::#{primitive_type_for_column(column)}"
43
54
  end
55
+ end
44
56
 
45
- def now_for_column(column)
46
- case column.type
47
- when :tsrange, :tstzrange then "timezone('UTC', current_timestamp)"
48
- when :daterange then "current_date"
49
- else raise "Cannot generate 'now()' for #{column.type} column #{column.name}"
50
- end
57
+ def now_for_column(column)
58
+ case column.type
59
+ when :tsrange, :tstzrange then "timezone('UTC', current_timestamp)"
60
+ when :daterange then 'current_date'
61
+ else raise "Cannot generate 'now()' for #{column.type} column #{column.name}"
51
62
  end
63
+ end
52
64
 
53
- def primitive_type_for_column(column)
54
- case column.type
55
- when :tsrange then :timestamp
56
- when :tstzrange then :timestamptz
57
- when :daterange then :date
58
- else raise "Don't know how to map #{column.type} column #{column.name} to a primitive type"
59
- end
65
+ def primitive_type_for_column(column)
66
+ case column.type
67
+ when :tsrange then :timestamp
68
+ when :tstzrange then :timestamptz
69
+ when :daterange then :date
70
+ else raise "Don't know how to map #{column.type} column #{column.name} to a primitive type"
60
71
  end
72
+ end
61
73
 
62
- def build_time_query_at(time, range)
63
- time = if time.kind_of?(Array)
64
- time.map! {|t| time_for_time_query(t, range)}
74
+ def build_time_query_at(time, range)
75
+ time =
76
+ if time.is_a?(Array)
77
+ time.map! { |t| time_for_time_query(t, range) }
65
78
 
66
79
  # If both edges of the range are the same the query fails using the '&&' operator.
67
80
  # The correct solution is to use the <@ operator.
68
- time.first == time.last ? time.first : time
81
+ if time.first == time.last
82
+ time.first
83
+ else
84
+ time
85
+ end
69
86
  else
70
87
  time_for_time_query(time, range)
71
88
  end
72
89
 
73
- build_time_query(time, range)
74
- end
90
+ build_time_query(time, range)
91
+ end
75
92
 
76
- def build_time_query(time, range, op = '&&')
77
- if time.kind_of?(Array)
78
- Arel.sql %[ #{range.type}(#{time.first}, #{time.last}) #{op} #{table_name}.#{range.name} ]
79
- else
80
- Arel.sql %[ #{time} <@ #{table_name}.#{range.name} ]
81
- end
93
+ def build_time_query(time, range, op = '&&')
94
+ if time.is_a?(Array)
95
+ Arel.sql %[ #{range.type}(#{time.first}, #{time.last}) #{op} #{table_name}.#{range.name} ]
96
+ else
97
+ Arel.sql %( #{time} <@ #{table_name}.#{range.name} )
82
98
  end
99
+ end
83
100
  end
84
-
85
101
  end
86
102
  end
@@ -1,59 +1,87 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  module TimeMachine
3
-
4
5
  module Timeline
5
6
  # Returns an Array of unique UTC timestamps for which at least an
6
7
  # history record exists. Takes temporal associations into account.
7
8
  #
8
9
  def timeline(record = nil, options = {})
9
- rid = record.respond_to?(:rid) ? record.rid : record.id if record
10
+ rid =
11
+ if record
12
+ if record.respond_to?(:rid)
13
+ record.rid
14
+ else
15
+ record.id
16
+ end
17
+ end
10
18
 
11
- assocs = options.key?(:with) ?
12
- timeline_associations_from(options[:with]) : timeline_associations
19
+ assocs =
20
+ if options.key?(:with)
21
+ timeline_associations_from(options[:with])
22
+ else
23
+ timeline_associations
24
+ end
13
25
 
14
26
  models = []
15
- models.push self if self.chrono?
16
- models.concat(assocs.map {|a| a.klass.history})
27
+ models.push self if chrono?
28
+ models.concat(assocs.map { |a| a.klass.history })
17
29
 
18
30
  return [] if models.empty?
19
31
 
20
- fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
32
+ fields = models.inject([]) { |a, m| a.concat m.quoted_history_fields }
21
33
 
22
- relation = self.except(:order).
23
- select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
34
+ relation = except(:order)
35
+ .select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
24
36
 
25
37
  if assocs.present?
26
- relation = relation.joins(*assocs.map(&:name))
38
+ assocs.each do |ass|
39
+ association_quoted_table_name = connection.quote_table_name(ass.table_name)
40
+ # `join` first, then use `where`s
41
+ relation =
42
+ if ass.belongs_to?
43
+ relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.association_primary_key} = #{quoted_table_name}.#{ass.association_foreign_key}")
44
+ else
45
+ relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.foreign_key} = #{quoted_table_name}.#{primary_key}")
46
+ end
47
+ end
27
48
  end
28
49
 
29
- relation = relation.
30
- order('ts ' << (options[:reverse] ? 'DESC' : 'ASC'))
50
+ relation_order =
51
+ if options[:reverse]
52
+ 'DESC'
53
+ else
54
+ 'ASC'
55
+ end
56
+
57
+ relation = relation.order("ts #{relation_order}")
31
58
 
32
- relation = relation.from(%["public".#{quoted_table_name}]) unless self.chrono?
59
+ relation = relation.from("public.#{quoted_table_name}") unless chrono?
33
60
  relation = relation.where(id: rid) if rid
34
61
 
35
- sql = "SELECT ts FROM ( #{relation.to_sql} ) foo WHERE ts IS NOT NULL"
62
+ sql = +"SELECT ts FROM ( #{relation.to_sql} ) AS foo WHERE ts IS NOT NULL"
36
63
 
37
64
  if options.key?(:before)
38
65
  sql << " AND ts < '#{Conversions.time_to_utc_string(options[:before])}'"
39
66
  end
40
67
 
41
68
  if options.key?(:after)
42
- sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after ])}'"
69
+ sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after])}'"
43
70
  end
44
71
 
45
72
  if rid && !options[:with]
46
- sql << (self.chrono? ? %{
47
- AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} )
48
- } : %[ AND ts < NOW() ])
73
+ sql <<
74
+ if chrono?
75
+ %{ AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} ) }
76
+ else
77
+ %[ AND ts < NOW() ]
78
+ end
49
79
  end
50
80
 
51
81
  sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)
52
82
 
53
- sql.gsub! 'INNER JOIN', 'LEFT OUTER JOIN'
54
-
55
83
  connection.on_schema(Adapter::HISTORY_SCHEMA) do
56
- connection.select_values(sql, "#{self.name} periods").map! do |ts|
84
+ connection.select_values(sql, "#{name} periods").map! do |ts|
57
85
  Conversions.string_to_utc_time ts
58
86
  end
59
87
  end
@@ -74,21 +102,17 @@ module ChronoModel
74
102
  def timeline_associations_from(names)
75
103
  Array.wrap(names).map do |name|
76
104
  reflect_on_association(name) or raise ArgumentError,
77
- "No association found for name `#{name}'"
105
+ "No association found for name `#{name}'"
78
106
  end
79
107
  end
80
108
 
81
109
  def quoted_history_fields
82
110
  @quoted_history_fields ||= begin
83
- validity =
84
- [connection.quote_table_name(table_name),
85
- connection.quote_column_name('validity')
86
- ].join('.')
111
+ validity = "#{quoted_table_name}.#{connection.quote_column_name('validity')}"
87
112
 
88
- [:lower, :upper].map! {|func| "#{func}(#{validity})"}
113
+ %w[lower upper].map! { |func| "#{func}(#{validity})" }
89
114
  end
90
115
  end
91
116
  end
92
-
93
117
  end
94
118
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'chrono_model/time_machine/time_query'
2
4
  require 'chrono_model/time_machine/timeline'
3
5
  require 'chrono_model/time_machine/history_model'
4
6
 
5
7
  module ChronoModel
6
-
7
8
  module TimeMachine
8
9
  include ChronoModel::Patches::AsOfTimeHolder
9
10
 
@@ -11,20 +12,41 @@ module ChronoModel
11
12
 
12
13
  included do
13
14
  if table_exists? && !chrono?
14
- puts "ChronoModel: #{table_name} is not a temporal table. " \
15
- "Please use `change_table :#{table_name}, temporal: true` in a migration."
15
+ logger.warn <<-MSG.squish
16
+ ChronoModel: #{table_name} is not a temporal table.
17
+ Please use `change_table :#{table_name}, temporal: true` in a migration.
18
+ MSG
16
19
  end
17
20
 
18
21
  history = ChronoModel::TimeMachine.define_history_model_for(self)
19
22
  ChronoModel.history_models[table_name] = history
20
23
 
21
24
  class << self
22
- alias_method :direct_descendants_with_history, :direct_descendants
23
- def direct_descendants
24
- direct_descendants_with_history.reject(&:history?)
25
+ def subclasses(with_history: false)
26
+ subclasses = super()
27
+ subclasses.reject!(&:history?) unless with_history
28
+ subclasses
29
+ end
30
+
31
+ def subclasses_with_history
32
+ subclasses(with_history: true)
33
+ end
34
+
35
+ # `direct_descendants` is deprecated method in 7.0 and has been
36
+ # removed in 7.1
37
+ if method_defined?(:direct_descendants)
38
+ alias_method :direct_descendants_with_history, :subclasses_with_history
39
+ alias_method :direct_descendants, :subclasses
40
+ end
41
+
42
+ # Ruby 3.1 has a native subclasses method and descendants is
43
+ # implemented with recursion of subclasses
44
+ if Class.method_defined?(:subclasses)
45
+ def descendants_with_history
46
+ subclasses_with_history.concat(subclasses.flat_map(&:descendants_with_history))
47
+ end
25
48
  end
26
49
 
27
- alias_method :descendants_with_history, :descendants
28
50
  def descendants
29
51
  descendants_with_history.reject(&:history?)
30
52
  end
@@ -43,9 +65,9 @@ module ChronoModel
43
65
  # Sadly, we can't avoid it by calling +.history?+, because in the
44
66
  # subclass the HistoryModel hasn't been included yet.
45
67
  #
46
- unless subclass.name.nil?
47
- ChronoModel::TimeMachine.define_history_model_for(subclass)
48
- end
68
+ return if subclass.name.nil?
69
+
70
+ ChronoModel::TimeMachine.define_history_model_for(subclass)
49
71
  end
50
72
  end
51
73
  end
@@ -71,21 +93,23 @@ module ChronoModel
71
93
 
72
94
  # Returns an ActiveRecord::Relation on the history of this model as
73
95
  # it was +time+ ago.
74
- def as_of(time)
75
- history.as_of(time)
76
- end
96
+ delegate :as_of, to: :history
77
97
 
78
98
  def attribute_names_for_history_changes
79
99
  @attribute_names_for_history_changes ||= attribute_names -
80
- %w( id hid validity recorded_at )
100
+ %w[id hid validity recorded_at]
81
101
  end
82
102
 
83
103
  def has_timeline(options)
84
104
  changes = options.delete(:changes)
85
105
  assocs = history.has_timeline(options)
86
106
 
87
- attributes = changes.present? ?
88
- Array.wrap(changes) : assocs.map(&:name)
107
+ attributes =
108
+ if changes.present?
109
+ Array.wrap(changes)
110
+ else
111
+ assocs.map(&:name)
112
+ end
89
113
 
90
114
  attribute_names_for_history_changes.concat(attributes.map(&:to_s))
91
115
  end
@@ -112,7 +136,7 @@ module ChronoModel
112
136
  # reasons, to avoid a `rescue` (@lleirborras).
113
137
  #
114
138
  def _as_of(time)
115
- self.class.as_of(time).where(id: self.id)
139
+ self.class.as_of(time).where(id: id)
116
140
  end
117
141
  protected :_as_of
118
142
 
@@ -132,13 +156,14 @@ module ChronoModel
132
156
  # Returns a boolean indicating whether this record is an history entry.
133
157
  #
134
158
  def historical?
135
- self.as_of_time.present?
159
+ as_of_time.present?
136
160
  end
137
161
 
138
162
  # Inhibit destroy of historical records
139
163
  #
140
164
  def destroy
141
165
  raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records' if historical?
166
+
142
167
  super
143
168
  end
144
169
 
@@ -147,7 +172,7 @@ module ChronoModel
147
172
  #
148
173
  def pred(options = {})
149
174
  if self.class.timeline_associations.empty?
150
- history.order(Arel.sql('upper(validity) DESC')).offset(1).first
175
+ history.reverse_order.second
151
176
  else
152
177
  return nil unless (ts = pred_timestamp(options))
153
178
 
@@ -178,7 +203,11 @@ module ChronoModel
178
203
  # Returns the current history version
179
204
  #
180
205
  def current_version
181
- self.historical? ? self.class.find(self.id) : self
206
+ if historical?
207
+ self.class.find(id)
208
+ else
209
+ self
210
+ end
182
211
  end
183
212
 
184
213
  # Returns the differences between this entry and the previous history one.
@@ -196,17 +225,20 @@ module ChronoModel
196
225
  #
197
226
  def changes_against(ref)
198
227
  self.class.attribute_names_for_history_changes.inject({}) do |changes, attr|
199
- old, new = ref.public_send(attr), self.public_send(attr)
228
+ old = ref.public_send(attr)
229
+ new = public_send(attr)
200
230
 
201
231
  changes.tap do |c|
202
- changed = old.respond_to?(:history_eql?) ?
203
- !old.history_eql?(new) : old != new
232
+ changed =
233
+ if old.respond_to?(:history_eql?)
234
+ !old.history_eql?(new)
235
+ else
236
+ old != new
237
+ end
204
238
 
205
239
  c[attr] = [old, new] if changed
206
240
  end
207
241
  end
208
242
  end
209
-
210
243
  end
211
-
212
244
  end
@@ -1,5 +1,6 @@
1
- module ChronoModel
1
+ # frozen_string_literal: true
2
2
 
3
+ module ChronoModel
3
4
  module Utilities
4
5
  # Amends the given history item setting a different period.
5
6
  # Useful when migrating from legacy systems.
@@ -11,7 +12,7 @@ module ChronoModel
11
12
  # end
12
13
  #
13
14
  def amend_period!(hid, from, to)
14
- unless [from, to].any? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
15
+ unless [from, to].any? { |ts| ts.respond_to?(:zone) && ts.zone == 'UTC' }
15
16
  raise 'Can amend history only with UTC timestamps'
16
17
  end
17
18
 
@@ -23,5 +24,4 @@ module ChronoModel
23
24
  ]
24
25
  end
25
26
  end
26
-
27
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
- VERSION = "1.2.2"
4
+ VERSION = '3.0.1'
3
5
  end