activerecord-bitemporal 1.0.0 → 2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -25
- data/README.md +1 -1
- data/activerecord-bitemporal.gemspec +2 -2
- data/lib/activerecord-bitemporal/scope.rb +77 -14
- data/lib/activerecord-bitemporal/version.rb +1 -1
- data/lib/activerecord-bitemporal/visualizer.rb +158 -0
- data/lib/activerecord-bitemporal.rb +1 -0
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 712d3f7bf1cb73ff98bbe7f5ca15f860c62096e419b89d44e6a419ba865eec38
|
4
|
+
data.tar.gz: 4071219bd39db37fc51dee4d1689f722c0a8c2ee6eb16120373d21ecf234def8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb0f10288f109171dd11aed5221fe0297224c6d798278ba60c18cad092cf4b757d2c5aff9463c9a9fafe0f1556f1d71d47d4d89d435f96f8debdda51883c0e42
|
7
|
+
data.tar.gz: 588eb1741174be2031b8e326fe34da93efebda700ad37bb26a260250a3e25d6b1ecb3ffc1964a7fb99c8ea4035d4a660e59ec617d5732320f944467a3d71b202
|
data/CHANGELOG.md
CHANGED
@@ -1,39 +1,37 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
##
|
3
|
+
## 2.0.0
|
4
4
|
|
5
|
-
### Breaking
|
6
|
-
|
7
|
-
- [#15](https://github.com/kufu/activerecord-bitemporal/pull/15) - `validates :bitemporal_id, uniqueness: true` is no raise by default.
|
8
|
-
- [#19](https://github.com/kufu/activerecord-bitemporal/pull/19) - Fix create history records after logical destroy in #destroy.
|
5
|
+
### Breaking Changed
|
6
|
+
- [[Proposal] Changed valid_in to exclude valid_from = to and valid_to = from. by osyo-manga · Pull Request #95](https://github.com/kufu/activerecord-bitemporal/pull/95)
|
9
7
|
|
10
8
|
### Added
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
- `.valid_in(from: from, to: to)`
|
15
|
-
- `.valid_allin(from: from, to: to)`
|
16
|
-
- `.bitemporal_histories_by(id)`
|
17
|
-
- `.bitemporal_most_future(id)`
|
18
|
-
- `.bitemporal_most_past(id)`
|
19
|
-
- [#15](https://github.com/kufu/activerecord-bitemporal/pull/15) - Added `.bitemporalize`. Use `.bitemporalize` instead of `include ActiveRecord::Bitemporal`.
|
20
|
-
- [#15](https://github.com/kufu/activerecord-bitemporal/pull/15) - Added `.bitemporalize` options.
|
10
|
+
### Changed
|
11
|
+
- [[Proposal] Add range argument to .valid_allin. by Dooor · Pull Request #98](https://github.com/kufu/activerecord-bitemporal/pull/98)
|
21
12
|
|
22
|
-
|
23
|
-
| --- | --- | --- |
|
24
|
-
| `enable_strict_by_validates_bitemporal_id` | raised with `validates :bitemporal_id, uniqueness: true` if `true` | false |
|
13
|
+
### Deprecated
|
25
14
|
|
15
|
+
### Removed
|
26
16
|
|
27
17
|
### Fixed
|
18
|
+
- [Fix JOIN query does not have valid_from / valid_to when using .or. by osyo-manga · Pull Request #99](https://github.com/kufu/activerecord-bitemporal/pull/99)
|
19
|
+
- [Fix typo in README.md by Naoya9922 · Pull Request #101](https://github.com/kufu/activerecord-bitemporal/pull/101)
|
20
|
+
|
21
|
+
## 1.1.0
|
22
|
+
|
23
|
+
### Added
|
28
24
|
|
29
|
-
- [#
|
30
|
-
|
31
|
-
|
32
|
-
- [#21](https://github.com/kufu/activerecord-bitemporal/pull/21) - Fixed bug in multi thread with `#update`.
|
33
|
-
- [#24](https://github.com/kufu/activerecord-bitemporal/pull/24) [#25](https://github.com/kufu/activerecord-bitemporal/pull/25) - Fixed bug. Does not respect table alias on join clause.
|
34
|
-
- [#27](https://github.com/kufu/activerecord-bitemporal/pull/27) - Fixed a bug that `swapped_id` doesn't change after `#reload`.
|
35
|
-
- [#28](https://github.com/kufu/activerecord-bitemporal/pull/28) - Fix the bug that `valid_from == valid_to` record is generated.
|
25
|
+
- [Add bitemporal data structure visualizer by wata727 · Pull Request #94](https://github.com/kufu/activerecord-bitemporal/pull/94)
|
26
|
+
|
27
|
+
### Changed
|
36
28
|
|
37
29
|
### Deprecated
|
38
30
|
|
39
|
-
|
31
|
+
### Removed
|
32
|
+
|
33
|
+
### Fixed
|
34
|
+
|
35
|
+
## 1.0.0
|
36
|
+
|
37
|
+
First stable release
|
data/README.md
CHANGED
@@ -195,7 +195,7 @@ end
|
|
195
195
|
| `valid_from` | `datetime` | 有効時間の開始時刻 |
|
196
196
|
| `valid_to` | `datetime` | 有効時間の終了時刻 |
|
197
197
|
| `transaction_from` | `datetime` | システム時間の開始時刻 |
|
198
|
-
| `transaction_to` | `datetime` |
|
198
|
+
| `transaction_to` | `datetime` | システム時間の終了時刻 |
|
199
199
|
|
200
200
|
また、モデルクラスでは `ActiveRecord::Bitemporal` を `include` をする必要があります。
|
201
201
|
|
@@ -7,8 +7,8 @@ require "activerecord-bitemporal/version"
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = "activerecord-bitemporal"
|
9
9
|
spec.version = ActiveRecord::Bitemporal::VERSION
|
10
|
-
spec.authors = ["
|
11
|
-
spec.email = ["
|
10
|
+
spec.authors = ["SmartHR"]
|
11
|
+
spec.email = ["oss@smarthr.co.jp"]
|
12
12
|
|
13
13
|
spec.summary = "BiTemporal Data Model for ActiveRecord"
|
14
14
|
spec.description = %q{Enable ActiveRecord models to be handled as BiTemporal Data Model.}
|
@@ -47,7 +47,7 @@ module ActiveRecord::Bitemporal
|
|
47
47
|
end
|
48
48
|
}
|
49
49
|
|
50
|
-
def
|
50
|
+
def each_operatable_node_6_0(nodes = predicates, &block)
|
51
51
|
if block
|
52
52
|
each_operatable_node(nodes).each(&block)
|
53
53
|
else
|
@@ -56,11 +56,6 @@ module ActiveRecord::Bitemporal
|
|
56
56
|
case node
|
57
57
|
when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
|
58
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
59
|
when Arel::Nodes::Grouping
|
65
60
|
each_operatable_node(node.expr) { |node| y << node }
|
66
61
|
end
|
@@ -69,6 +64,36 @@ module ActiveRecord::Bitemporal
|
|
69
64
|
end
|
70
65
|
end
|
71
66
|
|
67
|
+
def each_operatable_node_6_1(nodes = predicates, &block)
|
68
|
+
if block
|
69
|
+
each_operatable_node_6_1(nodes).each(&block)
|
70
|
+
else
|
71
|
+
Enumerator.new { |y|
|
72
|
+
Array(nodes).each { |node|
|
73
|
+
case node
|
74
|
+
when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
|
75
|
+
y << node if node && node.left.respond_to?(:relation)
|
76
|
+
when Arel::Nodes::And
|
77
|
+
each_operatable_node_6_1(node.children) { |node| y << node }
|
78
|
+
when Arel::Nodes::Binary
|
79
|
+
each_operatable_node_6_1(node.left) { |node| y << node }
|
80
|
+
each_operatable_node_6_1(node.right) { |node| y << node }
|
81
|
+
when Arel::Nodes::Unary
|
82
|
+
each_operatable_node_6_1(node.expr) { |node| y << node }
|
83
|
+
end
|
84
|
+
}
|
85
|
+
}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def each_operatable_node(nodes = predicates, &block)
|
90
|
+
if Gem::Version.new("6.1.0") <= ActiveRecord.version
|
91
|
+
each_operatable_node_6_1(nodes, &block)
|
92
|
+
else
|
93
|
+
each_operatable_node_6_0(nodes, &block)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
72
97
|
def bitemporal_query_hash(*names)
|
73
98
|
each_operatable_node
|
74
99
|
.select { |node| names.include? node.left.name.to_s }
|
@@ -466,15 +491,53 @@ module ActiveRecord::Bitemporal
|
|
466
491
|
where(bitemporal_id: id)
|
467
492
|
}
|
468
493
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
494
|
+
# from < valid_to AND valid_from < to
|
495
|
+
scope :valid_in, -> (range = nil, from: nil, to: nil) {
|
496
|
+
return valid_in(from...to) if range.nil?
|
497
|
+
|
498
|
+
relation = ignore_valid_datetime
|
499
|
+
begin_, end_ = range.begin, range.end
|
500
|
+
|
501
|
+
# beginless range
|
502
|
+
if begin_
|
503
|
+
# from < valid_to
|
504
|
+
relation = relation.bitemporal_where_bind("valid_to", :gt, begin_.in_time_zone.to_datetime)
|
505
|
+
end
|
506
|
+
|
507
|
+
# endless range
|
508
|
+
if end_
|
509
|
+
if range.exclude_end?
|
510
|
+
# valid_from < to
|
511
|
+
relation = relation.bitemporal_where_bind("valid_from", :lt, end_.in_time_zone.to_datetime)
|
512
|
+
else
|
513
|
+
# valid_from <= to
|
514
|
+
relation = relation.bitemporal_where_bind("valid_from", :lteq, end_.in_time_zone.to_datetime)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
relation
|
473
519
|
}
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
520
|
+
|
521
|
+
# from <= valid_from AND valid_to <= to
|
522
|
+
scope :valid_allin, -> (range = nil, from: nil, to: nil) {
|
523
|
+
return valid_allin(from..to) if range.nil?
|
524
|
+
|
525
|
+
relation = ignore_valid_datetime
|
526
|
+
begin_, end_ = range.begin, range.end
|
527
|
+
|
528
|
+
if begin_
|
529
|
+
relation = relation.bitemporal_where_bind("valid_from", :gteq, begin_.in_time_zone.to_datetime)
|
530
|
+
end
|
531
|
+
|
532
|
+
if end_
|
533
|
+
if range.exclude_end?
|
534
|
+
raise 'Range with excluding end is not supported'
|
535
|
+
else
|
536
|
+
relation = relation.bitemporal_where_bind("valid_to", :lteq, end_.in_time_zone.to_datetime)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
relation
|
478
541
|
}
|
479
542
|
end
|
480
543
|
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord::Bitemporal
|
4
|
+
module Visualizer
|
5
|
+
# Figure is a two-dimensional array holding plotted lines and columns
|
6
|
+
class Figure < Array
|
7
|
+
def print(str, line: 0, column: 0)
|
8
|
+
self[line] ||= []
|
9
|
+
str.each_char.with_index(column) do |c, i|
|
10
|
+
# The `#` represents a zero-length rectangle and should not be overwritten with lines
|
11
|
+
next if self[line][i] == '#' && (c == '+' || c == '|' || c == '-')
|
12
|
+
|
13
|
+
self[line][i] = c
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
map { |l| l&.map { |c| c || ' ' }&.join }.join("\n")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module_function
|
23
|
+
|
24
|
+
def visualize(record, height: 10, width: 40, highlight: true)
|
25
|
+
histories = record.class.ignore_bitemporal_datetime.bitemporal_for(record).order(:transaction_from, :valid_from)
|
26
|
+
|
27
|
+
if highlight
|
28
|
+
visualize_records(histories, [record], height: height, width: width)
|
29
|
+
else
|
30
|
+
visualize_records(histories, height: height, width: width)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# e.g. visualize_records(ActiveRecord::Relation, ActiveRecord::Relation)
|
35
|
+
def visualize_records(*relations, height: 10, width: 40)
|
36
|
+
raise 'More than 3 relations are not supported' if relations.size >= 3
|
37
|
+
records = relations.flatten
|
38
|
+
|
39
|
+
valid_times = (records.map(&:valid_from) + records.map(&:valid_to)).sort.uniq
|
40
|
+
transaction_times = (records.map(&:transaction_from) + records.map(&:transaction_to)).sort.uniq
|
41
|
+
|
42
|
+
time_length = Time.zone.now.strftime('%F %T.%3N').length
|
43
|
+
|
44
|
+
columns = compute_positions(valid_times, length: width, left_margin: time_length + 1, outlier: ActiveRecord::Bitemporal::DEFAULT_VALID_TO)
|
45
|
+
lines = compute_positions(transaction_times, length: height, outlier: ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO)
|
46
|
+
|
47
|
+
headers = Figure.new
|
48
|
+
valid_times.each_with_object([]).with_index do |(valid_time, prev_valid_times), line|
|
49
|
+
prev_valid_times.each do |valid_time|
|
50
|
+
headers.print('|', line: line, column: columns[valid_time])
|
51
|
+
end
|
52
|
+
headers.print("| #{valid_time.strftime('%F %T.%3N')}", line: line, column: columns[valid_time])
|
53
|
+
prev_valid_times << valid_time
|
54
|
+
end
|
55
|
+
|
56
|
+
body = Figure.new
|
57
|
+
relations.each.with_index do |relation, idx|
|
58
|
+
filler = idx == 0 ? ' ' : '*'
|
59
|
+
|
60
|
+
relation.each do |record|
|
61
|
+
line = lines[record.transaction_from]
|
62
|
+
column = columns[record.valid_from]
|
63
|
+
|
64
|
+
width = columns[record.valid_to] - columns[record.valid_from] - 1
|
65
|
+
height = lines[record.transaction_to] - lines[record.transaction_from] - 1
|
66
|
+
|
67
|
+
body.print("#{record.transaction_from.strftime('%F %T.%3N')} ", line: line)
|
68
|
+
if width > 0
|
69
|
+
if height > 0
|
70
|
+
body.print('+' + '-' * width + '+', line: line, column: column)
|
71
|
+
else
|
72
|
+
body.print('|' + '#' * width + '|', line: line, column: column)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
body.print('#', line: line, column: column)
|
76
|
+
end
|
77
|
+
|
78
|
+
1.upto(height) do |i|
|
79
|
+
if width > 0
|
80
|
+
body.print('|' + filler * width + '|', line: line + i, column: column)
|
81
|
+
else
|
82
|
+
body.print('#', line: line + i, column: column)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
body.print("#{record.transaction_to.strftime('%F %T.%3N')} ", line: line + height + 1)
|
87
|
+
if width > 0
|
88
|
+
body.print('+' + '-' * width + '+', line: line + height + 1, column: column)
|
89
|
+
else
|
90
|
+
body.print('#', line: line + height + 1, column: column)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
"#{headers.to_s}\n#{body.to_s}"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Compute a dictionary of where each time should be plotted.
|
99
|
+
# The position is normalized to the actual length of time.
|
100
|
+
#
|
101
|
+
# Example:
|
102
|
+
#
|
103
|
+
# t1 t2 t3 t4
|
104
|
+
# |------|------|-----------------|
|
105
|
+
#
|
106
|
+
# f(t1, t2, t3, t4) -> { t1 => 0, t2 => 2, t3 => 4, t4 => 10 }
|
107
|
+
#
|
108
|
+
def compute_positions(times, length:, left_margin: 0, outlier: nil)
|
109
|
+
lengths_from_beginning = compute_lengths_from_beginning(times, outlier: outlier)
|
110
|
+
# times must be sorted in ascending order. This is caller's responsibility.
|
111
|
+
# In that case, the last of lengths_from_beginning is equal to the total length.
|
112
|
+
total = lengths_from_beginning.values.last
|
113
|
+
|
114
|
+
times.each_with_object({}) do |time, ret|
|
115
|
+
prev = ret.values.last
|
116
|
+
pos = (lengths_from_beginning[time] / total * length).to_i + left_margin
|
117
|
+
|
118
|
+
if prev
|
119
|
+
# If the difference of times is too short, a position that have already been plotted may be computed.
|
120
|
+
# But we still want to plot the time, so allocate the required number to plot the smallest area.
|
121
|
+
if pos <= prev
|
122
|
+
# | -> |*|
|
123
|
+
# ^^ 2 columns
|
124
|
+
pos = prev + 2
|
125
|
+
elsif pos == prev + 1
|
126
|
+
# || -> |*|
|
127
|
+
# ^ 1 column
|
128
|
+
pos += 1
|
129
|
+
end
|
130
|
+
end
|
131
|
+
ret[time] = pos
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Example:
|
136
|
+
#
|
137
|
+
# t1 t2 t3 t4
|
138
|
+
# |-----------|-----------|-----------|
|
139
|
+
# <-----------> l1
|
140
|
+
# <-----------------------> l2
|
141
|
+
# <-----------------------------------> l3
|
142
|
+
#
|
143
|
+
# f([t1, t2, t3, t4]) -> { t1 => 0, t2 => l1, t3 => l2, t4 => l3 }
|
144
|
+
#
|
145
|
+
def compute_lengths_from_beginning(times, outlier: nil)
|
146
|
+
times.each_with_object({}) do |time, ret|
|
147
|
+
ret[time] = if time == outlier && times.size > 2
|
148
|
+
# If it contains an extremely large value such as 9999-12-31,
|
149
|
+
# that point will have a large effect on the visualization,
|
150
|
+
# so adjust the length so that it is half of the whole.
|
151
|
+
ret.values.last * 2
|
152
|
+
else
|
153
|
+
time - times.min
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -6,6 +6,7 @@ require "activerecord-bitemporal/bitemporal"
|
|
6
6
|
require "activerecord-bitemporal/scope"
|
7
7
|
require "activerecord-bitemporal/patches"
|
8
8
|
require "activerecord-bitemporal/version"
|
9
|
+
require "activerecord-bitemporal/visualizer"
|
9
10
|
|
10
11
|
module ActiveRecord::Bitemporal
|
11
12
|
DEFAULT_VALID_FROM = Time.utc(1900, 12, 31).in_time_zone.freeze
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-bitemporal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- SmartHR
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -138,7 +138,7 @@ dependencies:
|
|
138
138
|
version: '0'
|
139
139
|
description: Enable ActiveRecord models to be handled as BiTemporal Data Model.
|
140
140
|
email:
|
141
|
-
-
|
141
|
+
- oss@smarthr.co.jp
|
142
142
|
executables: []
|
143
143
|
extensions: []
|
144
144
|
extra_rdoc_files: []
|
@@ -168,6 +168,7 @@ files:
|
|
168
168
|
- lib/activerecord-bitemporal/patches.rb
|
169
169
|
- lib/activerecord-bitemporal/scope.rb
|
170
170
|
- lib/activerecord-bitemporal/version.rb
|
171
|
+
- lib/activerecord-bitemporal/visualizer.rb
|
171
172
|
homepage: https://github.com/kufu/activerecord-bitemporal
|
172
173
|
licenses:
|
173
174
|
- Apache 2.0
|
@@ -187,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
187
188
|
- !ruby/object:Gem::Version
|
188
189
|
version: '0'
|
189
190
|
requirements: []
|
190
|
-
rubygems_version: 3.3.
|
191
|
+
rubygems_version: 3.3.7
|
191
192
|
signing_key:
|
192
193
|
specification_version: 4
|
193
194
|
summary: BiTemporal Data Model for ActiveRecord
|