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 +4 -4
- data/CHANGELOG.md +74 -0
- data/README.md +1 -1
- data/lib/activerecord-bitemporal/bitemporal.rb +52 -83
- data/lib/activerecord-bitemporal/bitemporal_checker.rb +19 -0
- data/lib/activerecord-bitemporal/bitemporalize.rb +149 -0
- data/lib/activerecord-bitemporal/global_id.rb +33 -0
- data/lib/activerecord-bitemporal/scope.rb +24 -17
- data/lib/activerecord-bitemporal/version.rb +1 -1
- data/lib/activerecord-bitemporal/visualizer.rb +6 -4
- data/lib/activerecord-bitemporal.rb +5 -141
- metadata +37 -26
- data/.github/auto_assign.yml +0 -21
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/release.yml +0 -37
- data/.github/workflows/test.yml +0 -54
- data/.gitignore +0 -4
- data/Appraisals +0 -13
- data/CODE_OF_CONDUCT.jp.md +0 -86
- data/CODE_OF_CONDUCT.md +0 -133
- data/Gemfile +0 -9
- data/Rakefile +0 -8
- data/activerecord-bitemporal.gemspec +0 -35
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/compose.yml +0 -10
- data/gemfiles/rails_6.1.gemfile +0 -8
- data/gemfiles/rails_7.0.gemfile +0 -8
- data/gemfiles/rails_7.1.gemfile +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7676d93276c1c24acbb001b135c344b94d3b0b8084d2ef17423c238b5e06bfaf
|
|
4
|
+
data.tar.gz: f07cc0d7a716c99d3d85f352588309351b3ed408e6ddec5ad153096dd2310786
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
350
|
-
self
|
|
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
|
|
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
|
|
383
|
-
self
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
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 =
|
|
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
|
|
482
|
+
before_instance[valid_to_key] = target_datetime
|
|
520
483
|
if before_instance.valid_from_cannot_be_greater_equal_than_valid_to
|
|
521
|
-
|
|
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
|
|
527
|
-
after_instance
|
|
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
|
-
|
|
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).
|
|
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 '
|
|
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
|
|
550
|
-
after_instance
|
|
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.
|
|
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.
|
|
558
|
+
valid_from = record[record.valid_from_key] if record.force_update?
|
|
590
559
|
|
|
591
|
-
valid_to = record.
|
|
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(
|
|
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.
|
|
599
|
-
finder_class.ignore_valid_datetime.where(bitemporal_id: record.bitemporal_id).valid_from_gteq(target_datetime).order(
|
|
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(
|
|
90
|
-
valid_from = node_hash.dig(table_name,
|
|
91
|
-
valid_to = node_hash.dig(table_name,
|
|
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
|
-
|
|
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}.#{
|
|
262
|
-
.tap { |relation| relation.unscope!(where: bitemporal_value[:through].arel_table["#{
|
|
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}.#{
|
|
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}.#{
|
|
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("#{
|
|
285
|
-
.tap { |relation| break relation.bitemporal_rewhere_bind("#{
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
489
|
+
bitemporal_histories(id).order(valid_from_key => :asc).first
|
|
483
490
|
end
|
|
484
491
|
end
|
|
485
492
|
end
|