chrono_model 1.2.2 → 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/LICENSE +19 -20
- data/README.md +62 -40
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +17 -11
- data/lib/active_record/tasks/chronomodel_database_tasks.rb +64 -23
- 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/legacy.rb +41 -0
- 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 +64 -31
- data/lib/chrono_model/chrono.rb +17 -0
- data/lib/chrono_model/conversions.rb +15 -9
- data/lib/chrono_model/db_console.rb +9 -0
- data/lib/chrono_model/json.rb +9 -6
- data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
- data/lib/chrono_model/patches/as_of_time_relation.rb +2 -2
- data/lib/chrono_model/patches/association.rb +15 -12
- data/lib/chrono_model/patches/batches.rb +17 -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 +53 -8
- data/lib/chrono_model/patches.rb +3 -1
- data/lib/chrono_model/railtie.rb +29 -24
- 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 +66 -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 +39 -136
- 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/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
data/lib/chrono_model/railtie.rb
CHANGED
@@ -1,45 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_record/tasks/chronomodel_database_tasks'
|
2
4
|
|
3
5
|
module ChronoModel
|
4
6
|
class Railtie < ::Rails::Railtie
|
7
|
+
TASKS_CLASS = ActiveRecord::Tasks::ChronomodelDatabaseTasks
|
8
|
+
|
9
|
+
# Register our database tasks under our adapter name
|
10
|
+
if Rails.version < '5.2'
|
11
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS)
|
12
|
+
else
|
13
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS.to_s)
|
14
|
+
end
|
5
15
|
|
6
16
|
rake_tasks do
|
7
|
-
|
8
|
-
|
17
|
+
def task_config
|
18
|
+
if Rails.version < '6.1'
|
19
|
+
ActiveRecord::Tasks::DatabaseTasks.current_config.with_indifferent_access
|
20
|
+
else
|
21
|
+
ActiveRecord::Base.connection_db_config
|
22
|
+
end
|
9
23
|
end
|
10
24
|
|
11
|
-
|
25
|
+
if Rails.version < '6.1'
|
26
|
+
# Make schema:dump and schema:load invoke structure:dump and structure:load
|
27
|
+
Rake::Task['db:schema:dump'].clear.enhance(['environment']) do
|
28
|
+
Rake::Task['db:structure:dump'].invoke
|
29
|
+
end
|
12
30
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
# Make schema:dump and schema:load invoke structure:dump and structure:load
|
18
|
-
Rake::Task['db:schema:dump'].clear.enhance(['environment']) do
|
19
|
-
Rake::Task['db:structure:dump'].invoke
|
31
|
+
Rake::Task['db:schema:load'].clear.enhance(['environment']) do
|
32
|
+
Rake::Task['db:structure:load'].invoke
|
33
|
+
end
|
20
34
|
end
|
21
35
|
|
22
|
-
|
23
|
-
Rake::Task['db:structure:load'].invoke
|
24
|
-
end
|
25
|
-
|
26
|
-
desc "Dumps database into db/data.NOW.sql or file specified via DUMP="
|
36
|
+
desc 'Dumps database into db/data.NOW.sql or file specified via DUMP='
|
27
37
|
task 'db:data:dump' => :environment do
|
28
|
-
config = ActiveRecord::Tasks::DatabaseTasks.current_config
|
29
38
|
target = ENV['DUMP'] || Rails.root.join('db', "data.#{Time.now.to_f}.sql")
|
30
|
-
|
31
|
-
tasks_class.new(config).data_dump(target)
|
39
|
+
TASKS_CLASS.new(task_config).data_dump(target)
|
32
40
|
end
|
33
41
|
|
34
|
-
desc
|
42
|
+
desc 'Loads database dump from file specified via DUMP='
|
35
43
|
task 'db:data:load' => :environment do
|
36
|
-
config = ActiveRecord::Tasks::DatabaseTasks.current_config
|
37
44
|
source = ENV['DUMP'].presence or
|
38
|
-
raise ArgumentError,
|
39
|
-
|
40
|
-
tasks_class.new(config).data_load(source)
|
45
|
+
raise ArgumentError, 'Invoke as rake db:data:load DUMP=/path/to/data.sql'
|
46
|
+
TASKS_CLASS.new(task_config).data_load(source)
|
41
47
|
end
|
42
48
|
end
|
43
|
-
|
44
49
|
end
|
45
50
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module ChronoModel
|
3
4
|
# Provides the TimeMachine API to non-temporal models that associate
|
4
5
|
# temporal ones.
|
5
6
|
#
|
@@ -17,12 +18,11 @@ module ChronoModel
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def as_of(time)
|
20
|
-
self.class.as_of(time).where(id:
|
21
|
+
self.class.as_of(time).where(id: id).first!
|
21
22
|
end
|
22
23
|
|
23
24
|
def timeline
|
24
25
|
self.class.timeline(self)
|
25
26
|
end
|
26
27
|
end
|
27
|
-
|
28
28
|
end
|
@@ -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".dup
|
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
|