activerecord-bitemporal 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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