chrono_model 1.2.2 → 3.0.1

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.
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