chrono_model 1.2.2 → 3.0.1
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/LICENSE +19 -20
- data/README.md +73 -62
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +14 -14
- data/lib/active_record/tasks/chronomodel_database_tasks.rb +40 -39
- data/lib/chrono_model/adapter/ddl.rb +168 -153
- data/lib/chrono_model/adapter/indexes.rb +99 -94
- data/lib/chrono_model/adapter/migrations.rb +81 -104
- data/lib/chrono_model/adapter/migrations_modules/stable.rb +41 -0
- data/lib/chrono_model/adapter/tsrange.rb +20 -5
- data/lib/chrono_model/adapter/upgrade.rb +89 -91
- data/lib/chrono_model/adapter.rb +59 -31
- data/lib/chrono_model/chrono.rb +17 -0
- data/lib/chrono_model/conversions.rb +14 -8
- data/lib/chrono_model/db_console.rb +5 -0
- data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
- data/lib/chrono_model/patches/as_of_time_relation.rb +3 -13
- data/lib/chrono_model/patches/association.rb +15 -12
- data/lib/chrono_model/patches/batches.rb +13 -0
- data/lib/chrono_model/patches/db_console.rb +20 -4
- data/lib/chrono_model/patches/join_node.rb +4 -4
- data/lib/chrono_model/patches/preloader.rb +41 -11
- data/lib/chrono_model/patches/relation.rb +51 -8
- data/lib/chrono_model/patches.rb +3 -1
- data/lib/chrono_model/railtie.rb +13 -27
- data/lib/chrono_model/time_gate.rb +3 -3
- data/lib/chrono_model/time_machine/history_model.rb +65 -31
- data/lib/chrono_model/time_machine/time_query.rb +65 -49
- data/lib/chrono_model/time_machine/timeline.rb +52 -28
- data/lib/chrono_model/time_machine.rb +57 -25
- data/lib/chrono_model/utilities.rb +3 -3
- data/lib/chrono_model/version.rb +3 -1
- data/lib/chrono_model.rb +31 -36
- metadata +24 -263
- data/.gitignore +0 -21
- data/.rspec +0 -2
- data/.travis.yml +0 -41
- data/Gemfile +0 -4
- data/README.sql +0 -161
- data/Rakefile +0 -25
- data/chrono_model.gemspec +0 -33
- data/gemfiles/rails_5.0.gemfile +0 -6
- data/gemfiles/rails_5.1.gemfile +0 -6
- data/gemfiles/rails_5.2.gemfile +0 -6
- data/lib/chrono_model/json.rb +0 -28
- data/spec/aruba/dbconsole_spec.rb +0 -25
- data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
- data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
- data/spec/aruba/fixtures/empty_structure.sql +0 -27
- data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
- data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
- data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
- data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
- data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
- data/spec/aruba/migrations_spec.rb +0 -48
- data/spec/aruba/rake_task_spec.rb +0 -71
- data/spec/chrono_model/adapter/base_spec.rb +0 -157
- data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
- data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
- data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
- data/spec/chrono_model/conversions_spec.rb +0 -43
- data/spec/chrono_model/history_models_spec.rb +0 -32
- data/spec/chrono_model/json_ops_spec.rb +0 -59
- data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
- data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
- data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
- data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
- data/spec/chrono_model/time_machine/history_spec.rb +0 -104
- data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
- data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
- data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
- data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
- data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
- data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
- data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
- data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
- data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
- data/spec/config.travis.yml +0 -5
- data/spec/config.yml.example +0 -9
- data/spec/spec_helper.rb +0 -33
- data/spec/support/adapter/helpers.rb +0 -53
- data/spec/support/adapter/structure.rb +0 -44
- data/spec/support/aruba.rb +0 -44
- data/spec/support/connection.rb +0 -70
- data/spec/support/matchers/base.rb +0 -56
- data/spec/support/matchers/column.rb +0 -99
- data/spec/support/matchers/function.rb +0 -79
- data/spec/support/matchers/index.rb +0 -69
- data/spec/support/matchers/schema.rb +0 -39
- data/spec/support/matchers/table.rb +0 -275
- data/spec/support/time_machine/helpers.rb +0 -47
- data/spec/support/time_machine/structure.rb +0 -111
- data/sql/json_ops.sql +0 -56
- data/sql/uninstall-json_ops.sql +0 -24
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module TimeMachine
|
|
3
|
-
|
|
4
5
|
module HistoryModel
|
|
5
6
|
extend ActiveSupport::Concern
|
|
6
7
|
|
|
7
8
|
included do
|
|
8
|
-
self.table_name =
|
|
9
|
+
self.table_name = "#{Adapter::HISTORY_SCHEMA}.#{superclass.table_name}"
|
|
9
10
|
|
|
10
11
|
scope :chronological, -> { order(Arel.sql('lower(validity) ASC')) }
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
# ACTIVE RECORD 7 does not call `class.find` but a new internal method called `_find_record`
|
|
15
|
+
def _find_record(options)
|
|
16
|
+
if options && options[:lock]
|
|
17
|
+
self.class.preload(strict_loaded_associations).lock(options[:lock]).find_by!(hid: hid)
|
|
18
|
+
else
|
|
19
|
+
self.class.preload(strict_loaded_associations).find_by!(hid: hid)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
13
23
|
# Methods that make up the history interface of the companion History
|
|
14
24
|
# model, automatically built for each Model that includes TimeMachine
|
|
15
25
|
#
|
|
@@ -21,11 +31,11 @@ module ChronoModel
|
|
|
21
31
|
# setting it now and ensuring to reset it to the original one after
|
|
22
32
|
# execution completes. FIXME
|
|
23
33
|
#
|
|
24
|
-
def with_hid_pkey
|
|
25
|
-
old =
|
|
34
|
+
def with_hid_pkey
|
|
35
|
+
old = primary_key
|
|
26
36
|
self.primary_key = :hid
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
yield
|
|
29
39
|
ensure
|
|
30
40
|
self.primary_key = old
|
|
31
41
|
end
|
|
@@ -61,9 +71,12 @@ module ChronoModel
|
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
def virtual_table_at(time, table_name: nil)
|
|
64
|
-
virtual_name =
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
virtual_name =
|
|
75
|
+
if table_name
|
|
76
|
+
connection.quote_table_name(table_name)
|
|
77
|
+
else
|
|
78
|
+
superclass.quoted_table_name
|
|
79
|
+
end
|
|
67
80
|
|
|
68
81
|
"(#{at(time).to_sql}) #{virtual_name}"
|
|
69
82
|
end
|
|
@@ -77,7 +90,7 @@ module ChronoModel
|
|
|
77
90
|
# Returns the history sorted by recorded_at
|
|
78
91
|
#
|
|
79
92
|
def sorted
|
|
80
|
-
all.order(Arel.sql(%
|
|
93
|
+
all.order(Arel.sql(%( #{quoted_table_name}."recorded_at" ASC, #{quoted_table_name}."hid" ASC )))
|
|
81
94
|
end
|
|
82
95
|
|
|
83
96
|
# Fetches the given +object+ history, sorted by history record time
|
|
@@ -96,9 +109,7 @@ module ChronoModel
|
|
|
96
109
|
#
|
|
97
110
|
# As such it is overriden here to return the same contents that
|
|
98
111
|
# the parent would have returned.
|
|
99
|
-
|
|
100
|
-
superclass.sti_name
|
|
101
|
-
end
|
|
112
|
+
delegate :sti_name, to: :superclass
|
|
102
113
|
|
|
103
114
|
# For STI to work, the history model needs to have the exact same
|
|
104
115
|
# semantics as the model it inherits from. However given it is
|
|
@@ -107,18 +118,17 @@ module ChronoModel
|
|
|
107
118
|
# same exact hierarchy location as its parent, thus this is defined in
|
|
108
119
|
# this override.
|
|
109
120
|
#
|
|
110
|
-
|
|
111
|
-
superclass.descends_from_active_record?
|
|
112
|
-
end
|
|
121
|
+
delegate :descends_from_active_record?, to: :superclass
|
|
113
122
|
|
|
114
123
|
private
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
|
|
125
|
+
# STI fails when a Foo::History record has Foo as type in the
|
|
126
|
+
# inheritance column; AR expects the type to be an instance of the
|
|
127
|
+
# current class or a descendant (or self).
|
|
128
|
+
#
|
|
129
|
+
def find_sti_class(type_name)
|
|
130
|
+
super("#{type_name}::History")
|
|
131
|
+
end
|
|
122
132
|
end
|
|
123
133
|
|
|
124
134
|
# The history id is `hid`, but this cannot set as primary key
|
|
@@ -134,15 +144,15 @@ module ChronoModel
|
|
|
134
144
|
end
|
|
135
145
|
|
|
136
146
|
def save(*)
|
|
137
|
-
|
|
147
|
+
with_hid_pkey { super }
|
|
138
148
|
end
|
|
139
149
|
|
|
140
150
|
def save!(*)
|
|
141
|
-
|
|
151
|
+
with_hid_pkey { super }
|
|
142
152
|
end
|
|
143
153
|
|
|
144
154
|
def update_columns(*)
|
|
145
|
-
|
|
155
|
+
with_hid_pkey { super }
|
|
146
156
|
end
|
|
147
157
|
|
|
148
158
|
def historical?
|
|
@@ -153,7 +163,7 @@ module ChronoModel
|
|
|
153
163
|
# is the first one.
|
|
154
164
|
#
|
|
155
165
|
def pred
|
|
156
|
-
return if
|
|
166
|
+
return if valid_from.nil?
|
|
157
167
|
|
|
158
168
|
if self.class.timeline_associations.empty?
|
|
159
169
|
self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first
|
|
@@ -166,7 +176,7 @@ module ChronoModel
|
|
|
166
176
|
# last one.
|
|
167
177
|
#
|
|
168
178
|
def succ
|
|
169
|
-
return if
|
|
179
|
+
return if valid_to.nil?
|
|
170
180
|
|
|
171
181
|
if self.class.timeline_associations.empty?
|
|
172
182
|
self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first
|
|
@@ -174,7 +184,7 @@ module ChronoModel
|
|
|
174
184
|
super(id: rid, after: valid_to, table: self.class.superclass.quoted_table_name)
|
|
175
185
|
end
|
|
176
186
|
end
|
|
177
|
-
alias
|
|
187
|
+
alias next succ
|
|
178
188
|
|
|
179
189
|
# Returns the first history entry
|
|
180
190
|
#
|
|
@@ -194,9 +204,9 @@ module ChronoModel
|
|
|
194
204
|
self.class.superclass.find(rid)
|
|
195
205
|
end
|
|
196
206
|
|
|
197
|
-
def record
|
|
207
|
+
def record # :nodoc:
|
|
198
208
|
ActiveSupport::Deprecation.warn '.record is deprecated in favour of .current_version'
|
|
199
|
-
|
|
209
|
+
current_version
|
|
200
210
|
end
|
|
201
211
|
|
|
202
212
|
def valid_from
|
|
@@ -211,7 +221,31 @@ module ChronoModel
|
|
|
211
221
|
def recorded_at
|
|
212
222
|
ChronoModel::Conversions.string_to_utc_time attributes_before_type_cast['recorded_at']
|
|
213
223
|
end
|
|
214
|
-
end
|
|
215
224
|
|
|
225
|
+
# Starting from Rails 6.0, `.read_attribute` will use the memoized
|
|
226
|
+
# `primary_key` if it detects that the attribute name is `id`.
|
|
227
|
+
#
|
|
228
|
+
# Since the `primary key` may have been changed to `hid` because of
|
|
229
|
+
# `.find` overload, the new behavior may break relations where `id` is
|
|
230
|
+
# still the correct attribute to read
|
|
231
|
+
#
|
|
232
|
+
# Ref: ifad/chronomodel#181
|
|
233
|
+
def read_attribute(attr_name, &block)
|
|
234
|
+
return super unless attr_name.to_s == 'id' && @primary_key.to_s == 'hid'
|
|
235
|
+
|
|
236
|
+
_read_attribute('id', &block)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
def with_hid_pkey(&block)
|
|
242
|
+
old_primary_key = @primary_key
|
|
243
|
+
@primary_key = :hid
|
|
244
|
+
|
|
245
|
+
self.class.with_hid_pkey(&block)
|
|
246
|
+
ensure
|
|
247
|
+
@primary_key = old_primary_key
|
|
248
|
+
end
|
|
249
|
+
end
|
|
216
250
|
end
|
|
217
251
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module TimeMachine
|
|
3
|
-
|
|
4
5
|
#
|
|
5
6
|
# TODO Documentation
|
|
6
7
|
#
|
|
@@ -12,75 +13,90 @@ module ChronoModel
|
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
private
|
|
15
|
-
def time_query_sql(match, time, range, options)
|
|
16
|
-
case match
|
|
17
|
-
when :at
|
|
18
|
-
build_time_query_at(time, range)
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
def time_query_sql(match, time, range, options)
|
|
18
|
+
case match
|
|
19
|
+
when :at
|
|
20
|
+
build_time_query_at(time, range)
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
build_time_query(['NULL', time_for_time_query(time, range)], range, op)
|
|
22
|
+
when :not
|
|
23
|
+
"NOT (#{build_time_query_at(time, range)})"
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
when :before
|
|
26
|
+
op =
|
|
27
|
+
if options.fetch(:inclusive, true)
|
|
28
|
+
'&&'
|
|
29
|
+
else
|
|
30
|
+
'@>'
|
|
31
|
+
end
|
|
32
|
+
build_time_query(['NULL', time_for_time_query(time, range)], range, op)
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
when :after
|
|
35
|
+
op =
|
|
36
|
+
if options.fetch(:inclusive, true)
|
|
37
|
+
'&&'
|
|
38
|
+
else
|
|
39
|
+
'@>'
|
|
40
|
+
end
|
|
41
|
+
build_time_query([time_for_time_query(time, range), 'NULL'], range, op)
|
|
42
|
+
|
|
43
|
+
else
|
|
44
|
+
raise ChronoModel::Error, "Invalid time_query: #{match}"
|
|
34
45
|
end
|
|
46
|
+
end
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
48
|
+
def time_for_time_query(t, column)
|
|
49
|
+
if t == :now || t == :today
|
|
50
|
+
now_for_column(column)
|
|
51
|
+
else
|
|
52
|
+
quoted_t = connection.quote(connection.quoted_date(t))
|
|
53
|
+
"#{quoted_t}::#{primitive_type_for_column(column)}"
|
|
43
54
|
end
|
|
55
|
+
end
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
57
|
+
def now_for_column(column)
|
|
58
|
+
case column.type
|
|
59
|
+
when :tsrange, :tstzrange then "timezone('UTC', current_timestamp)"
|
|
60
|
+
when :daterange then 'current_date'
|
|
61
|
+
else raise "Cannot generate 'now()' for #{column.type} column #{column.name}"
|
|
51
62
|
end
|
|
63
|
+
end
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
end
|
|
65
|
+
def primitive_type_for_column(column)
|
|
66
|
+
case column.type
|
|
67
|
+
when :tsrange then :timestamp
|
|
68
|
+
when :tstzrange then :timestamptz
|
|
69
|
+
when :daterange then :date
|
|
70
|
+
else raise "Don't know how to map #{column.type} column #{column.name} to a primitive type"
|
|
60
71
|
end
|
|
72
|
+
end
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
def build_time_query_at(time, range)
|
|
75
|
+
time =
|
|
76
|
+
if time.is_a?(Array)
|
|
77
|
+
time.map! { |t| time_for_time_query(t, range) }
|
|
65
78
|
|
|
66
79
|
# If both edges of the range are the same the query fails using the '&&' operator.
|
|
67
80
|
# The correct solution is to use the <@ operator.
|
|
68
|
-
time.first == time.last
|
|
81
|
+
if time.first == time.last
|
|
82
|
+
time.first
|
|
83
|
+
else
|
|
84
|
+
time
|
|
85
|
+
end
|
|
69
86
|
else
|
|
70
87
|
time_for_time_query(time, range)
|
|
71
88
|
end
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
build_time_query(time, range)
|
|
91
|
+
end
|
|
75
92
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
end
|
|
93
|
+
def build_time_query(time, range, op = '&&')
|
|
94
|
+
if time.is_a?(Array)
|
|
95
|
+
Arel.sql %[ #{range.type}(#{time.first}, #{time.last}) #{op} #{table_name}.#{range.name} ]
|
|
96
|
+
else
|
|
97
|
+
Arel.sql %( #{time} <@ #{table_name}.#{range.name} )
|
|
82
98
|
end
|
|
99
|
+
end
|
|
83
100
|
end
|
|
84
|
-
|
|
85
101
|
end
|
|
86
102
|
end
|
|
@@ -1,59 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module TimeMachine
|
|
3
|
-
|
|
4
5
|
module Timeline
|
|
5
6
|
# Returns an Array of unique UTC timestamps for which at least an
|
|
6
7
|
# history record exists. Takes temporal associations into account.
|
|
7
8
|
#
|
|
8
9
|
def timeline(record = nil, options = {})
|
|
9
|
-
rid =
|
|
10
|
+
rid =
|
|
11
|
+
if record
|
|
12
|
+
if record.respond_to?(:rid)
|
|
13
|
+
record.rid
|
|
14
|
+
else
|
|
15
|
+
record.id
|
|
16
|
+
end
|
|
17
|
+
end
|
|
10
18
|
|
|
11
|
-
assocs =
|
|
12
|
-
|
|
19
|
+
assocs =
|
|
20
|
+
if options.key?(:with)
|
|
21
|
+
timeline_associations_from(options[:with])
|
|
22
|
+
else
|
|
23
|
+
timeline_associations
|
|
24
|
+
end
|
|
13
25
|
|
|
14
26
|
models = []
|
|
15
|
-
models.push self if
|
|
16
|
-
models.concat(assocs.map {|a| a.klass.history})
|
|
27
|
+
models.push self if chrono?
|
|
28
|
+
models.concat(assocs.map { |a| a.klass.history })
|
|
17
29
|
|
|
18
30
|
return [] if models.empty?
|
|
19
31
|
|
|
20
|
-
fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
|
|
32
|
+
fields = models.inject([]) { |a, m| a.concat m.quoted_history_fields }
|
|
21
33
|
|
|
22
|
-
relation =
|
|
23
|
-
|
|
34
|
+
relation = except(:order)
|
|
35
|
+
.select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
|
|
24
36
|
|
|
25
37
|
if assocs.present?
|
|
26
|
-
|
|
38
|
+
assocs.each do |ass|
|
|
39
|
+
association_quoted_table_name = connection.quote_table_name(ass.table_name)
|
|
40
|
+
# `join` first, then use `where`s
|
|
41
|
+
relation =
|
|
42
|
+
if ass.belongs_to?
|
|
43
|
+
relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.association_primary_key} = #{quoted_table_name}.#{ass.association_foreign_key}")
|
|
44
|
+
else
|
|
45
|
+
relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.foreign_key} = #{quoted_table_name}.#{primary_key}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
27
48
|
end
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
|
|
50
|
+
relation_order =
|
|
51
|
+
if options[:reverse]
|
|
52
|
+
'DESC'
|
|
53
|
+
else
|
|
54
|
+
'ASC'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
relation = relation.order("ts #{relation_order}")
|
|
31
58
|
|
|
32
|
-
relation = relation.from(
|
|
59
|
+
relation = relation.from("public.#{quoted_table_name}") unless chrono?
|
|
33
60
|
relation = relation.where(id: rid) if rid
|
|
34
61
|
|
|
35
|
-
sql = "SELECT ts FROM ( #{relation.to_sql} ) foo WHERE ts IS NOT NULL"
|
|
62
|
+
sql = +"SELECT ts FROM ( #{relation.to_sql} ) AS foo WHERE ts IS NOT NULL"
|
|
36
63
|
|
|
37
64
|
if options.key?(:before)
|
|
38
65
|
sql << " AND ts < '#{Conversions.time_to_utc_string(options[:before])}'"
|
|
39
66
|
end
|
|
40
67
|
|
|
41
68
|
if options.key?(:after)
|
|
42
|
-
sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after
|
|
69
|
+
sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after])}'"
|
|
43
70
|
end
|
|
44
71
|
|
|
45
72
|
if rid && !options[:with]
|
|
46
|
-
sql <<
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
sql <<
|
|
74
|
+
if chrono?
|
|
75
|
+
%{ AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} ) }
|
|
76
|
+
else
|
|
77
|
+
%[ AND ts < NOW() ]
|
|
78
|
+
end
|
|
49
79
|
end
|
|
50
80
|
|
|
51
81
|
sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)
|
|
52
82
|
|
|
53
|
-
sql.gsub! 'INNER JOIN', 'LEFT OUTER JOIN'
|
|
54
|
-
|
|
55
83
|
connection.on_schema(Adapter::HISTORY_SCHEMA) do
|
|
56
|
-
connection.select_values(sql, "#{
|
|
84
|
+
connection.select_values(sql, "#{name} periods").map! do |ts|
|
|
57
85
|
Conversions.string_to_utc_time ts
|
|
58
86
|
end
|
|
59
87
|
end
|
|
@@ -74,21 +102,17 @@ module ChronoModel
|
|
|
74
102
|
def timeline_associations_from(names)
|
|
75
103
|
Array.wrap(names).map do |name|
|
|
76
104
|
reflect_on_association(name) or raise ArgumentError,
|
|
77
|
-
|
|
105
|
+
"No association found for name `#{name}'"
|
|
78
106
|
end
|
|
79
107
|
end
|
|
80
108
|
|
|
81
109
|
def quoted_history_fields
|
|
82
110
|
@quoted_history_fields ||= begin
|
|
83
|
-
validity =
|
|
84
|
-
[connection.quote_table_name(table_name),
|
|
85
|
-
connection.quote_column_name('validity')
|
|
86
|
-
].join('.')
|
|
111
|
+
validity = "#{quoted_table_name}.#{connection.quote_column_name('validity')}"
|
|
87
112
|
|
|
88
|
-
[
|
|
113
|
+
%w[lower upper].map! { |func| "#{func}(#{validity})" }
|
|
89
114
|
end
|
|
90
115
|
end
|
|
91
116
|
end
|
|
92
|
-
|
|
93
117
|
end
|
|
94
118
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'chrono_model/time_machine/time_query'
|
|
2
4
|
require 'chrono_model/time_machine/timeline'
|
|
3
5
|
require 'chrono_model/time_machine/history_model'
|
|
4
6
|
|
|
5
7
|
module ChronoModel
|
|
6
|
-
|
|
7
8
|
module TimeMachine
|
|
8
9
|
include ChronoModel::Patches::AsOfTimeHolder
|
|
9
10
|
|
|
@@ -11,20 +12,41 @@ module ChronoModel
|
|
|
11
12
|
|
|
12
13
|
included do
|
|
13
14
|
if table_exists? && !chrono?
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
logger.warn <<-MSG.squish
|
|
16
|
+
ChronoModel: #{table_name} is not a temporal table.
|
|
17
|
+
Please use `change_table :#{table_name}, temporal: true` in a migration.
|
|
18
|
+
MSG
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
history = ChronoModel::TimeMachine.define_history_model_for(self)
|
|
19
22
|
ChronoModel.history_models[table_name] = history
|
|
20
23
|
|
|
21
24
|
class << self
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
def subclasses(with_history: false)
|
|
26
|
+
subclasses = super()
|
|
27
|
+
subclasses.reject!(&:history?) unless with_history
|
|
28
|
+
subclasses
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def subclasses_with_history
|
|
32
|
+
subclasses(with_history: true)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# `direct_descendants` is deprecated method in 7.0 and has been
|
|
36
|
+
# removed in 7.1
|
|
37
|
+
if method_defined?(:direct_descendants)
|
|
38
|
+
alias_method :direct_descendants_with_history, :subclasses_with_history
|
|
39
|
+
alias_method :direct_descendants, :subclasses
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Ruby 3.1 has a native subclasses method and descendants is
|
|
43
|
+
# implemented with recursion of subclasses
|
|
44
|
+
if Class.method_defined?(:subclasses)
|
|
45
|
+
def descendants_with_history
|
|
46
|
+
subclasses_with_history.concat(subclasses.flat_map(&:descendants_with_history))
|
|
47
|
+
end
|
|
25
48
|
end
|
|
26
49
|
|
|
27
|
-
alias_method :descendants_with_history, :descendants
|
|
28
50
|
def descendants
|
|
29
51
|
descendants_with_history.reject(&:history?)
|
|
30
52
|
end
|
|
@@ -43,9 +65,9 @@ module ChronoModel
|
|
|
43
65
|
# Sadly, we can't avoid it by calling +.history?+, because in the
|
|
44
66
|
# subclass the HistoryModel hasn't been included yet.
|
|
45
67
|
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
return if subclass.name.nil?
|
|
69
|
+
|
|
70
|
+
ChronoModel::TimeMachine.define_history_model_for(subclass)
|
|
49
71
|
end
|
|
50
72
|
end
|
|
51
73
|
end
|
|
@@ -71,21 +93,23 @@ module ChronoModel
|
|
|
71
93
|
|
|
72
94
|
# Returns an ActiveRecord::Relation on the history of this model as
|
|
73
95
|
# it was +time+ ago.
|
|
74
|
-
|
|
75
|
-
history.as_of(time)
|
|
76
|
-
end
|
|
96
|
+
delegate :as_of, to: :history
|
|
77
97
|
|
|
78
98
|
def attribute_names_for_history_changes
|
|
79
99
|
@attribute_names_for_history_changes ||= attribute_names -
|
|
80
|
-
|
|
100
|
+
%w[id hid validity recorded_at]
|
|
81
101
|
end
|
|
82
102
|
|
|
83
103
|
def has_timeline(options)
|
|
84
104
|
changes = options.delete(:changes)
|
|
85
105
|
assocs = history.has_timeline(options)
|
|
86
106
|
|
|
87
|
-
attributes =
|
|
88
|
-
|
|
107
|
+
attributes =
|
|
108
|
+
if changes.present?
|
|
109
|
+
Array.wrap(changes)
|
|
110
|
+
else
|
|
111
|
+
assocs.map(&:name)
|
|
112
|
+
end
|
|
89
113
|
|
|
90
114
|
attribute_names_for_history_changes.concat(attributes.map(&:to_s))
|
|
91
115
|
end
|
|
@@ -112,7 +136,7 @@ module ChronoModel
|
|
|
112
136
|
# reasons, to avoid a `rescue` (@lleirborras).
|
|
113
137
|
#
|
|
114
138
|
def _as_of(time)
|
|
115
|
-
self.class.as_of(time).where(id:
|
|
139
|
+
self.class.as_of(time).where(id: id)
|
|
116
140
|
end
|
|
117
141
|
protected :_as_of
|
|
118
142
|
|
|
@@ -132,13 +156,14 @@ module ChronoModel
|
|
|
132
156
|
# Returns a boolean indicating whether this record is an history entry.
|
|
133
157
|
#
|
|
134
158
|
def historical?
|
|
135
|
-
|
|
159
|
+
as_of_time.present?
|
|
136
160
|
end
|
|
137
161
|
|
|
138
162
|
# Inhibit destroy of historical records
|
|
139
163
|
#
|
|
140
164
|
def destroy
|
|
141
165
|
raise ActiveRecord::ReadOnlyRecord, 'Cannot delete historical records' if historical?
|
|
166
|
+
|
|
142
167
|
super
|
|
143
168
|
end
|
|
144
169
|
|
|
@@ -147,7 +172,7 @@ module ChronoModel
|
|
|
147
172
|
#
|
|
148
173
|
def pred(options = {})
|
|
149
174
|
if self.class.timeline_associations.empty?
|
|
150
|
-
history.
|
|
175
|
+
history.reverse_order.second
|
|
151
176
|
else
|
|
152
177
|
return nil unless (ts = pred_timestamp(options))
|
|
153
178
|
|
|
@@ -178,7 +203,11 @@ module ChronoModel
|
|
|
178
203
|
# Returns the current history version
|
|
179
204
|
#
|
|
180
205
|
def current_version
|
|
181
|
-
|
|
206
|
+
if historical?
|
|
207
|
+
self.class.find(id)
|
|
208
|
+
else
|
|
209
|
+
self
|
|
210
|
+
end
|
|
182
211
|
end
|
|
183
212
|
|
|
184
213
|
# Returns the differences between this entry and the previous history one.
|
|
@@ -196,17 +225,20 @@ module ChronoModel
|
|
|
196
225
|
#
|
|
197
226
|
def changes_against(ref)
|
|
198
227
|
self.class.attribute_names_for_history_changes.inject({}) do |changes, attr|
|
|
199
|
-
old
|
|
228
|
+
old = ref.public_send(attr)
|
|
229
|
+
new = public_send(attr)
|
|
200
230
|
|
|
201
231
|
changes.tap do |c|
|
|
202
|
-
changed =
|
|
203
|
-
|
|
232
|
+
changed =
|
|
233
|
+
if old.respond_to?(:history_eql?)
|
|
234
|
+
!old.history_eql?(new)
|
|
235
|
+
else
|
|
236
|
+
old != new
|
|
237
|
+
end
|
|
204
238
|
|
|
205
239
|
c[attr] = [old, new] if changed
|
|
206
240
|
end
|
|
207
241
|
end
|
|
208
242
|
end
|
|
209
|
-
|
|
210
243
|
end
|
|
211
|
-
|
|
212
244
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module ChronoModel
|
|
3
4
|
module Utilities
|
|
4
5
|
# Amends the given history item setting a different period.
|
|
5
6
|
# Useful when migrating from legacy systems.
|
|
@@ -11,7 +12,7 @@ module ChronoModel
|
|
|
11
12
|
# end
|
|
12
13
|
#
|
|
13
14
|
def amend_period!(hid, from, to)
|
|
14
|
-
unless [from, to].any? {|ts| ts.respond_to?(:zone) && ts.zone == 'UTC'}
|
|
15
|
+
unless [from, to].any? { |ts| ts.respond_to?(:zone) && ts.zone == 'UTC' }
|
|
15
16
|
raise 'Can amend history only with UTC timestamps'
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -23,5 +24,4 @@ module ChronoModel
|
|
|
23
24
|
]
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
|
-
|
|
27
27
|
end
|
data/lib/chrono_model/version.rb
CHANGED