activerecord-bitemporal 0.0.1 → 1.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/.circleci/config.yml +123 -0
- data/.github/auto_assign.yml +27 -0
- data/.gitignore +2 -8
- data/Appraisals +21 -0
- data/CHANGELOG.md +39 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +62 -2
- data/LICENSE +202 -0
- data/README.md +724 -0
- data/Rakefile +6 -0
- data/activerecord-bitemporal.gemspec +20 -18
- data/bin/console +1 -0
- data/docker-compose.yml +11 -0
- data/gemfiles/rails_5.2.gemfile +8 -0
- data/gemfiles/rails_6.0.gemfile +8 -0
- data/gemfiles/rails_6.1.gemfile +8 -0
- data/gemfiles/rails_7.0.gemfile +8 -0
- data/gemfiles/rails_main.gemfile +8 -0
- data/lib/activerecord-bitemporal/bitemporal.rb +588 -0
- data/lib/activerecord-bitemporal/patches.rb +130 -0
- data/lib/activerecord-bitemporal/scope.rb +501 -0
- data/lib/activerecord-bitemporal/version.rb +4 -2
- data/lib/activerecord-bitemporal.rb +177 -4
- metadata +156 -15
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./bitemporal.rb"
|
4
|
+
|
5
|
+
module ActiveRecord::Bitemporal
|
6
|
+
module Patches
|
7
|
+
using Module.new {
|
8
|
+
refine ::ActiveRecord::Reflection::AssociationReflection do
|
9
|
+
# handle raise error, when `polymorphic? == true`
|
10
|
+
def klass
|
11
|
+
polymorphic? ? nil : super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
}
|
15
|
+
using BitemporalChecker
|
16
|
+
|
17
|
+
# nested_attributes 用の拡張
|
18
|
+
module Persistence
|
19
|
+
using Module.new {
|
20
|
+
refine Persistence do
|
21
|
+
def copy_bitemporal_option(src, dst)
|
22
|
+
return unless [src.class, dst.class].all? { |klass|
|
23
|
+
# NOTE: Can't call refine method.
|
24
|
+
# klass.bi_temporal_model?
|
25
|
+
klass.include?(ActiveRecord::Bitemporal)
|
26
|
+
}
|
27
|
+
dst.bitemporal_option_merge! src.bitemporal_option
|
28
|
+
end
|
29
|
+
end
|
30
|
+
}
|
31
|
+
|
32
|
+
# MEMO: このメソッドは BTDM 以外にもフックする必要がある
|
33
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, _attributes)
|
34
|
+
super
|
35
|
+
target = send(association_name)
|
36
|
+
return if target.nil? || !target.changed?
|
37
|
+
copy_bitemporal_option(self, target)
|
38
|
+
end
|
39
|
+
|
40
|
+
def assign_nested_attributes_for_collection_association(association_name, _attributes_collection)
|
41
|
+
# Preloading records
|
42
|
+
send(association_name).load if association(association_name).klass&.bi_temporal_model?
|
43
|
+
super
|
44
|
+
send(association_name)&.each do |target|
|
45
|
+
next unless target.changed?
|
46
|
+
copy_bitemporal_option(self, target)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Association
|
52
|
+
def skip_statement_cache?(scope)
|
53
|
+
super || bi_temporal_model?
|
54
|
+
end
|
55
|
+
|
56
|
+
def scope
|
57
|
+
scope = super
|
58
|
+
return scope unless scope.bi_temporal_model?
|
59
|
+
|
60
|
+
scope_ = scope
|
61
|
+
if owner.class&.bi_temporal_model?
|
62
|
+
if owner.valid_datetime
|
63
|
+
valid_datetime = owner.valid_datetime
|
64
|
+
scope_ = scope_.valid_at(valid_datetime)
|
65
|
+
scope_.merge!(scope_.bitemporal_value[:through].valid_at(valid_datetime)) if scope_.bitemporal_value[:through]
|
66
|
+
end
|
67
|
+
|
68
|
+
if owner.transaction_datetime
|
69
|
+
transaction_datetime = owner.transaction_datetime
|
70
|
+
scope_ = scope_.transaction_at(transaction_datetime)
|
71
|
+
scope_.merge!(scope_.bitemporal_value[:through].transaction_at(transaction_datetime)) if scope_.bitemporal_value[:through]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
return scope_
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def bi_temporal_model?
|
80
|
+
owner.class.bi_temporal_model? && klass&.bi_temporal_model?
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module ThroughAssociation
|
85
|
+
def target_scope
|
86
|
+
scope = super
|
87
|
+
return scope unless scope.bi_temporal_model?
|
88
|
+
|
89
|
+
reflection.chain.drop(1).each do |reflection|
|
90
|
+
klass = reflection.klass&.scope_for_association&.klass
|
91
|
+
next unless klass&.bi_temporal_model?
|
92
|
+
scope.bitemporal_value[:through] = klass
|
93
|
+
end
|
94
|
+
scope
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
module AssociationReflection
|
99
|
+
JoinKeys = Struct.new(:key, :foreign_key)
|
100
|
+
def get_join_keys(association_klass)
|
101
|
+
return super unless association_klass&.bi_temporal_model?
|
102
|
+
self.belongs_to? ? JoinKeys.new(association_klass.bitemporal_id_key, join_foreign_key) : super
|
103
|
+
end
|
104
|
+
|
105
|
+
def primary_key(klass)
|
106
|
+
return super unless klass&.bi_temporal_model?
|
107
|
+
klass.bitemporal_id_key
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
module Merger
|
112
|
+
def merge
|
113
|
+
if relation.klass.bi_temporal_model? && other.klass.bi_temporal_model?
|
114
|
+
relation.bitemporal_value.merge! other.bitemporal_value
|
115
|
+
end
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
module SingularAssociation
|
121
|
+
# MEMO: Except for primary_key in ActiveRecord
|
122
|
+
# https://github.com/rails/rails/blob/6-0-stable/activerecord/lib/active_record/associations/singular_association.rb#L34-L36
|
123
|
+
# excluding bitemporal_id_key
|
124
|
+
def scope_for_create
|
125
|
+
return super unless klass&.bi_temporal_model?
|
126
|
+
super.except!(klass.bitemporal_id_key)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,501 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord::Bitemporal
|
4
|
+
module NodeBitemporalInclude
|
5
|
+
refine String do
|
6
|
+
def bitemporal_include?(*)
|
7
|
+
false
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine ::Arel::Nodes::Node do
|
12
|
+
def bitemporal_include?(*columns)
|
13
|
+
case self
|
14
|
+
when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
|
15
|
+
if self.left.kind_of?(Arel::Attributes::Attribute)
|
16
|
+
subrelation = self.left
|
17
|
+
columns.include?(subrelation.name.to_s) || columns.include?("#{subrelation.relation.name}.#{subrelation.name}")
|
18
|
+
elsif self.right.kind_of?(Arel::Attributes::Attribute)
|
19
|
+
subrelation = self.right
|
20
|
+
columns.include?(subrelation.name.to_s) || columns.include?("#{subrelation.relation.name}.#{subrelation.name}")
|
21
|
+
end
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Relation
|
30
|
+
using Module.new {
|
31
|
+
refine ActiveRecord::Relation::WhereClause do
|
32
|
+
using Module.new {
|
33
|
+
refine Arel::Nodes::LessThan do
|
34
|
+
def operator; :< ; end
|
35
|
+
end
|
36
|
+
refine Arel::Nodes::LessThanOrEqual do
|
37
|
+
def operator; :<= ; end
|
38
|
+
end
|
39
|
+
refine Arel::Nodes::GreaterThan do
|
40
|
+
def operator; :> ; end
|
41
|
+
end
|
42
|
+
refine Arel::Nodes::GreaterThanOrEqual do
|
43
|
+
def operator; :>= ; end
|
44
|
+
end
|
45
|
+
refine Arel::Nodes::Node do
|
46
|
+
def operator; nil ; end
|
47
|
+
end
|
48
|
+
}
|
49
|
+
|
50
|
+
def each_operatable_node(nodes = predicates, &block)
|
51
|
+
if block
|
52
|
+
each_operatable_node(nodes).each(&block)
|
53
|
+
else
|
54
|
+
Enumerator.new { |y|
|
55
|
+
Array(nodes).each { |node|
|
56
|
+
case node
|
57
|
+
when Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
|
58
|
+
y << node if node && node.left.respond_to?(:relation)
|
59
|
+
when Arel::Nodes::Or, Arel::Nodes::And
|
60
|
+
if Gem::Version.new("6.1.0") <= ActiveRecord.version
|
61
|
+
each_operatable_node(node.left) { |node| y << node }
|
62
|
+
each_operatable_node(node.right) { |node| y << node }
|
63
|
+
end
|
64
|
+
when Arel::Nodes::Grouping
|
65
|
+
each_operatable_node(node.expr) { |node| y << node }
|
66
|
+
end
|
67
|
+
}
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def bitemporal_query_hash(*names)
|
73
|
+
each_operatable_node
|
74
|
+
.select { |node| names.include? node.left.name.to_s }
|
75
|
+
.inject(Hash.new { |hash, key| hash[key] = {} }) { |result, node|
|
76
|
+
value = node.right.try(:val) || node.right.try(:value).then { |it| it.try(:value_before_type_cast) || it }
|
77
|
+
result[node.left.relation.name][node.left.name.to_s] = [node.operator, value]
|
78
|
+
result
|
79
|
+
}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
refine Relation do
|
84
|
+
def bitemporal_clause(table_name = klass.table_name)
|
85
|
+
node_hash = where_clause.bitemporal_query_hash("valid_from", "valid_to", "transaction_from", "transaction_to")
|
86
|
+
valid_from = node_hash.dig(table_name, "valid_from", 1)
|
87
|
+
valid_to = node_hash.dig(table_name, "valid_to", 1)
|
88
|
+
transaction_from = node_hash.dig(table_name, "transaction_from", 1)
|
89
|
+
transaction_to = node_hash.dig(table_name, "transaction_to", 1)
|
90
|
+
{
|
91
|
+
valid_from: valid_from,
|
92
|
+
valid_to: valid_to,
|
93
|
+
valid_datetime: valid_from == valid_to ? valid_from : nil,
|
94
|
+
transaction_from: transaction_from,
|
95
|
+
transaction_to: transaction_to,
|
96
|
+
transaction_datetime: transaction_from == transaction_to ? transaction_from : nil,
|
97
|
+
ignore_valid_datetime: valid_from.nil? && valid_to.nil? ? true : false,
|
98
|
+
ignore_transaction_datetime: transaction_from.nil? && transaction_to.nil? ? true : false
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
}
|
103
|
+
|
104
|
+
if ActiveRecord.version < Gem::Version.new("6.1.0")
|
105
|
+
class WhereClauseWithCheckTable < ActiveRecord::Relation::WhereClause
|
106
|
+
using NodeBitemporalInclude
|
107
|
+
|
108
|
+
def bitemporal_include?(column)
|
109
|
+
!!predicates.grep(::Arel::Nodes::Node).find do |node|
|
110
|
+
node.bitemporal_include?(column)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def except_predicates(columns)
|
117
|
+
columns = Array(columns)
|
118
|
+
predicates.reject do |node|
|
119
|
+
::Arel::Nodes::Node === node && node.bitemporal_include?(*columns)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def where_clause
|
125
|
+
WhereClauseWithCheckTable.new(super.send(:predicates))
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
module MergeWithExceptBitemporalDefaultScope
|
130
|
+
using BitemporalChecker
|
131
|
+
def merge(other)
|
132
|
+
if other.is_a?(Relation) && other.klass.bi_temporal_model?
|
133
|
+
super(other.except_bitemporal_default_scope)
|
134
|
+
else
|
135
|
+
super
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def valid_datetime
|
141
|
+
bitemporal_clause[:valid_datetime]&.in_time_zone
|
142
|
+
end
|
143
|
+
|
144
|
+
def transaction_datetime
|
145
|
+
bitemporal_clause[:transaction_datetime]&.in_time_zone
|
146
|
+
end
|
147
|
+
|
148
|
+
def bitemporal_option
|
149
|
+
::ActiveRecord::Bitemporal.merge_by(bitemporal_value.merge bitemporal_clause)
|
150
|
+
end
|
151
|
+
|
152
|
+
def bitemporal_option_merge!(other)
|
153
|
+
self.bitemporal_value = bitemporal_value.merge other
|
154
|
+
end
|
155
|
+
|
156
|
+
def bitemporal_value
|
157
|
+
@values[:bitemporal_value] ||= {}
|
158
|
+
end
|
159
|
+
|
160
|
+
def bitemporal_value=(value)
|
161
|
+
@values[:bitemporal_value] = value
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
module Scope
|
166
|
+
extend ActiveSupport::Concern
|
167
|
+
|
168
|
+
module ActiveRecordRelationScope
|
169
|
+
refine ::ActiveRecord::Relation do
|
170
|
+
def bitemporal_where_bind(attr_name, operator, value)
|
171
|
+
where(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
|
172
|
+
end
|
173
|
+
|
174
|
+
def bitemporal_where_bind!(attr_name, operator, value)
|
175
|
+
where!(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
|
176
|
+
end
|
177
|
+
|
178
|
+
def with_valid_datetime
|
179
|
+
tap { |relation| relation.bitemporal_value[:with_valid_datetime] = true }
|
180
|
+
end
|
181
|
+
|
182
|
+
def without_valid_datetime
|
183
|
+
tap { |relation| relation.bitemporal_value[:with_valid_datetime] = false }
|
184
|
+
end
|
185
|
+
|
186
|
+
def with_transaction_datetime
|
187
|
+
tap { |relation| relation.bitemporal_value[:with_transaction_datetime] = true }
|
188
|
+
end
|
189
|
+
|
190
|
+
def without_transaction_datetime
|
191
|
+
tap { |relation| relation.bitemporal_value[:with_transaction_datetime] = false }
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
included do
|
197
|
+
using Module.new {
|
198
|
+
refine ActiveRecord::Bitemporal::Optionable do
|
199
|
+
def force_valid_datetime?
|
200
|
+
bitemporal_option_storage[:force_valid_datetime]
|
201
|
+
end
|
202
|
+
|
203
|
+
def ignore_valid_datetime?
|
204
|
+
bitemporal_option_storage[:ignore_valid_datetime]
|
205
|
+
end
|
206
|
+
|
207
|
+
def force_transaction_datetime?
|
208
|
+
bitemporal_option_storage[:force_transaction_datetime]
|
209
|
+
end
|
210
|
+
|
211
|
+
def ignore_transaction_datetime?
|
212
|
+
bitemporal_option_storage[:ignore_transaction_datetime]
|
213
|
+
end
|
214
|
+
end
|
215
|
+
}
|
216
|
+
|
217
|
+
if ActiveRecord.version < Gem::Version.new("6.1.0")
|
218
|
+
module ActiveRecordRelationScope
|
219
|
+
refine ::ActiveRecord::Relation do
|
220
|
+
%i(valid_from valid_to transaction_from transaction_to).each { |column|
|
221
|
+
module_eval <<-STR, __FILE__, __LINE__ + 1
|
222
|
+
def _ignore_#{column}
|
223
|
+
unscope(where: "\#{table.name}.#{column}")
|
224
|
+
.tap { |relation| relation.merge!(bitemporal_value[:through].unscoped._ignore_#{column}) if bitemporal_value[:through] }
|
225
|
+
end
|
226
|
+
|
227
|
+
def _except_#{column}
|
228
|
+
return self unless where_clause.bitemporal_include?("\#{table.name}.#{column}")
|
229
|
+
all._ignore_#{column}.tap { |relation|
|
230
|
+
relation.unscope_values.delete({ where: "\#{table.name}.#{column}" })
|
231
|
+
}
|
232
|
+
end
|
233
|
+
STR
|
234
|
+
|
235
|
+
[
|
236
|
+
:lt, # column < datetime
|
237
|
+
:lteq, # column <= datetime
|
238
|
+
:gt, # column > datetime
|
239
|
+
:gteq # column >= datetime
|
240
|
+
].each { |op|
|
241
|
+
module_eval <<-STR, __FILE__, __LINE__ + 1
|
242
|
+
def _#{column}_#{op}(datetime, without_ignore: false)
|
243
|
+
target_datetime = datetime&.in_time_zone || Time.current
|
244
|
+
relation = self.tap { |relation| break relation._ignore_#{column} unless without_ignore }
|
245
|
+
relation.bitemporal_where_bind!(:#{column}, :#{op}, target_datetime)
|
246
|
+
.tap { |relation| relation.merge!(bitemporal_value[:through].unscoped._#{column}_#{op}(target_datetime)) if bitemporal_value[:through] }
|
247
|
+
end
|
248
|
+
STR
|
249
|
+
}
|
250
|
+
}
|
251
|
+
end
|
252
|
+
end
|
253
|
+
else
|
254
|
+
module ActiveRecordRelationScope
|
255
|
+
module EqualAttributeName
|
256
|
+
refine ::Object do
|
257
|
+
def equal_attribute_name(*)
|
258
|
+
false
|
259
|
+
end
|
260
|
+
end
|
261
|
+
refine ::Hash do
|
262
|
+
def equal_attribute_name(other)
|
263
|
+
self[:where].equal_attribute_name(other)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
refine ::Array do
|
267
|
+
def equal_attribute_name(other)
|
268
|
+
first.equal_attribute_name(other)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
refine ::String do
|
272
|
+
def equal_attribute_name(other)
|
273
|
+
self == other.to_s
|
274
|
+
end
|
275
|
+
end
|
276
|
+
refine ::Symbol do
|
277
|
+
def equal_attribute_name(other)
|
278
|
+
self.to_s == other.to_s
|
279
|
+
end
|
280
|
+
end
|
281
|
+
refine ::Arel::Attributes::Attribute do
|
282
|
+
def equal_attribute_name(other)
|
283
|
+
"#{relation.name}.#{name}" == other.to_s
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
refine ::ActiveRecord::Relation do
|
289
|
+
using EqualAttributeName
|
290
|
+
using NodeBitemporalInclude
|
291
|
+
|
292
|
+
def bitemporal_rewhere_bind(attr_name, operator, value, table = self.table)
|
293
|
+
rewhere(table[attr_name].public_send(operator, predicate_builder.build_bind_attribute(attr_name, value)))
|
294
|
+
end
|
295
|
+
|
296
|
+
%i(valid_from valid_to transaction_from transaction_to).each { |column|
|
297
|
+
module_eval <<-STR, __FILE__, __LINE__ + 1
|
298
|
+
def _ignore_#{column}
|
299
|
+
unscope(where: :"\#{table.name}.#{column}")
|
300
|
+
.tap { |relation| relation.unscope!(where: bitemporal_value[:through].arel_table["#{column}"]) if bitemporal_value[:through] }
|
301
|
+
end
|
302
|
+
|
303
|
+
def _except_#{column}
|
304
|
+
return self unless where_clause.send(:predicates).find { |node|
|
305
|
+
node.bitemporal_include?("\#{table.name}.#{column}")
|
306
|
+
}
|
307
|
+
_ignore_#{column}.tap { |relation|
|
308
|
+
relation.unscope_values.reject! { |query| query.equal_attribute_name("\#{table.name}.#{column}") }
|
309
|
+
}
|
310
|
+
end
|
311
|
+
STR
|
312
|
+
|
313
|
+
[
|
314
|
+
:lt, # column < datetime
|
315
|
+
:lteq, # column <= datetime
|
316
|
+
:gt, # column > datetime
|
317
|
+
:gteq # column >= datetime
|
318
|
+
].each { |op|
|
319
|
+
module_eval <<-STR, __FILE__, __LINE__ + 1
|
320
|
+
def _#{column}_#{op}(datetime,**)
|
321
|
+
target_datetime = datetime&.in_time_zone || Time.current
|
322
|
+
bitemporal_rewhere_bind("#{column}", :#{op}, target_datetime)
|
323
|
+
.tap { |relation| break relation.bitemporal_rewhere_bind("#{column}", :#{op}, target_datetime, bitemporal_value[:through].arel_table) if bitemporal_value[:through] }
|
324
|
+
end
|
325
|
+
STR
|
326
|
+
}
|
327
|
+
}
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
using ActiveRecordRelationScope
|
332
|
+
|
333
|
+
%i(valid_from valid_to transaction_from transaction_to).each { |column|
|
334
|
+
scope :"ignore_#{column}", -> {
|
335
|
+
public_send(:"_ignore_#{column}")
|
336
|
+
}
|
337
|
+
|
338
|
+
scope :"except_#{column}", -> {
|
339
|
+
public_send(:"_except_#{column}")
|
340
|
+
}
|
341
|
+
|
342
|
+
[
|
343
|
+
:lt, # column < datetime
|
344
|
+
:lteq, # column <= datetime
|
345
|
+
:gt, # column > datetime
|
346
|
+
:gteq # column >= datetime
|
347
|
+
].each { |op|
|
348
|
+
scope :"#{column}_#{op}", -> (datetime) {
|
349
|
+
public_send(:"_#{column}_#{op}", datetime)
|
350
|
+
}
|
351
|
+
}
|
352
|
+
}
|
353
|
+
|
354
|
+
# valid_from <= datetime && datetime < valid_to
|
355
|
+
scope :valid_at, -> (datetime) {
|
356
|
+
if ActiveRecord::Bitemporal.force_valid_datetime?
|
357
|
+
datetime = ActiveRecord::Bitemporal.valid_datetime
|
358
|
+
end
|
359
|
+
datetime = Time.current if datetime.nil?
|
360
|
+
valid_from_lteq(datetime).valid_to_gt(datetime).with_valid_datetime
|
361
|
+
}
|
362
|
+
scope :ignore_valid_datetime, -> {
|
363
|
+
ignore_valid_from.ignore_valid_to.without_valid_datetime
|
364
|
+
}
|
365
|
+
scope :except_valid_datetime, -> {
|
366
|
+
except_valid_from.except_valid_to.tap { |relation| relation.bitemporal_value.except! :with_valid_datetime }
|
367
|
+
}
|
368
|
+
|
369
|
+
# transaction_from <= datetime && datetime < transaction_to
|
370
|
+
scope :transaction_at, -> (datetime) {
|
371
|
+
if ActiveRecord::Bitemporal.force_transaction_datetime?
|
372
|
+
datetime = ActiveRecord::Bitemporal.transaction_datetime
|
373
|
+
end
|
374
|
+
datetime = Time.current if datetime.nil?
|
375
|
+
transaction_from_lteq(datetime).transaction_to_gt(datetime).with_transaction_datetime
|
376
|
+
}
|
377
|
+
scope :ignore_transaction_datetime, -> {
|
378
|
+
ignore_transaction_from.ignore_transaction_to.without_transaction_datetime
|
379
|
+
}
|
380
|
+
scope :except_transaction_datetime, -> {
|
381
|
+
except_transaction_from.except_transaction_to.tap { |relation| relation.bitemporal_value.except! :with_transaction_datetime }
|
382
|
+
}
|
383
|
+
|
384
|
+
scope :bitemporal_at, -> (datetime) {
|
385
|
+
datetime = Time.current if datetime.nil?
|
386
|
+
relation = self
|
387
|
+
|
388
|
+
if !ActiveRecord::Bitemporal.ignore_transaction_datetime?
|
389
|
+
relation = relation.transaction_at(ActiveRecord::Bitemporal.transaction_datetime || datetime)
|
390
|
+
end
|
391
|
+
|
392
|
+
if !ActiveRecord::Bitemporal.ignore_valid_datetime?
|
393
|
+
relation = relation.valid_at(ActiveRecord::Bitemporal.valid_datetime || datetime)
|
394
|
+
end
|
395
|
+
|
396
|
+
relation
|
397
|
+
}
|
398
|
+
scope :ignore_bitemporal_datetime, -> {
|
399
|
+
ignore_transaction_datetime.ignore_valid_datetime
|
400
|
+
}
|
401
|
+
scope :except_bitemporal_datetime, -> {
|
402
|
+
except_transaction_datetime.except_valid_datetime
|
403
|
+
}
|
404
|
+
|
405
|
+
scope :bitemporal_default_scope, -> {
|
406
|
+
datetime = Time.current
|
407
|
+
relation = self
|
408
|
+
|
409
|
+
if !ActiveRecord::Bitemporal.ignore_transaction_datetime?
|
410
|
+
if ActiveRecord::Bitemporal.transaction_datetime
|
411
|
+
transaction_datetime = ActiveRecord::Bitemporal.transaction_datetime
|
412
|
+
relation.bitemporal_value[:with_transaction_datetime] = :default_scope_with_transaction_datetime
|
413
|
+
else
|
414
|
+
relation.bitemporal_value[:with_transaction_datetime] = :default_scope
|
415
|
+
end
|
416
|
+
|
417
|
+
# Calling scope was slow, so don't call scope
|
418
|
+
relation.unscope_values += [
|
419
|
+
{ where: "#{table.name}.transaction_from" },
|
420
|
+
{ where: "#{table.name}.transaction_to" }
|
421
|
+
]
|
422
|
+
relation = relation
|
423
|
+
._transaction_from_lteq(transaction_datetime || datetime, without_ignore: true)
|
424
|
+
._transaction_to_gt(transaction_datetime || datetime, without_ignore: true)
|
425
|
+
else
|
426
|
+
relation.tap { |relation| relation.without_transaction_datetime unless ActiveRecord::Bitemporal.transaction_datetime }
|
427
|
+
end
|
428
|
+
|
429
|
+
if !ActiveRecord::Bitemporal.ignore_valid_datetime?
|
430
|
+
if ActiveRecord::Bitemporal.valid_datetime
|
431
|
+
valid_datetime = ActiveRecord::Bitemporal.valid_datetime
|
432
|
+
relation.bitemporal_value[:with_valid_datetime] = :default_scope_with_valid_datetime
|
433
|
+
else
|
434
|
+
relation.bitemporal_value[:with_valid_datetime] = :default_scope
|
435
|
+
end
|
436
|
+
|
437
|
+
relation.unscope_values += [
|
438
|
+
{ where: "#{table.name}.valid_from" },
|
439
|
+
{ where: "#{table.name}.valid_to" }
|
440
|
+
]
|
441
|
+
relation = relation
|
442
|
+
._valid_from_lteq(valid_datetime || datetime, without_ignore: true)
|
443
|
+
._valid_to_gt(valid_datetime || datetime, without_ignore: true)
|
444
|
+
else
|
445
|
+
relation.tap { |relation| relation.without_valid_datetime unless ActiveRecord::Bitemporal.valid_datetime }
|
446
|
+
end
|
447
|
+
|
448
|
+
relation
|
449
|
+
}
|
450
|
+
|
451
|
+
scope :except_bitemporal_default_scope, -> {
|
452
|
+
scope = all
|
453
|
+
scope = scope.except_valid_datetime if bitemporal_value[:with_valid_datetime] == :default_scope || bitemporal_value[:with_valid_datetime] == :default_scope_with_valid_datetime
|
454
|
+
scope = scope.except_transaction_datetime if bitemporal_value[:with_transaction_datetime] == :default_scope || bitemporal_value[:with_transaction_datetime] == :default_scope_with_transaction_datetime
|
455
|
+
scope
|
456
|
+
}
|
457
|
+
|
458
|
+
scope :within_deleted, -> {
|
459
|
+
ignore_transaction_datetime
|
460
|
+
}
|
461
|
+
scope :without_deleted, -> {
|
462
|
+
valid_at(Time.current)
|
463
|
+
}
|
464
|
+
|
465
|
+
scope :bitemporal_for, -> (id) {
|
466
|
+
where(bitemporal_id: id)
|
467
|
+
}
|
468
|
+
|
469
|
+
scope :valid_in, -> (from: nil, to: nil) {
|
470
|
+
ignore_valid_datetime
|
471
|
+
.tap { |relation| break relation.bitemporal_where_bind("valid_to", :gteq, from.in_time_zone.to_datetime) if from }
|
472
|
+
.tap { |relation| break relation.bitemporal_where_bind("valid_from", :lteq, to.in_time_zone.to_datetime) if to }
|
473
|
+
}
|
474
|
+
scope :valid_allin, -> (from: nil, to: nil) {
|
475
|
+
ignore_valid_datetime
|
476
|
+
.tap { |relation| break relation.bitemporal_where_bind("valid_from", :gteq, from.in_time_zone.to_datetime) if from }
|
477
|
+
.tap { |relation| break relation.bitemporal_where_bind("valid_to", :lteq, to.in_time_zone.to_datetime) if to }
|
478
|
+
}
|
479
|
+
end
|
480
|
+
|
481
|
+
module Extension
|
482
|
+
extend ActiveSupport::Concern
|
483
|
+
|
484
|
+
included do
|
485
|
+
scope :bitemporal_histories, -> (*ids) {
|
486
|
+
ignore_valid_datetime.bitemporal_for(*ids)
|
487
|
+
}
|
488
|
+
def self.bitemporal_most_future(id)
|
489
|
+
bitemporal_histories(id).order(valid_from: :asc).last
|
490
|
+
end
|
491
|
+
def self.bitemporal_most_past(id)
|
492
|
+
bitemporal_histories(id).order(valid_from: :asc).first
|
493
|
+
end
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
module Experimental
|
498
|
+
extend ActiveSupport::Concern
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|