activerecord-bitemporal 0.0.1 → 1.0.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.
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./bitemporal.rb"
4
+
5
+ module ActiveRecord::Bitemporal
6
+ module Patches
7
+ using Module.new {
8
+ refine ::ActiveRecord::Reflection::AssociationReflection do
9
+ # handle raise error, when `polymorphic? == true`
10
+ def klass
11
+ polymorphic? ? nil : super
12
+ end
13
+ end
14
+ }
15
+ using BitemporalChecker
16
+
17
+ # nested_attributes 用の拡張
18
+ module Persistence
19
+ using Module.new {
20
+ refine Persistence do
21
+ def copy_bitemporal_option(src, dst)
22
+ return unless [src.class, dst.class].all? { |klass|
23
+ # NOTE: Can't call refine method.
24
+ # klass.bi_temporal_model?
25
+ klass.include?(ActiveRecord::Bitemporal)
26
+ }
27
+ dst.bitemporal_option_merge! src.bitemporal_option
28
+ end
29
+ end
30
+ }
31
+
32
+ # MEMO: このメソッドは BTDM 以外にもフックする必要がある
33
+ def assign_nested_attributes_for_one_to_one_association(association_name, _attributes)
34
+ super
35
+ target = send(association_name)
36
+ return if target.nil? || !target.changed?
37
+ copy_bitemporal_option(self, target)
38
+ end
39
+
40
+ def assign_nested_attributes_for_collection_association(association_name, _attributes_collection)
41
+ # Preloading records
42
+ send(association_name).load if association(association_name).klass&.bi_temporal_model?
43
+ super
44
+ send(association_name)&.each do |target|
45
+ next unless target.changed?
46
+ copy_bitemporal_option(self, target)
47
+ end
48
+ end
49
+ end
50
+
51
+ module Association
52
+ def skip_statement_cache?(scope)
53
+ super || bi_temporal_model?
54
+ end
55
+
56
+ def scope
57
+ scope = super
58
+ return scope unless scope.bi_temporal_model?
59
+
60
+ scope_ = scope
61
+ if owner.class&.bi_temporal_model?
62
+ if owner.valid_datetime
63
+ valid_datetime = owner.valid_datetime
64
+ scope_ = scope_.valid_at(valid_datetime)
65
+ scope_.merge!(scope_.bitemporal_value[:through].valid_at(valid_datetime)) if scope_.bitemporal_value[:through]
66
+ end
67
+
68
+ if owner.transaction_datetime
69
+ transaction_datetime = owner.transaction_datetime
70
+ scope_ = scope_.transaction_at(transaction_datetime)
71
+ scope_.merge!(scope_.bitemporal_value[:through].transaction_at(transaction_datetime)) if scope_.bitemporal_value[:through]
72
+ end
73
+ end
74
+ return scope_
75
+ end
76
+
77
+ private
78
+
79
+ def bi_temporal_model?
80
+ owner.class.bi_temporal_model? && klass&.bi_temporal_model?
81
+ end
82
+ end
83
+
84
+ module ThroughAssociation
85
+ def target_scope
86
+ scope = super
87
+ return scope unless scope.bi_temporal_model?
88
+
89
+ reflection.chain.drop(1).each do |reflection|
90
+ klass = reflection.klass&.scope_for_association&.klass
91
+ next unless klass&.bi_temporal_model?
92
+ scope.bitemporal_value[:through] = klass
93
+ end
94
+ scope
95
+ end
96
+ end
97
+
98
+ module AssociationReflection
99
+ JoinKeys = Struct.new(:key, :foreign_key)
100
+ def get_join_keys(association_klass)
101
+ return super unless association_klass&.bi_temporal_model?
102
+ self.belongs_to? ? JoinKeys.new(association_klass.bitemporal_id_key, join_foreign_key) : super
103
+ end
104
+
105
+ def primary_key(klass)
106
+ return super unless klass&.bi_temporal_model?
107
+ klass.bitemporal_id_key
108
+ end
109
+ end
110
+
111
+ module Merger
112
+ def merge
113
+ if relation.klass.bi_temporal_model? && other.klass.bi_temporal_model?
114
+ relation.bitemporal_value.merge! other.bitemporal_value
115
+ end
116
+ super
117
+ end
118
+ end
119
+
120
+ module SingularAssociation
121
+ # MEMO: Except for primary_key in ActiveRecord
122
+ # https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/associations/singular_association.rb#L34-L36
123
+ # excluding bitemporal_id_key
124
+ def scope_for_create
125
+ return super unless klass&.bi_temporal_model?
126
+ super.except!(klass.bitemporal_id_key)
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,501 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Bitemporal
4
+ module NodeBitemporalInclude
5
+ refine String do
6
+ def bitemporal_include?(*)
7
+ false
8
+ end
9
+ end
10
+
11
+ refine ::Arel::Nodes::Node do
12
+ def bitemporal_include?(*columns)
13
+ case self
14
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
15
+ if self.left.kind_of?(Arel::Attributes::Attribute)
16
+ subrelation = self.left
17
+ columns.include?(subrelation.name.to_s) || columns.include?("#{subrelation.relation.name}.#{subrelation.name}")
18
+ elsif self.right.kind_of?(Arel::Attributes::Attribute)
19
+ subrelation = self.right
20
+ columns.include?(subrelation.name.to_s) || columns.include?("#{subrelation.relation.name}.#{subrelation.name}")
21
+ end
22
+ else
23
+ false
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ module Relation
30
+ using Module.new {
31
+ refine ActiveRecord::Relation::WhereClause do
32
+ using Module.new {
33
+ refine Arel::Nodes::LessThan do
34
+ def operator; :< ; end
35
+ end
36
+ refine Arel::Nodes::LessThanOrEqual do
37
+ def operator; :<= ; end
38
+ end
39
+ refine Arel::Nodes::GreaterThan do
40
+ def operator; :> ; end
41
+ end
42
+ refine Arel::Nodes::GreaterThanOrEqual do
43
+ def operator; :>= ; end
44
+ end
45
+ refine Arel::Nodes::Node do
46
+ def operator; nil ; end
47
+ end
48
+ }
49
+
50
+ def each_operatable_node(nodes = predicates, &block)
51
+ if block
52
+ each_operatable_node(nodes).each(&block)
53
+ else
54
+ Enumerator.new { |y|
55
+ Array(nodes).each { |node|
56
+ case node
57
+ when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
58
+ y << node if node && node.left.respond_to?(:relation)
59
+ when Arel::Nodes::Or, Arel::Nodes::And
60
+ if Gem::Version.new("6.1.0") <= ActiveRecord.version
61
+ each_operatable_node(node.left) { |node| y << node }
62
+ each_operatable_node(node.right) { |node| y << node }
63
+ end
64
+ when Arel::Nodes::Grouping
65
+ each_operatable_node(node.expr) { |node| y << node }
66
+ end
67
+ }
68
+ }
69
+ end
70
+ end
71
+
72
+ def bitemporal_query_hash(*names)
73
+ each_operatable_node
74
+ .select { |node| names.include? node.left.name.to_s }
75
+ .inject(Hash.new { |hash, key| hash[key] = {} }) { |result, node|
76
+ value = node.right.try(:val) || node.right.try(:value).then { |it| it.try(:value_before_type_cast) || it }
77
+ result[node.left.relation.name][node.left.name.to_s] = [node.operator, value]
78
+ result
79
+ }
80
+ end
81
+ end
82
+
83
+ refine Relation do
84
+ def bitemporal_clause(table_name = klass.table_name)
85
+ node_hash = where_clause.bitemporal_query_hash("valid_from", "valid_to", "transaction_from", "transaction_to")
86
+ valid_from = node_hash.dig(table_name, "valid_from", 1)
87
+ valid_to = node_hash.dig(table_name, "valid_to", 1)
88
+ transaction_from = node_hash.dig(table_name, "transaction_from", 1)
89
+ transaction_to = node_hash.dig(table_name, "transaction_to", 1)
90
+ {
91
+ valid_from: valid_from,
92
+ valid_to: valid_to,
93
+ valid_datetime: valid_from == valid_to ? valid_from : nil,
94
+ transaction_from: transaction_from,
95
+ transaction_to: transaction_to,
96
+ transaction_datetime: transaction_from == transaction_to ? transaction_from : nil,
97
+ ignore_valid_datetime: valid_from.nil? && valid_to.nil? ? true : false,
98
+ ignore_transaction_datetime: transaction_from.nil? && transaction_to.nil? ? true : false
99
+ }
100
+ end
101
+ end
102
+ }
103
+
104
+ if ActiveRecord.version < Gem::Version.new("6.1.0")
105
+ class WhereClauseWithCheckTable < ActiveRecord::Relation::WhereClause
106
+ using NodeBitemporalInclude
107
+
108
+ def bitemporal_include?(column)
109
+ !!predicates.grep(::Arel::Nodes::Node).find do |node|
110
+ node.bitemporal_include?(column)
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def except_predicates(columns)
117
+ columns = Array(columns)
118
+ predicates.reject do |node|
119
+ ::Arel::Nodes::Node === node && node.bitemporal_include?(*columns)
120
+ end
121
+ end
122
+ end
123
+
124
+ def where_clause
125
+ WhereClauseWithCheckTable.new(super.send(:predicates))
126
+ end
127
+ end
128
+
129
+ module MergeWithExceptBitemporalDefaultScope
130
+ using BitemporalChecker
131
+ def merge(other)
132
+ if other.is_a?(Relation) && other.klass.bi_temporal_model?
133
+ super(other.except_bitemporal_default_scope)
134
+ else
135
+ super
136
+ end
137
+ end
138
+ end
139
+
140
+ def valid_datetime
141
+ bitemporal_clause[:valid_datetime]&.in_time_zone
142
+ end
143
+
144
+ def transaction_datetime
145
+ bitemporal_clause[:transaction_datetime]&.in_time_zone
146
+ end
147
+
148
+ def bitemporal_option
149
+ ::ActiveRecord::Bitemporal.merge_by(bitemporal_value.merge bitemporal_clause)
150
+ end
151
+
152
+ def bitemporal_option_merge!(other)
153
+ self.bitemporal_value = bitemporal_value.merge other
154
+ end
155
+
156
+ def bitemporal_value
157
+ @values[:bitemporal_value] ||= {}
158
+ end
159
+
160
+ def bitemporal_value=(value)
161
+ @values[:bitemporal_value] = value
162
+ end
163
+ end
164
+
165
+ module Scope
166
+ extend ActiveSupport::Concern
167
+
168
+ module ActiveRecordRelationScope
169
+ refine ::ActiveRecord::Relation do
170
+ def bitemporal_where_bind(attr_name, operator, value)
171
+ where(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
172
+ end
173
+
174
+ def bitemporal_where_bind!(attr_name, operator, value)
175
+ where!(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
176
+ end
177
+
178
+ def with_valid_datetime
179
+ tap { |relation| relation.bitemporal_value[:with_valid_datetime] = true }
180
+ end
181
+
182
+ def without_valid_datetime
183
+ tap { |relation| relation.bitemporal_value[:with_valid_datetime] = false }
184
+ end
185
+
186
+ def with_transaction_datetime
187
+ tap { |relation| relation.bitemporal_value[:with_transaction_datetime] = true }
188
+ end
189
+
190
+ def without_transaction_datetime
191
+ tap { |relation| relation.bitemporal_value[:with_transaction_datetime] = false }
192
+ end
193
+ end
194
+ end
195
+
196
+ included do
197
+ using Module.new {
198
+ refine ActiveRecord::Bitemporal::Optionable do
199
+ def force_valid_datetime?
200
+ bitemporal_option_storage[:force_valid_datetime]
201
+ end
202
+
203
+ def ignore_valid_datetime?
204
+ bitemporal_option_storage[:ignore_valid_datetime]
205
+ end
206
+
207
+ def force_transaction_datetime?
208
+ bitemporal_option_storage[:force_transaction_datetime]
209
+ end
210
+
211
+ def ignore_transaction_datetime?
212
+ bitemporal_option_storage[:ignore_transaction_datetime]
213
+ end
214
+ end
215
+ }
216
+
217
+ if ActiveRecord.version < Gem::Version.new("6.1.0")
218
+ module ActiveRecordRelationScope
219
+ refine ::ActiveRecord::Relation do
220
+ %i(valid_from valid_to transaction_from transaction_to).each { |column|
221
+ module_eval <<-STR, __FILE__, __LINE__ + 1
222
+ def _ignore_#{column}
223
+ unscope(where: "\#{table.name}.#{column}")
224
+ .tap { |relation| relation.merge!(bitemporal_value[:through].unscoped._ignore_#{column}) if bitemporal_value[:through] }
225
+ end
226
+
227
+ def _except_#{column}
228
+ return self unless where_clause.bitemporal_include?("\#{table.name}.#{column}")
229
+ all._ignore_#{column}.tap { |relation|
230
+ relation.unscope_values.delete({ where: "\#{table.name}.#{column}" })
231
+ }
232
+ end
233
+ STR
234
+
235
+ [
236
+ :lt, # column < datetime
237
+ :lteq, # column <= datetime
238
+ :gt, # column > datetime
239
+ :gteq # column >= datetime
240
+ ].each { |op|
241
+ module_eval <<-STR, __FILE__, __LINE__ + 1
242
+ def _#{column}_#{op}(datetime, without_ignore: false)
243
+ target_datetime = datetime&.in_time_zone || Time.current
244
+ relation = self.tap { |relation| break relation._ignore_#{column} unless without_ignore }
245
+ relation.bitemporal_where_bind!(:#{column}, :#{op}, target_datetime)
246
+ .tap { |relation| relation.merge!(bitemporal_value[:through].unscoped._#{column}_#{op}(target_datetime)) if bitemporal_value[:through] }
247
+ end
248
+ STR
249
+ }
250
+ }
251
+ end
252
+ end
253
+ else
254
+ module ActiveRecordRelationScope
255
+ module EqualAttributeName
256
+ refine ::Object do
257
+ def equal_attribute_name(*)
258
+ false
259
+ end
260
+ end
261
+ refine ::Hash do
262
+ def equal_attribute_name(other)
263
+ self[:where].equal_attribute_name(other)
264
+ end
265
+ end
266
+ refine ::Array do
267
+ def equal_attribute_name(other)
268
+ first.equal_attribute_name(other)
269
+ end
270
+ end
271
+ refine ::String do
272
+ def equal_attribute_name(other)
273
+ self == other.to_s
274
+ end
275
+ end
276
+ refine ::Symbol do
277
+ def equal_attribute_name(other)
278
+ self.to_s == other.to_s
279
+ end
280
+ end
281
+ refine ::Arel::Attributes::Attribute do
282
+ def equal_attribute_name(other)
283
+ "#{relation.name}.#{name}" == other.to_s
284
+ end
285
+ end
286
+ end
287
+
288
+ refine ::ActiveRecord::Relation do
289
+ using EqualAttributeName
290
+ using NodeBitemporalInclude
291
+
292
+ def bitemporal_rewhere_bind(attr_name, operator, value, table = self.table)
293
+ rewhere(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
294
+ end
295
+
296
+ %i(valid_from valid_to transaction_from transaction_to).each { |column|
297
+ module_eval <<-STR, __FILE__, __LINE__ + 1
298
+ def _ignore_#{column}
299
+ unscope(where: :"\#{table.name}.#{column}")
300
+ .tap { |relation| relation.unscope!(where: bitemporal_value[:through].arel_table["#{column}"]) if bitemporal_value[:through] }
301
+ end
302
+
303
+ def _except_#{column}
304
+ return self unless where_clause.send(:predicates).find { |node|
305
+ node.bitemporal_include?("\#{table.name}.#{column}")
306
+ }
307
+ _ignore_#{column}.tap { |relation|
308
+ relation.unscope_values.reject! { |query| query.equal_attribute_name("\#{table.name}.#{column}") }
309
+ }
310
+ end
311
+ STR
312
+
313
+ [
314
+ :lt, # column < datetime
315
+ :lteq, # column <= datetime
316
+ :gt, # column > datetime
317
+ :gteq # column >= datetime
318
+ ].each { |op|
319
+ module_eval <<-STR, __FILE__, __LINE__ + 1
320
+ def _#{column}_#{op}(datetime,**)
321
+ target_datetime = datetime&.in_time_zone || Time.current
322
+ bitemporal_rewhere_bind("#{column}", :#{op}, target_datetime)
323
+ .tap { |relation| break relation.bitemporal_rewhere_bind("#{column}", :#{op}, target_datetime, bitemporal_value[:through].arel_table) if bitemporal_value[:through] }
324
+ end
325
+ STR
326
+ }
327
+ }
328
+ end
329
+ end
330
+ end
331
+ using ActiveRecordRelationScope
332
+
333
+ %i(valid_from valid_to transaction_from transaction_to).each { |column|
334
+ scope :"ignore_#{column}", -> {
335
+ public_send(:"_ignore_#{column}")
336
+ }
337
+
338
+ scope :"except_#{column}", -> {
339
+ public_send(:"_except_#{column}")
340
+ }
341
+
342
+ [
343
+ :lt, # column < datetime
344
+ :lteq, # column <= datetime
345
+ :gt, # column > datetime
346
+ :gteq # column >= datetime
347
+ ].each { |op|
348
+ scope :"#{column}_#{op}", -> (datetime) {
349
+ public_send(:"_#{column}_#{op}", datetime)
350
+ }
351
+ }
352
+ }
353
+
354
+ # valid_from <= datetime && datetime < valid_to
355
+ scope :valid_at, -> (datetime) {
356
+ if ActiveRecord::Bitemporal.force_valid_datetime?
357
+ datetime = ActiveRecord::Bitemporal.valid_datetime
358
+ end
359
+ datetime = Time.current if datetime.nil?
360
+ valid_from_lteq(datetime).valid_to_gt(datetime).with_valid_datetime
361
+ }
362
+ scope :ignore_valid_datetime, -> {
363
+ ignore_valid_from.ignore_valid_to.without_valid_datetime
364
+ }
365
+ scope :except_valid_datetime, -> {
366
+ except_valid_from.except_valid_to.tap { |relation| relation.bitemporal_value.except! :with_valid_datetime }
367
+ }
368
+
369
+ # transaction_from <= datetime && datetime < transaction_to
370
+ scope :transaction_at, -> (datetime) {
371
+ if ActiveRecord::Bitemporal.force_transaction_datetime?
372
+ datetime = ActiveRecord::Bitemporal.transaction_datetime
373
+ end
374
+ datetime = Time.current if datetime.nil?
375
+ transaction_from_lteq(datetime).transaction_to_gt(datetime).with_transaction_datetime
376
+ }
377
+ scope :ignore_transaction_datetime, -> {
378
+ ignore_transaction_from.ignore_transaction_to.without_transaction_datetime
379
+ }
380
+ scope :except_transaction_datetime, -> {
381
+ except_transaction_from.except_transaction_to.tap { |relation| relation.bitemporal_value.except! :with_transaction_datetime }
382
+ }
383
+
384
+ scope :bitemporal_at, -> (datetime) {
385
+ datetime = Time.current if datetime.nil?
386
+ relation = self
387
+
388
+ if !ActiveRecord::Bitemporal.ignore_transaction_datetime?
389
+ relation = relation.transaction_at(ActiveRecord::Bitemporal.transaction_datetime || datetime)
390
+ end
391
+
392
+ if !ActiveRecord::Bitemporal.ignore_valid_datetime?
393
+ relation = relation.valid_at(ActiveRecord::Bitemporal.valid_datetime || datetime)
394
+ end
395
+
396
+ relation
397
+ }
398
+ scope :ignore_bitemporal_datetime, -> {
399
+ ignore_transaction_datetime.ignore_valid_datetime
400
+ }
401
+ scope :except_bitemporal_datetime, -> {
402
+ except_transaction_datetime.except_valid_datetime
403
+ }
404
+
405
+ scope :bitemporal_default_scope, -> {
406
+ datetime = Time.current
407
+ relation = self
408
+
409
+ if !ActiveRecord::Bitemporal.ignore_transaction_datetime?
410
+ if ActiveRecord::Bitemporal.transaction_datetime
411
+ transaction_datetime = ActiveRecord::Bitemporal.transaction_datetime
412
+ relation.bitemporal_value[:with_transaction_datetime] = :default_scope_with_transaction_datetime
413
+ else
414
+ relation.bitemporal_value[:with_transaction_datetime] = :default_scope
415
+ end
416
+
417
+ # Calling scope was slow, so don't call scope
418
+ relation.unscope_values += [
419
+ { where: "#{table.name}.transaction_from" },
420
+ { where: "#{table.name}.transaction_to" }
421
+ ]
422
+ relation = relation
423
+ ._transaction_from_lteq(transaction_datetime || datetime, without_ignore: true)
424
+ ._transaction_to_gt(transaction_datetime || datetime, without_ignore: true)
425
+ else
426
+ relation.tap { |relation| relation.without_transaction_datetime unless ActiveRecord::Bitemporal.transaction_datetime }
427
+ end
428
+
429
+ if !ActiveRecord::Bitemporal.ignore_valid_datetime?
430
+ if ActiveRecord::Bitemporal.valid_datetime
431
+ valid_datetime = ActiveRecord::Bitemporal.valid_datetime
432
+ relation.bitemporal_value[:with_valid_datetime] = :default_scope_with_valid_datetime
433
+ else
434
+ relation.bitemporal_value[:with_valid_datetime] = :default_scope
435
+ end
436
+
437
+ relation.unscope_values += [
438
+ { where: "#{table.name}.valid_from" },
439
+ { where: "#{table.name}.valid_to" }
440
+ ]
441
+ relation = relation
442
+ ._valid_from_lteq(valid_datetime || datetime, without_ignore: true)
443
+ ._valid_to_gt(valid_datetime || datetime, without_ignore: true)
444
+ else
445
+ relation.tap { |relation| relation.without_valid_datetime unless ActiveRecord::Bitemporal.valid_datetime }
446
+ end
447
+
448
+ relation
449
+ }
450
+
451
+ scope :except_bitemporal_default_scope, -> {
452
+ scope = all
453
+ scope = scope.except_valid_datetime if bitemporal_value[:with_valid_datetime] == :default_scope || bitemporal_value[:with_valid_datetime] == :default_scope_with_valid_datetime
454
+ scope = scope.except_transaction_datetime if bitemporal_value[:with_transaction_datetime] == :default_scope || bitemporal_value[:with_transaction_datetime] == :default_scope_with_transaction_datetime
455
+ scope
456
+ }
457
+
458
+ scope :within_deleted, -> {
459
+ ignore_transaction_datetime
460
+ }
461
+ scope :without_deleted, -> {
462
+ valid_at(Time.current)
463
+ }
464
+
465
+ scope :bitemporal_for, -> (id) {
466
+ where(bitemporal_id: id)
467
+ }
468
+
469
+ scope :valid_in, -> (from: nil, to: nil) {
470
+ ignore_valid_datetime
471
+ .tap { |relation| break relation.bitemporal_where_bind("valid_to", :gteq, from.in_time_zone.to_datetime) if from }
472
+ .tap { |relation| break relation.bitemporal_where_bind("valid_from", :lteq, to.in_time_zone.to_datetime) if to }
473
+ }
474
+ scope :valid_allin, -> (from: nil, to: nil) {
475
+ ignore_valid_datetime
476
+ .tap { |relation| break relation.bitemporal_where_bind("valid_from", :gteq, from.in_time_zone.to_datetime) if from }
477
+ .tap { |relation| break relation.bitemporal_where_bind("valid_to", :lteq, to.in_time_zone.to_datetime) if to }
478
+ }
479
+ end
480
+
481
+ module Extension
482
+ extend ActiveSupport::Concern
483
+
484
+ included do
485
+ scope :bitemporal_histories, -> (*ids) {
486
+ ignore_valid_datetime.bitemporal_for(*ids)
487
+ }
488
+ def self.bitemporal_most_future(id)
489
+ bitemporal_histories(id).order(valid_from: :asc).last
490
+ end
491
+ def self.bitemporal_most_past(id)
492
+ bitemporal_histories(id).order(valid_from: :asc).first
493
+ end
494
+ end
495
+ end
496
+
497
+ module Experimental
498
+ extend ActiveSupport::Concern
499
+ end
500
+ end
501
+ end
@@ -1,5 +1,7 @@
1
- module Activerecord
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
2
4
  module Bitemporal
3
- VERSION = "0.0.1"
5
+ VERSION = "1.0.0"
4
6
  end
5
7
  end