activerecord-bitemporal 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|