activerecord-bitemporal 5.3.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c660ad9554b7d5285a28f3efcd6a6c10e2b5e45b6922956e37f931ae807ba483
4
- data.tar.gz: 24646d6dc75344e11f613f70c7847639f8c86524ce3d1b178f4abb69dc60694f
3
+ metadata.gz: 7676d93276c1c24acbb001b135c344b94d3b0b8084d2ef17423c238b5e06bfaf
4
+ data.tar.gz: f07cc0d7a716c99d3d85f352588309351b3ed408e6ddec5ad153096dd2310786
5
5
  SHA512:
6
- metadata.gz: 39e0aea4e39482bcb929d0df586232413193f57a232ca7b2b928ca9aaef90d84fa88fc1edb925bcbff466864314beafa69e9caf36057ba7f4529019791d33698
7
- data.tar.gz: 84bc4e20042ffb77c2f317fcf6ce85b1b469edb338ec0a1346a9dd0008d1b01e49037503124778144521704ed97af904632bf91f424acc41448968d4beba0a5b
6
+ metadata.gz: 95b6ea9e934a5818d3fc3e0409faef14bde66aeafbd0a0a168a1649e3b88ed6a11a0e8174094f7527ae4ba47076449db17a17597b83587a8986815af458b6c19
7
+ data.tar.gz: ece5cc5e3a8846f6495b63b30eb35eb4648e95b712e582b72fa67dafd1cfe9af1384737a11cf272d06cf73ed9e9121a674a6e0ea90434edb77d1715603cf7ffe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,79 @@
1
1
  # Changelog
2
2
 
3
+ ## 6.1.0
4
+
5
+ ### Breaking Changed
6
+
7
+ ### Added
8
+
9
+ - [Support GlobalID integration #176](https://github.com/kufu/activerecord-bitemporal/pull/176)
10
+
11
+ ### Changed
12
+
13
+ - [Add explicit activesupport dependency #208](https://github.com/kufu/activerecord-bitemporal/pull/208)
14
+ - [Improve ValidDatetimeRangeError message with better grammar and context #207](https://github.com/kufu/activerecord-bitemporal/pull/207)
15
+ - [Delay execution of ActiveRecord::Base-related processing #189](https://github.com/kufu/activerecord-bitemporal/pull/189)
16
+
17
+ ### Deprecated
18
+
19
+ ### Removed
20
+
21
+ ### Fixed
22
+
23
+ ### Chores
24
+
25
+ - [Bump ruby/setup-ruby from 1.263.0 to 1.265.0 #223](https://github.com/kufu/activerecord-bitemporal/pull/223)
26
+ - [Update gemspec files to avoid using git #222](https://github.com/kufu/activerecord-bitemporal/pull/222)
27
+ - [Bump ruby/setup-ruby from 1.257.0 to 1.263.0 #221](https://github.com/kufu/activerecord-bitemporal/pull/221)
28
+ - [Add CodeSpell workflow for spell checking in pull requests #220](https://github.com/kufu/activerecord-bitemporal/pull/220)
29
+ - [Configure dependabot cooldown period to 3 days #219](https://github.com/kufu/activerecord-bitemporal/pull/219)
30
+ - [Bump ruby/setup-ruby from 1.255.0 to 1.257.0 #218](https://github.com/kufu/activerecord-bitemporal/pull/218)
31
+ - [Bump actions/checkout from 4.2.2 to 5.0.0 #215](https://github.com/kufu/activerecord-bitemporal/pull/215)
32
+ - [Bump ruby/setup-ruby from 1.254.0 to 1.255.0 #214](https://github.com/kufu/activerecord-bitemporal/pull/214)
33
+ - [Bump ruby/setup-ruby from 1.247.0 to 1.254.0 #213](https://github.com/kufu/activerecord-bitemporal/pull/213)
34
+ - [Bump ruby/setup-ruby from 1.247.0 to 1.253.0 #212](https://github.com/kufu/activerecord-bitemporal/pull/212)
35
+ - [Setup RuboCop #211](https://github.com/kufu/activerecord-bitemporal/pull/211)
36
+ - [Bump ruby/setup-ruby from 1.245.0 to 1.247.0 #210](https://github.com/kufu/activerecord-bitemporal/pull/210)
37
+ - [Using Trusted Publishing for RubyGems.org. #209](https://github.com/kufu/activerecord-bitemporal/pull/209)
38
+ - [Bump ruby/setup-ruby from 1.244.0 to 1.245.0 #206](https://github.com/kufu/activerecord-bitemporal/pull/206)
39
+
40
+ ## 6.0.0
41
+
42
+ ### Breaking Changed
43
+
44
+ - [Add Ruby 3.4 and remove Ruby 3.0 in CI #185](https://github.com/kufu/activerecord-bitemporal/pull/185)
45
+ - [Drop support Rails 6.1 #192](https://github.com/kufu/activerecord-bitemporal/pull/192)
46
+
47
+ ### Added
48
+
49
+ - [CI against Rails 7.2 #164](https://github.com/kufu/activerecord-bitemporal/pull/164)
50
+ - [Support custom column names for valid time in .bitemporalize #200](https://github.com/kufu/activerecord-bitemporal/pull/200)
51
+
52
+ ### Changed
53
+
54
+ ### Deprecated
55
+
56
+ ### Removed
57
+
58
+ ### Fixed
59
+
60
+ - [Prevent where clauses from ignored by `ignore_valid_datetime` #190](https://github.com/kufu/activerecord-bitemporal/pull/190)
61
+
62
+ ### Chores
63
+
64
+ - [Add a note to the README that PostgreSQL is required to run the tests. #188](https://github.com/kufu/activerecord-bitemporal/pull/188)
65
+ - [Remove specs for Rails 5.x #191](https://github.com/kufu/activerecord-bitemporal/pull/191)
66
+ - [Update auto assign member #193](https://github.com/kufu/activerecord-bitemporal/pull/193)
67
+ - [Pin GitHub Actions dependencies to specific commit hashes #194](https://github.com/kufu/activerecord-bitemporal/pull/194)
68
+ - [Bump ruby/setup-ruby from 1.227.0 to 1.229.0 #195](https://github.com/kufu/activerecord-bitemporal/pull/195)
69
+ - [Bump ruby/setup-ruby from 1.229.0 to 1.233.0 #197](https://github.com/kufu/activerecord-bitemporal/pull/197)
70
+ - [Bump ruby/setup-ruby from 1.233.0 to 1.235.0 #198](https://github.com/kufu/activerecord-bitemporal/pull/198)
71
+ - [Bump ruby/setup-ruby from 1.235.0 to 1.237.0 #199](https://github.com/kufu/activerecord-bitemporal/pull/199)
72
+ - [Bump ruby/setup-ruby from 1.237.0 to 1.238.0 #201](https://github.com/kufu/activerecord-bitemporal/pull/201)
73
+ - [Bump ruby/setup-ruby from 1.238.0 to 1.242.0 #202](https://github.com/kufu/activerecord-bitemporal/pull/202)
74
+ - [Bump ruby/setup-ruby from 1.242.0 to 1.244.0 #203](https://github.com/kufu/activerecord-bitemporal/pull/203)
75
+ - [Update auto assign member #204](https://github.com/kufu/activerecord-bitemporal/pull/204)
76
+
3
77
  ## 5.3.0
4
78
 
5
79
  ### Added
data/README.md CHANGED
@@ -711,7 +711,7 @@ Employee.where(bitemporal_id: employee.bitemporal_id)
711
711
 
712
712
  ## Development
713
713
 
714
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
714
+ After checking out the repo, run `bin/setup` to install dependencies. And start PostgreSQL on port 5432. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
715
715
 
716
716
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
717
717
 
@@ -1,20 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "activerecord-bitemporal/bitemporal_checker"
3
4
  module ActiveRecord
4
5
  module Bitemporal
5
- module BitemporalChecker
6
- refine ::Class do
7
- def bi_temporal_model?
8
- include?(ActiveRecord::Bitemporal)
9
- end
10
- end
11
-
12
- refine ::ActiveRecord::Relation do
13
- def bi_temporal_model?
14
- klass.include?(ActiveRecord::Bitemporal)
15
- end
16
- end
17
- end
18
6
  using BitemporalChecker
19
7
 
20
8
  module Optionable
@@ -313,7 +301,7 @@ module ActiveRecord
313
301
  def _create_record(attribute_names = self.attribute_names)
314
302
  bitemporal_assign_initialize_value(valid_datetime: self.valid_datetime)
315
303
 
316
- ActiveRecord::Bitemporal.valid_at!(self.valid_from) {
304
+ ActiveRecord::Bitemporal.valid_at!(self[valid_from_key]) {
317
305
  super()
318
306
  }
319
307
  end
@@ -346,8 +334,8 @@ module ActiveRecord
346
334
  # update 後に新しく生成したインスタンスのデータを移行する
347
335
  @_swapped_id_previously_was = swapped_id
348
336
  @_swapped_id = after_instance.swapped_id
349
- self.valid_from = after_instance.valid_from
350
- self.valid_to = after_instance.valid_to
337
+ self[valid_from_key] = after_instance[valid_from_key]
338
+ self[valid_to_key] = after_instance[valid_to_key]
351
339
  self.transaction_from = after_instance.transaction_from
352
340
  self.transaction_to = after_instance.transaction_to
353
341
 
@@ -373,14 +361,14 @@ module ActiveRecord
373
361
  # force_update の場合は削除時の状態の履歴を残さない
374
362
  unless force_update?
375
363
  # 削除時の状態を履歴レコードとして保存する
376
- duplicated_instance.valid_to = target_datetime
364
+ duplicated_instance[valid_to_key] = target_datetime
377
365
  duplicated_instance.transaction_from = operated_at
378
366
  duplicated_instance.save_without_bitemporal_callbacks!(validate: false)
379
367
  if @destroyed
380
368
  @_swapped_id_previously_was = swapped_id
381
369
  @_swapped_id = duplicated_instance.swapped_id
382
- self.valid_from = duplicated_instance.valid_from
383
- self.valid_to = duplicated_instance.valid_to
370
+ self[valid_from_key] = duplicated_instance[valid_from_key]
371
+ self[valid_to_key] = duplicated_instance[valid_to_key]
384
372
  self.transaction_from = duplicated_instance.transaction_from
385
373
  self.transaction_to = duplicated_instance.transaction_to
386
374
  end
@@ -393,6 +381,7 @@ module ActiveRecord
393
381
  rescue => e
394
382
  @destroyed = false
395
383
  @_association_destroy_exception = ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record: class=#{e.class}, message=#{e.message}", self)
384
+ @_association_destroy_exception.set_backtrace(e.backtrace)
396
385
  false
397
386
  end
398
387
 
@@ -411,55 +400,29 @@ module ActiveRecord
411
400
  module ::ActiveRecord::Persistence
412
401
  # MEMO: Must be override ActiveRecord::Persistence#reload
413
402
  alias_method :active_record_bitemporal_original_reload, :reload unless method_defined? :active_record_bitemporal_original_reload
414
- if Gem::Version.new("7.0.0.alpha") <= ActiveRecord.version
415
- def reload(options = nil)
416
- return active_record_bitemporal_original_reload(options) unless self.class.bi_temporal_model?
417
-
418
- self.class.connection.clear_query_cache
419
-
420
- fresh_object =
421
- ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
422
- if apply_scoping?(options)
423
- _find_record(options)
424
- else
425
- self.class.unscoped { self.class.bitemporal_default_scope.scoping { _find_record(options) } }
426
- end
427
- }
403
+ def reload(options = nil)
404
+ return active_record_bitemporal_original_reload(options) unless self.class.bi_temporal_model?
428
405
 
429
- @association_cache = fresh_object.instance_variable_get(:@association_cache)
430
- @attributes = fresh_object.instance_variable_get(:@attributes)
431
- @new_record = false
432
- @previously_new_record = false
433
- # NOTE: Hook to copying swapped_id
434
- @_swapped_id_previously_was = nil
435
- @_swapped_id = fresh_object.swapped_id
436
- @previously_force_updated = false
437
- self
438
- end
439
- else
440
- def reload(options = nil)
441
- return active_record_bitemporal_original_reload(options) unless self.class.bi_temporal_model?
442
-
443
- self.class.connection.clear_query_cache
444
-
445
- fresh_object =
446
- ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
447
- if options && options[:lock]
448
- self.class.unscoped { self.class.lock(options[:lock]).bitemporal_default_scope.find(id) }
449
- else
450
- self.class.unscoped { self.class.bitemporal_default_scope.find(id) }
451
- end
452
- }
406
+ self.class.connection.clear_query_cache
453
407
 
454
- @attributes = fresh_object.instance_variable_get(:@attributes)
455
- @new_record = false
456
- @previously_new_record = false
457
- # NOTE: Hook to copying swapped_id
458
- @_swapped_id_previously_was = nil
459
- @_swapped_id = fresh_object.swapped_id
460
- @previously_force_updated = false
461
- self
462
- end
408
+ fresh_object =
409
+ ActiveRecord::Bitemporal.with_bitemporal_option(**bitemporal_option) {
410
+ if apply_scoping?(options)
411
+ _find_record(options)
412
+ else
413
+ self.class.unscoped { self.class.bitemporal_default_scope.scoping { _find_record(options) } }
414
+ end
415
+ }
416
+
417
+ @association_cache = fresh_object.instance_variable_get(:@association_cache)
418
+ @attributes = fresh_object.instance_variable_get(:@attributes)
419
+ @new_record = false
420
+ @previously_new_record = false
421
+ # NOTE: Hook to copying swapped_id
422
+ @_swapped_id_previously_was = nil
423
+ @_swapped_id = fresh_object.swapped_id
424
+ @previously_force_updated = false
425
+ self
463
426
  end
464
427
  end
465
428
 
@@ -467,7 +430,7 @@ module ActiveRecord
467
430
 
468
431
  def bitemporal_assign_initialize_value(valid_datetime:, current_time: Time.current)
469
432
  # 自身の `valid_from` を設定
470
- self.valid_from = valid_datetime || current_time if self.valid_from == ActiveRecord::Bitemporal::DEFAULT_VALID_FROM
433
+ self[valid_from_key] = valid_datetime || current_time if self[valid_from_key] == ActiveRecord::Bitemporal::DEFAULT_VALID_FROM
471
434
 
472
435
  self.transaction_from = current_time if self.transaction_from == ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_FROM
473
436
 
@@ -485,7 +448,7 @@ module ActiveRecord
485
448
  def bitemporal_build_update_records(valid_datetime:, current_time: Time.current, force_update: false)
486
449
  target_datetime = valid_datetime || current_time
487
450
  # NOTE: force_update の場合は自身のレコードを取得するような時間を指定しておく
488
- target_datetime = valid_from_changed? ? valid_from_was : valid_from if force_update
451
+ target_datetime = attribute_changed?(valid_from_key) ? attribute_was(valid_from_key) : self[valid_from_key] if force_update
489
452
 
490
453
  # 対象基準日において有効なレコード
491
454
  # NOTE: 論理削除対象
@@ -516,26 +479,32 @@ module ActiveRecord
516
479
  current_valid_record.assign_transaction_to(current_time)
517
480
 
518
481
  # 以前の履歴データは valid_to を詰めて保存
519
- before_instance.valid_to = target_datetime
482
+ before_instance[valid_to_key] = target_datetime
520
483
  if before_instance.valid_from_cannot_be_greater_equal_than_valid_to
521
- raise ValidDatetimeRangeError.new("valid_from #{before_instance.valid_from} can't be greater equal than valid_to #{before_instance.valid_to}")
484
+ message = "#{valid_from_key} #{before_instance[valid_from_key]} can't be " \
485
+ "greater than or equal to #{valid_to_key} #{before_instance[valid_to_key]} " \
486
+ "for #{self.class} with bitemporal_id=#{bitemporal_id}"
487
+ raise ValidDatetimeRangeError.new(message)
522
488
  end
523
489
  before_instance.transaction_from = current_time
524
490
 
525
491
  # 以降の履歴データは valid_from と valid_to を調整して保存する
526
- after_instance.valid_from = target_datetime
527
- after_instance.valid_to = current_valid_record.valid_to
492
+ after_instance[valid_from_key] = target_datetime
493
+ after_instance[valid_to_key] = current_valid_record[valid_to_key]
528
494
  if after_instance.valid_from_cannot_be_greater_equal_than_valid_to
529
- raise ValidDatetimeRangeError.new("valid_from #{after_instance.valid_from} can't be greater equal than valid_to #{after_instance.valid_to}")
495
+ message = "#{valid_from_key} #{after_instance[valid_from_key]} can't be " \
496
+ "greater than or equal to #{valid_to_key} #{after_instance[valid_to_key]} " \
497
+ "for #{self.class} with bitemporal_id=#{bitemporal_id}"
498
+ raise ValidDatetimeRangeError.new(message)
530
499
  end
531
500
  after_instance.transaction_from = current_time
532
501
 
533
502
  # 有効なレコードがない場合
534
503
  else
535
504
  # 一番近い未来にある Instance を取ってきて、その valid_from を valid_to に入れる
536
- nearest_instance = self.class.where(bitemporal_id: bitemporal_id).valid_from_gt(target_datetime).ignore_valid_datetime.order(valid_from: :asc).first
505
+ nearest_instance = self.class.where(bitemporal_id: bitemporal_id).ignore_valid_datetime.valid_from_gt(target_datetime).order(valid_from_key => :asc).first
537
506
  if nearest_instance.nil?
538
- message = "Update failed: Couldn't find #{self.class} with 'bitemporal_id'=#{self.bitemporal_id} and 'valid_from' < #{target_datetime}"
507
+ message = "Update failed: Couldn't find #{self.class} with 'bitemporal_id'=#{self.bitemporal_id} and '#{valid_from_key}' > #{target_datetime}"
539
508
  raise ActiveRecord::RecordNotFound.new(message, self.class, "bitemporal_id", self.bitemporal_id)
540
509
  end
541
510
 
@@ -546,8 +515,8 @@ module ActiveRecord
546
515
  before_instance = nil
547
516
 
548
517
  # 以降の履歴データは valid_from と valid_to を調整して保存する
549
- after_instance.valid_from = target_datetime
550
- after_instance.valid_to = nearest_instance.valid_from
518
+ after_instance[valid_from_key] = target_datetime
519
+ after_instance[valid_to_key] = nearest_instance[valid_from_key]
551
520
  after_instance.transaction_from = current_time
552
521
  end
553
522
 
@@ -569,7 +538,7 @@ module ActiveRecord
569
538
 
570
539
  target_datetime = record.valid_datetime || Time.current
571
540
 
572
- valid_from = record.valid_from.yield_self { |valid_from|
541
+ valid_from = record[record.valid_from_key].yield_self { |valid_from|
573
542
  # NOTE: valid_from が初期値の場合は現在の時間を基準としてバリデーションする
574
543
  # valid_from が初期値の場合は Persistence#_create_record に Time.current が割り当てられる為
575
544
  # バリデーション時と生成時で若干時間がずれてしまうことには考慮する
@@ -586,17 +555,17 @@ module ActiveRecord
586
555
  }
587
556
 
588
557
  # MEMO: `force_update` does not refer to `valid_datetime`
589
- valid_from = record.valid_from if record.force_update?
558
+ valid_from = record[record.valid_from_key] if record.force_update?
590
559
 
591
- valid_to = record.valid_to.yield_self { |valid_to|
560
+ valid_to = record[record.valid_to_key].yield_self { |valid_to|
592
561
  # NOTE: `cover?` may give incorrect results, when the time zone is not UTC and `valid_from` is date type
593
562
  # Therefore, cast to type of `valid_from`
594
- record_valid_time = finder_class.type_for_attribute(:valid_from).cast(record.valid_datetime)
563
+ record_valid_time = finder_class.type_for_attribute(record.valid_from_key).cast(record.valid_datetime)
595
564
  # レコードを更新する時に valid_datetime が valid_from ~ valid_to の範囲外だった場合、
596
565
  # 一番近い未来の履歴レコードを参照して更新する
597
566
  # という仕様があるため、それを考慮して valid_to を設定する
598
- if (record_valid_time && (record.valid_from...record.valid_to).cover?(record_valid_time)) == false && (record.persisted?)
599
- finder_class.ignore_valid_datetime.where(bitemporal_id: record.bitemporal_id).valid_from_gteq(target_datetime).order(valid_from: :asc).first.valid_from
567
+ if (record_valid_time && (record[record.valid_from_key]...record[record.valid_to_key]).cover?(record_valid_time)) == false && (record.persisted?)
568
+ finder_class.ignore_valid_datetime.where(bitemporal_id: record.bitemporal_id).valid_from_gteq(target_datetime).order(record.valid_from_key => :asc).first[record.valid_from_key]
600
569
  else
601
570
  valid_to
602
571
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Bitemporal
5
+ module BitemporalChecker
6
+ refine ::Class do
7
+ def bi_temporal_model?
8
+ include?(ActiveRecord::Bitemporal)
9
+ end
10
+ end
11
+
12
+ refine ::ActiveRecord::Relation do
13
+ def bi_temporal_model?
14
+ klass.include?(ActiveRecord::Bitemporal)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord::Bitemporal::Bitemporalize
4
+ using Module.new {
5
+ refine ::ActiveRecord::Base do
6
+ class << ::ActiveRecord::Base
7
+ def prepend_relation_delegate_class(mod)
8
+ relation_delegate_class(ActiveRecord::Relation).prepend mod
9
+ relation_delegate_class(ActiveRecord::AssociationRelation).prepend mod
10
+ end
11
+ end
12
+ end
13
+ }
14
+
15
+ module ClassMethods
16
+ include ActiveRecord::Bitemporal::Relation::Finder
17
+
18
+ def bitemporal_id_key
19
+ 'bitemporal_id'
20
+ end
21
+
22
+ # Override ActiveRecord::Core::ClassMethods#cached_find_by_statement
23
+ # `.find_by` not use caching
24
+ def cached_find_by_statement(key, &block)
25
+ ActiveRecord::StatementCache.create(connection, &block)
26
+ end
27
+
28
+ def inherited(klass)
29
+ super
30
+ klass.prepend_relation_delegate_class ActiveRecord::Bitemporal::Relation
31
+ klass.relation_delegate_class(ActiveRecord::Associations::CollectionProxy).prepend ActiveRecord::Bitemporal::CollectionProxy
32
+ if relation_delegate_class(ActiveRecord::Relation).ancestors.include? ActiveRecord::Bitemporal::Relation::MergeWithExceptBitemporalDefaultScope
33
+ klass.relation_delegate_class(ActiveRecord::Relation).prepend ActiveRecord::Bitemporal::Relation::MergeWithExceptBitemporalDefaultScope
34
+ end
35
+ end
36
+ end
37
+
38
+ module InstanceMethods
39
+ include ActiveRecord::Bitemporal::Persistence
40
+
41
+ def swap_id!(without_clear_changes_information: false)
42
+ @_swapped_id_previously_was = nil
43
+ @_swapped_id = self.id
44
+ self.id = self.send(bitemporal_id_key)
45
+ clear_attribute_changes([:id]) unless without_clear_changes_information
46
+ end
47
+
48
+ def swapped_id
49
+ @_swapped_id || self.id
50
+ end
51
+
52
+ def swapped_id_previously_was
53
+ @_swapped_id_previously_was
54
+ end
55
+
56
+ def bitemporal_id_key
57
+ self.class.bitemporal_id_key
58
+ end
59
+
60
+ def bitemporal_ignore_update_columns
61
+ []
62
+ end
63
+
64
+ def id_in_database
65
+ swapped_id.presence || super
66
+ end
67
+
68
+ def previously_force_updated?
69
+ @previously_force_updated
70
+ end
71
+
72
+ def valid_from_cannot_be_greater_equal_than_valid_to
73
+ if self[valid_from_key] && self[valid_to_key] && self[valid_from_key] >= self[valid_to_key]
74
+ errors.add(valid_from_key, "can't be greater equal than #{valid_to_key}")
75
+ end
76
+ end
77
+
78
+ def transaction_from_cannot_be_greater_equal_than_transaction_to
79
+ if transaction_from && transaction_to && transaction_from >= transaction_to
80
+ errors.add(:transaction_from, "can't be greater equal than transaction_to")
81
+ end
82
+ end
83
+ end
84
+
85
+ def bitemporalize(
86
+ enable_strict_by_validates_bitemporal_id: false,
87
+ enable_default_scope: true,
88
+ enable_merge_with_except_bitemporal_default_scope: false,
89
+ valid_from_key: :valid_from,
90
+ valid_to_key: :valid_to
91
+ )
92
+ return if ancestors.include? InstanceMethods
93
+
94
+ extend ClassMethods
95
+ include InstanceMethods
96
+ include ActiveRecord::Bitemporal
97
+ include ActiveRecord::Bitemporal::Scope
98
+ include ActiveRecord::Bitemporal::Callbacks
99
+ prepend ActiveRecord::Bitemporal::GlobalID if defined?(::GlobalID)
100
+
101
+ if enable_merge_with_except_bitemporal_default_scope
102
+ relation_delegate_class(ActiveRecord::Relation).prepend ActiveRecord::Bitemporal::Relation::MergeWithExceptBitemporalDefaultScope
103
+ end
104
+
105
+ if enable_default_scope
106
+ default_scope {
107
+ bitemporal_default_scope
108
+ }
109
+ end
110
+
111
+ after_create do
112
+ # MEMO: #update_columns is not call #_update_row (and validations, callbacks)
113
+ update_columns(bitemporal_id_key => swapped_id) unless send(bitemporal_id_key)
114
+ swap_id!(without_clear_changes_information: true)
115
+ @previously_force_updated = false
116
+ end
117
+
118
+ after_find do
119
+ self.swap_id! if self.send(bitemporal_id_key).present?
120
+ @previously_force_updated = false
121
+ end
122
+
123
+ self.class_attribute :valid_from_key, :valid_to_key, instance_writer: false
124
+ self.valid_from_key = valid_from_key.to_s
125
+ self.valid_to_key = valid_to_key.to_s
126
+ attribute valid_from_key, default: ActiveRecord::Bitemporal::DEFAULT_VALID_FROM
127
+ attribute valid_to_key, default: ActiveRecord::Bitemporal::DEFAULT_VALID_TO
128
+ attribute :transaction_from, default: ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_FROM
129
+ attribute :transaction_to, default: ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO
130
+
131
+ # Callback hook to `validates :xxx, uniqueness: true`
132
+ const_set(:UniquenessValidator, Class.new(ActiveRecord::Validations::UniquenessValidator) {
133
+ prepend ActiveRecord::Bitemporal::Uniqueness
134
+ })
135
+
136
+ # validations
137
+ validates valid_from_key, presence: true
138
+ validates valid_to_key, presence: true
139
+ validates :transaction_from, presence: true
140
+ validates :transaction_to, presence: true
141
+ validate :valid_from_cannot_be_greater_equal_than_valid_to
142
+ validate :transaction_from_cannot_be_greater_equal_than_transaction_to
143
+
144
+ validates bitemporal_id_key, uniqueness: true, allow_nil: true, strict: enable_strict_by_validates_bitemporal_id
145
+
146
+ prepend_relation_delegate_class ActiveRecord::Bitemporal::Relation
147
+ relation_delegate_class(ActiveRecord::Associations::CollectionProxy).prepend ActiveRecord::Bitemporal::CollectionProxy
148
+ end
149
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "globalid"
5
+ rescue LoadError
6
+ # If GlobalID is not available, we skip the GlobalID integration.
7
+ return
8
+ end
9
+
10
+ module ActiveRecord
11
+ module Bitemporal
12
+ module GlobalID
13
+ include ::GlobalID::Identification
14
+
15
+ def to_global_id(options = {})
16
+ super(options.merge(app: "bitemporal"))
17
+ end
18
+ alias to_gid to_global_id
19
+
20
+ class BitemporalLocator < ::GlobalID::Locator::BaseLocator
21
+ private
22
+
23
+ # @override https://github.com/rails/globalid/blob/v1.2.1/lib/global_id/locator.rb#L203
24
+ def primary_key(model_class)
25
+ model_class.respond_to?(:bitemporal_id_key) ? model_class.bitemporal_id_key : :id
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ # BiTemporal Data Model requires default scope, so `UnscopedLocator` cannot be used.
33
+ GlobalID::Locator.use :bitemporal, ActiveRecord::Bitemporal::GlobalID::BitemporalLocator.new
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "activerecord-bitemporal/bitemporal_checker"
4
+
3
5
  module ActiveRecord::Bitemporal
4
6
  module NodeBitemporalInclude
5
7
  refine String do
@@ -86,9 +88,9 @@ module ActiveRecord::Bitemporal
86
88
 
87
89
  refine Relation do
88
90
  def bitemporal_clause(table_name = klass.table_name)
89
- node_hash = where_clause.bitemporal_query_hash("valid_from", "valid_to", "transaction_from", "transaction_to")
90
- valid_from = node_hash.dig(table_name, "valid_from", 1)
91
- valid_to = node_hash.dig(table_name, "valid_to", 1)
91
+ node_hash = where_clause.bitemporal_query_hash(valid_from_key, valid_to_key, "transaction_from", "transaction_to")
92
+ valid_from = node_hash.dig(table_name, valid_from_key, 1)
93
+ valid_to = node_hash.dig(table_name, valid_to_key, 1)
92
94
  transaction_from = node_hash.dig(table_name, "transaction_from", 1)
93
95
  transaction_to = node_hash.dig(table_name, "transaction_to", 1)
94
96
  {
@@ -255,19 +257,24 @@ module ActiveRecord::Bitemporal
255
257
  rewhere(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
256
258
  end
257
259
 
258
- %i(valid_from valid_to transaction_from transaction_to).each { |column|
260
+ [
261
+ [:valid_from, "\#{valid_from_key}"], # Evaluate `valid_from/to_key` at runtime using `\#`
262
+ [:valid_to, "\#{valid_to_key}"],
263
+ [:transaction_from, "transaction_from"],
264
+ [:transaction_to, "transaction_to"]
265
+ ].each { |column, column_name|
259
266
  module_eval <<-STR, __FILE__, __LINE__ + 1
260
267
  def _ignore_#{column}
261
- unscope(where: :"\#{table.name}.#{column}")
262
- .tap { |relation| relation.unscope!(where: bitemporal_value[:through].arel_table["#{column}"]) if bitemporal_value[:through] }
268
+ unscope(where: :"\#{table.name}.#{column_name}")
269
+ .tap { |relation| relation.unscope!(where: bitemporal_value[:through].arel_table["#{column_name}"]) if bitemporal_value[:through] }
263
270
  end
264
271
 
265
272
  def _except_#{column}
266
273
  return self unless where_clause.send(:predicates).find { |node|
267
- node.bitemporal_include?("\#{table.name}.#{column}")
274
+ node.bitemporal_include?("\#{table.name}.#{column_name}")
268
275
  }
269
276
  _ignore_#{column}.tap { |relation|
270
- relation.unscope_values.reject! { |query| query.equal_attribute_name("\#{table.name}.#{column}") }
277
+ relation.unscope_values.reject! { |query| query.equal_attribute_name("\#{table.name}.#{column_name}") }
271
278
  }
272
279
  end
273
280
  STR
@@ -281,8 +288,8 @@ module ActiveRecord::Bitemporal
281
288
  module_eval <<-STR, __FILE__, __LINE__ + 1
282
289
  def _#{column}_#{op}(datetime)
283
290
  target_datetime = datetime&.in_time_zone || Time.current
284
- bitemporal_rewhere_bind("#{column}", :#{op}, target_datetime)
285
- .tap { |relation| break relation.bitemporal_rewhere_bind("#{column}", :#{op}, target_datetime, bitemporal_value[:through].arel_table) if bitemporal_value[:through] }
291
+ bitemporal_rewhere_bind("#{column_name}", :#{op}, target_datetime)
292
+ .tap { |relation| break relation.bitemporal_rewhere_bind("#{column_name}", :#{op}, target_datetime, bitemporal_value[:through].arel_table) if bitemporal_value[:through] }
286
293
  end
287
294
  STR
288
295
  }
@@ -428,17 +435,17 @@ module ActiveRecord::Bitemporal
428
435
  # beginless range
429
436
  if begin_
430
437
  # from < valid_to
431
- relation = relation.bitemporal_where_bind("valid_to", :gt, begin_.in_time_zone.to_datetime)
438
+ relation = relation.bitemporal_where_bind(valid_to_key, :gt, begin_.in_time_zone.to_datetime)
432
439
  end
433
440
 
434
441
  # endless range
435
442
  if end_
436
443
  if range.exclude_end?
437
444
  # valid_from < to
438
- relation = relation.bitemporal_where_bind("valid_from", :lt, end_.in_time_zone.to_datetime)
445
+ relation = relation.bitemporal_where_bind(valid_from_key, :lt, end_.in_time_zone.to_datetime)
439
446
  else
440
447
  # valid_from <= to
441
- relation = relation.bitemporal_where_bind("valid_from", :lteq, end_.in_time_zone.to_datetime)
448
+ relation = relation.bitemporal_where_bind(valid_from_key, :lteq, end_.in_time_zone.to_datetime)
442
449
  end
443
450
  end
444
451
 
@@ -453,14 +460,14 @@ module ActiveRecord::Bitemporal
453
460
  begin_, end_ = range.begin, range.end
454
461
 
455
462
  if begin_
456
- relation = relation.bitemporal_where_bind("valid_from", :gteq, begin_.in_time_zone.to_datetime)
463
+ relation = relation.bitemporal_where_bind(valid_from_key, :gteq, begin_.in_time_zone.to_datetime)
457
464
  end
458
465
 
459
466
  if end_
460
467
  if range.exclude_end?
461
468
  raise 'Range with excluding end is not supported'
462
469
  else
463
- relation = relation.bitemporal_where_bind("valid_to", :lteq, end_.in_time_zone.to_datetime)
470
+ relation = relation.bitemporal_where_bind(valid_to_key, :lteq, end_.in_time_zone.to_datetime)
464
471
  end
465
472
  end
466
473
 
@@ -476,10 +483,10 @@ module ActiveRecord::Bitemporal
476
483
  ignore_valid_datetime.bitemporal_for(*ids)
477
484
  }
478
485
  def self.bitemporal_most_future(id)
479
- bitemporal_histories(id).order(valid_from: :asc).last
486
+ bitemporal_histories(id).order(valid_from_key => :asc).last
480
487
  end
481
488
  def self.bitemporal_most_past(id)
482
- bitemporal_histories(id).order(valid_from: :asc).first
489
+ bitemporal_histories(id).order(valid_from_key => :asc).first
483
490
  end
484
491
  end
485
492
  end