chrono_model 1.2.2 → 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/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/adapter.rb
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'active_record/connection_adapters/postgresql_adapter'
|
|
2
4
|
|
|
3
5
|
require 'chrono_model/adapter/migrations'
|
|
6
|
+
|
|
7
|
+
if ActiveRecord::VERSION::STRING >= '6.1'
|
|
8
|
+
require 'chrono_model/adapter/migrations_modules/stable'
|
|
9
|
+
else
|
|
10
|
+
require 'chrono_model/adapter/migrations_modules/legacy'
|
|
11
|
+
end
|
|
12
|
+
|
|
4
13
|
require 'chrono_model/adapter/ddl'
|
|
5
14
|
require 'chrono_model/adapter/indexes'
|
|
6
15
|
require 'chrono_model/adapter/tsrange'
|
|
7
16
|
require 'chrono_model/adapter/upgrade'
|
|
8
17
|
|
|
9
18
|
module ChronoModel
|
|
10
|
-
|
|
11
19
|
# This class implements all ActiveRecord::ConnectionAdapters::SchemaStatements
|
|
12
20
|
# methods adding support for temporal extensions. It inherits from the Postgres
|
|
13
21
|
# adapter for a clean override of its methods using super.
|
|
@@ -25,12 +33,28 @@ module ChronoModel
|
|
|
25
33
|
# The schema holding historical data
|
|
26
34
|
HISTORY_SCHEMA = 'history'
|
|
27
35
|
|
|
36
|
+
if ActiveRecord::VERSION::STRING >= '7.1'
|
|
37
|
+
def initialize(*)
|
|
38
|
+
super
|
|
39
|
+
|
|
40
|
+
connect!
|
|
41
|
+
|
|
42
|
+
unless chrono_supported?
|
|
43
|
+
raise ChronoModel::Error, 'Your database server is not supported by ChronoModel. ' \
|
|
44
|
+
'Currently, only PostgreSQL >= 9.3 is supported.'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
chrono_setup!
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
28
51
|
# Returns true whether the connection adapter supports our
|
|
29
52
|
# implementation of temporal tables. Currently, Chronomodel
|
|
30
|
-
# is supported starting with PostgreSQL 9.3
|
|
53
|
+
# is supported starting with PostgreSQL 9.3 (90300 in PostgreSQL's
|
|
54
|
+
# `PG_VERSION_NUM` numeric format).
|
|
31
55
|
#
|
|
32
56
|
def chrono_supported?
|
|
33
|
-
postgresql_version >= 90300
|
|
57
|
+
postgresql_version >= 90300 # rubocop:disable Style/NumericLiterals
|
|
34
58
|
end
|
|
35
59
|
|
|
36
60
|
def chrono_setup!
|
|
@@ -53,13 +77,13 @@ module ChronoModel
|
|
|
53
77
|
#
|
|
54
78
|
# NOTE: These methods are dynamically defined, see the source.
|
|
55
79
|
#
|
|
56
|
-
def primary_key(table_name)
|
|
57
|
-
end
|
|
80
|
+
def primary_key(table_name); end
|
|
58
81
|
|
|
59
|
-
[
|
|
82
|
+
%i[primary_key indexes default_sequence_name].each do |method|
|
|
60
83
|
define_method(method) do |*args|
|
|
61
84
|
table_name = args.first
|
|
62
85
|
return super(*args) unless is_chrono?(table_name)
|
|
86
|
+
|
|
63
87
|
on_schema(TEMPORAL_SCHEMA, recurse: :ignore) { super(*args) }
|
|
64
88
|
end
|
|
65
89
|
end
|
|
@@ -73,12 +97,12 @@ module ChronoModel
|
|
|
73
97
|
#
|
|
74
98
|
# NOTE: This method is dynamically defined, see the source.
|
|
75
99
|
#
|
|
76
|
-
def column_definitions
|
|
77
|
-
end
|
|
100
|
+
def column_definitions; end
|
|
78
101
|
|
|
79
102
|
define_method(:column_definitions) do |table_name|
|
|
80
103
|
return super(table_name) unless is_chrono?(table_name)
|
|
81
|
-
|
|
104
|
+
|
|
105
|
+
on_schema("#{TEMPORAL_SCHEMA},#{schema_search_path}", recurse: :ignore) { super(table_name) }
|
|
82
106
|
end
|
|
83
107
|
|
|
84
108
|
# Evaluates the given block in the temporal schema.
|
|
@@ -101,16 +125,15 @@ module ChronoModel
|
|
|
101
125
|
# See specs for examples and behaviour.
|
|
102
126
|
#
|
|
103
127
|
def on_schema(schema, recurse: :follow)
|
|
104
|
-
old_path =
|
|
128
|
+
old_path = schema_search_path
|
|
105
129
|
|
|
106
130
|
count_recursions do
|
|
107
|
-
if recurse == :follow
|
|
131
|
+
if (recurse == :follow) || (Thread.current['recursions'] == 1)
|
|
108
132
|
self.schema_search_path = schema
|
|
109
133
|
end
|
|
110
134
|
|
|
111
135
|
yield
|
|
112
136
|
end
|
|
113
|
-
|
|
114
137
|
ensure
|
|
115
138
|
# If the transaction is aborted, any execute() call will raise
|
|
116
139
|
# "transaction is aborted errors" - thus calling the Adapter's
|
|
@@ -121,7 +144,7 @@ module ChronoModel
|
|
|
121
144
|
# transaction ends.
|
|
122
145
|
#
|
|
123
146
|
transaction_aborted =
|
|
124
|
-
|
|
147
|
+
chrono_connection.transaction_status == PG::Connection::PQTRANS_INERROR
|
|
125
148
|
|
|
126
149
|
if transaction_aborted && Thread.current['recursions'] == 1
|
|
127
150
|
@schema_search_path = nil
|
|
@@ -143,7 +166,8 @@ module ChronoModel
|
|
|
143
166
|
def chrono_metadata_for(view_name)
|
|
144
167
|
comment = select_value(
|
|
145
168
|
"SELECT obj_description(#{quote(view_name)}::regclass)",
|
|
146
|
-
"ChronoModel metadata for #{view_name}"
|
|
169
|
+
"ChronoModel metadata for #{view_name}"
|
|
170
|
+
) if data_source_exists?(view_name)
|
|
147
171
|
|
|
148
172
|
MultiJson.load(comment || '{}').with_indifferent_access
|
|
149
173
|
end
|
|
@@ -153,29 +177,38 @@ module ChronoModel
|
|
|
153
177
|
def chrono_metadata_set(view_name, metadata)
|
|
154
178
|
comment = MultiJson.dump(metadata)
|
|
155
179
|
|
|
156
|
-
execute %
|
|
180
|
+
execute %( COMMENT ON VIEW #{view_name} IS #{quote(comment)} )
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def valid_table_definition_options
|
|
184
|
+
super + %i[temporal journal no_journal full_journal]
|
|
157
185
|
end
|
|
158
186
|
|
|
159
187
|
private
|
|
160
|
-
# Counts the number of recursions in a thread local variable
|
|
161
|
-
#
|
|
162
|
-
def count_recursions # yield
|
|
163
|
-
Thread.current['recursions'] ||= 0
|
|
164
|
-
Thread.current['recursions'] += 1
|
|
165
188
|
|
|
166
|
-
|
|
189
|
+
# Rails 7.1 uses `@raw_connection`, older versions use `@connection`
|
|
190
|
+
#
|
|
191
|
+
def chrono_connection
|
|
192
|
+
@chrono_connection ||= @raw_connection || @connection
|
|
193
|
+
end
|
|
167
194
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
195
|
+
# Counts the number of recursions in a thread local variable
|
|
196
|
+
#
|
|
197
|
+
def count_recursions # yield
|
|
198
|
+
Thread.current['recursions'] ||= 0
|
|
199
|
+
Thread.current['recursions'] += 1
|
|
171
200
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
201
|
+
yield
|
|
202
|
+
ensure
|
|
203
|
+
Thread.current['recursions'] -= 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Create the temporal and history schemas, unless they already exist
|
|
207
|
+
#
|
|
208
|
+
def chrono_ensure_schemas
|
|
209
|
+
[TEMPORAL_SCHEMA, HISTORY_SCHEMA].each do |schema|
|
|
210
|
+
execute "CREATE SCHEMA #{schema}" unless schema_exists?(schema)
|
|
178
211
|
end
|
|
212
|
+
end
|
|
179
213
|
end
|
|
180
|
-
|
|
181
214
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChronoModel
|
|
4
|
+
# A module to add to ActiveRecord::Base to check if they are backed by
|
|
5
|
+
# temporal tables.
|
|
6
|
+
module Chrono
|
|
7
|
+
# Checks whether this Active Record model is backed by a temporal table
|
|
8
|
+
#
|
|
9
|
+
# @return [Boolean] false if the connection does not respond to is_chrono?
|
|
10
|
+
# the result of connection.is_chrono?(table_name) otherwise
|
|
11
|
+
def chrono?
|
|
12
|
+
return false unless connection.respond_to? :is_chrono?
|
|
13
|
+
|
|
14
|
+
connection.is_chrono?(table_name)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module ChronoModel
|
|
3
4
|
module Conversions
|
|
4
|
-
|
|
5
|
+
module_function
|
|
5
6
|
|
|
6
|
-
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z
|
|
7
|
+
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/.freeze
|
|
7
8
|
|
|
9
|
+
# rubocop:disable Style/PerlBackrefs
|
|
8
10
|
def string_to_utc_time(string)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
return string if string.is_a?(Time)
|
|
12
|
+
|
|
13
|
+
return unless string =~ ISO_DATETIME
|
|
14
|
+
|
|
15
|
+
# .1 is .100000, not .000001
|
|
16
|
+
usec = $7.ljust(6, '0') unless $7.nil?
|
|
17
|
+
|
|
18
|
+
Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec.to_i
|
|
13
19
|
end
|
|
20
|
+
# rubocop:enable Style/PerlBackrefs
|
|
14
21
|
|
|
15
22
|
def time_to_utc_string(time)
|
|
16
|
-
|
|
23
|
+
time.to_formatted_s(:db) << '.' << format('%06d', time.usec)
|
|
17
24
|
end
|
|
18
25
|
end
|
|
19
|
-
|
|
20
26
|
end
|
data/lib/chrono_model/json.rb
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module ChronoModel
|
|
3
4
|
module Json
|
|
4
5
|
extend self
|
|
5
6
|
|
|
6
7
|
def create
|
|
7
|
-
|
|
8
|
+
ActiveSupport::Deprecation.warn <<-MSG.squish
|
|
9
|
+
ChronoModel: JSON ops are deprecated. Please migrate to JSONB.
|
|
10
|
+
MSG
|
|
8
11
|
|
|
9
12
|
adapter.execute 'CREATE OR REPLACE LANGUAGE plpythonu'
|
|
10
|
-
adapter.execute File.read(sql
|
|
13
|
+
adapter.execute File.read(sql('json_ops.sql'))
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def drop
|
|
14
|
-
adapter.execute File.read(sql
|
|
17
|
+
adapter.execute File.read(sql('uninstall-json_ops.sql'))
|
|
15
18
|
adapter.execute 'DROP LANGUAGE IF EXISTS plpythonu'
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
private
|
|
22
|
+
|
|
19
23
|
def sql(file)
|
|
20
|
-
File.dirname(__FILE__)
|
|
24
|
+
"#{File.dirname(__FILE__)}/../../sql/#{file}"
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def adapter
|
|
24
28
|
ActiveRecord::Base.connection
|
|
25
29
|
end
|
|
26
30
|
end
|
|
27
|
-
|
|
28
31
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module Patches
|
|
3
|
-
|
|
4
5
|
# This class is a dummy relation whose scope is only to pass around the
|
|
5
6
|
# as_of_time parameters across ActiveRecord call chains.
|
|
6
7
|
#
|
|
@@ -14,6 +15,5 @@ module ChronoModel
|
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
|
-
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module Patches
|
|
3
|
-
|
|
4
5
|
# Patches ActiveRecord::Associations::Association to add support for
|
|
5
6
|
# temporal associations.
|
|
6
7
|
#
|
|
@@ -30,23 +31,25 @@ module ChronoModel
|
|
|
30
31
|
|
|
31
32
|
scope.as_of_time!(owner.as_of_time)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
scope
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
private
|
|
37
|
-
def _chrono_record?
|
|
38
|
-
owner.respond_to?(:as_of_time) && owner.as_of_time.present?
|
|
39
|
-
end
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
reflection.klass
|
|
39
|
+
def _chrono_record?
|
|
40
|
+
owner.class.include?(ChronoModel::Patches::AsOfTimeHolder) && owner.as_of_time.present?
|
|
41
|
+
end
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
def _chrono_target?
|
|
44
|
+
@_target_klass ||=
|
|
45
|
+
if reflection.options[:polymorphic]
|
|
46
|
+
owner.public_send(reflection.foreign_type).constantize
|
|
47
|
+
else
|
|
48
|
+
reflection.klass
|
|
49
|
+
end
|
|
48
50
|
|
|
51
|
+
@_target_klass.chrono?
|
|
52
|
+
end
|
|
49
53
|
end
|
|
50
|
-
|
|
51
54
|
end
|
|
52
55
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChronoModel
|
|
4
|
+
module Patches
|
|
5
|
+
module Batches
|
|
6
|
+
module BatchEnumerator
|
|
7
|
+
def each(&block)
|
|
8
|
+
if @relation.try(:history?)
|
|
9
|
+
@relation.with_hid_pkey { super }
|
|
10
|
+
else
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module Patches
|
|
3
|
-
|
|
4
5
|
module DBConsole
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
module Config
|
|
7
|
+
def config
|
|
8
|
+
super.dup.tap { |config| config['adapter'] = 'postgresql/chronomodel' }
|
|
9
|
+
end
|
|
7
10
|
end
|
|
8
|
-
end
|
|
9
11
|
|
|
12
|
+
module DbConfig
|
|
13
|
+
def db_config
|
|
14
|
+
return @patched_db_config if @patched_db_config
|
|
15
|
+
|
|
16
|
+
original_db_config = super
|
|
17
|
+
patched_db_configuration_hash = original_db_config.configuration_hash.dup.tap do |config|
|
|
18
|
+
config[:adapter] = 'postgresql/chronomodel'
|
|
19
|
+
end
|
|
20
|
+
original_db_config.instance_variable_set :@configuration_hash, patched_db_configuration_hash
|
|
21
|
+
|
|
22
|
+
@patched_db_config = original_db_config
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
10
26
|
end
|
|
11
27
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module Patches
|
|
3
|
-
|
|
4
5
|
# This class supports the AR 5.0 code that expects to receive an
|
|
5
6
|
# Arel::Table as the left join node. We need to replace the node
|
|
6
7
|
# with a virtual table that fetches from the history at a given
|
|
@@ -21,12 +22,11 @@ module ChronoModel
|
|
|
21
22
|
|
|
22
23
|
@as_of_time = as_of_time
|
|
23
24
|
|
|
24
|
-
virtual_table = history_model
|
|
25
|
-
|
|
25
|
+
virtual_table = history_model
|
|
26
|
+
.virtual_table_at(@as_of_time, table_name: @table_alias || @table_name)
|
|
26
27
|
|
|
27
28
|
super(virtual_table)
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
|
-
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -1,18 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module Patches
|
|
3
|
-
|
|
4
5
|
# Patches ActiveRecord::Associations::Preloader to add support for
|
|
5
6
|
# temporal associations. This is tying itself to Rails internals
|
|
6
7
|
# and it is ugly :-(.
|
|
7
8
|
#
|
|
8
9
|
module Preloader
|
|
9
|
-
attr_reader :
|
|
10
|
+
attr_reader :chronomodel_options
|
|
10
11
|
|
|
11
12
|
# We overwrite the initializer in order to pass the +as_of_time+
|
|
12
13
|
# parameter above in the build_preloader
|
|
13
14
|
#
|
|
14
|
-
def initialize(options
|
|
15
|
-
@
|
|
15
|
+
def initialize(**options)
|
|
16
|
+
@chronomodel_options = options.extract!(:as_of_time, :model)
|
|
17
|
+
options[:scope] = chronomodel_scope(options[:scope]) if options.key?(:scope)
|
|
18
|
+
|
|
19
|
+
if options.empty?
|
|
20
|
+
super()
|
|
21
|
+
else
|
|
22
|
+
super(**options)
|
|
23
|
+
end
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
# Patches the AR Preloader (lib/active_record/associations/preloader.rb)
|
|
@@ -37,14 +45,20 @@ module ChronoModel
|
|
|
37
45
|
# so we use it directly.
|
|
38
46
|
#
|
|
39
47
|
def preload(records, associations, given_preload_scope = nil)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
super(records, associations, chronomodel_scope(given_preload_scope))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
def chronomodel_scope(given_preload_scope)
|
|
54
|
+
preload_scope = given_preload_scope
|
|
55
|
+
|
|
56
|
+
if chronomodel_options[:as_of_time]
|
|
57
|
+
preload_scope ||= ChronoModel::Patches::AsOfTimeRelation.new(chronomodel_options[:model])
|
|
58
|
+
preload_scope.as_of_time!(chronomodel_options[:as_of_time])
|
|
45
59
|
end
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
preload_scope
|
|
48
62
|
end
|
|
49
63
|
|
|
50
64
|
module Association
|
|
@@ -59,10 +73,26 @@ module ChronoModel
|
|
|
59
73
|
scope = scope.as_of(preload_scope.as_of_time)
|
|
60
74
|
end
|
|
61
75
|
|
|
62
|
-
|
|
76
|
+
scope
|
|
63
77
|
end
|
|
64
78
|
end
|
|
65
|
-
end
|
|
66
79
|
|
|
80
|
+
module ThroughAssociation
|
|
81
|
+
# Builds the preloader scope taking into account a potential
|
|
82
|
+
# +as_of_time+ passed down the call chain starting at the
|
|
83
|
+
# end user invocation.
|
|
84
|
+
#
|
|
85
|
+
def through_scope
|
|
86
|
+
scope = super
|
|
87
|
+
return unless scope # Rails 5.2 may not return a scope
|
|
88
|
+
|
|
89
|
+
if preload_scope.try(:as_of_time)
|
|
90
|
+
scope = scope.as_of(preload_scope.as_of_time)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
scope
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
67
97
|
end
|
|
68
98
|
end
|
|
@@ -1,13 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ChronoModel
|
|
2
4
|
module Patches
|
|
3
|
-
|
|
4
5
|
module Relation
|
|
5
6
|
include ChronoModel::Patches::AsOfTimeHolder
|
|
6
7
|
|
|
8
|
+
if ActiveRecord::Associations::Preloader.instance_methods.include?(:call)
|
|
9
|
+
def preload_associations(records) # :nodoc:
|
|
10
|
+
preload = preload_values
|
|
11
|
+
preload += includes_values unless eager_loading?
|
|
12
|
+
scope = StrictLoadingScope if strict_loading_value
|
|
13
|
+
|
|
14
|
+
preload.each do |associations|
|
|
15
|
+
ActiveRecord::Associations::Preloader.new(
|
|
16
|
+
records: records, associations: associations, scope: scope, model: model, as_of_time: as_of_time
|
|
17
|
+
).call
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def empty_scope?
|
|
23
|
+
return super unless @_as_of_time
|
|
24
|
+
|
|
25
|
+
@values == klass.unscoped.as_of(as_of_time).values
|
|
26
|
+
end
|
|
27
|
+
|
|
7
28
|
def load
|
|
8
29
|
return super unless @_as_of_time && !loaded?
|
|
9
30
|
|
|
10
|
-
super.each {|record| record.as_of_time!(@_as_of_time) }
|
|
31
|
+
super.each { |record| record.as_of_time!(@_as_of_time) }
|
|
11
32
|
end
|
|
12
33
|
|
|
13
34
|
def merge(*)
|
|
@@ -20,11 +41,9 @@ module ChronoModel
|
|
|
20
41
|
return super unless @_as_of_time
|
|
21
42
|
|
|
22
43
|
super.tap do |arel|
|
|
23
|
-
|
|
24
44
|
arel.join_sources.each do |join|
|
|
25
45
|
chrono_join_history(join)
|
|
26
46
|
end
|
|
27
|
-
|
|
28
47
|
end
|
|
29
48
|
end
|
|
30
49
|
|
|
@@ -37,11 +56,18 @@ module ChronoModel
|
|
|
37
56
|
#
|
|
38
57
|
return if join.left.respond_to?(:as_of_time)
|
|
39
58
|
|
|
40
|
-
model =
|
|
59
|
+
model =
|
|
60
|
+
if join.left.respond_to?(:table_name)
|
|
61
|
+
ChronoModel.history_models[join.left.table_name]
|
|
62
|
+
else
|
|
63
|
+
ChronoModel.history_models[join.left]
|
|
64
|
+
end
|
|
65
|
+
|
|
41
66
|
return unless model
|
|
42
67
|
|
|
43
68
|
join.left = ChronoModel::Patches::JoinNode.new(
|
|
44
|
-
join.left, model.history, @_as_of_time
|
|
69
|
+
join.left, model.history, @_as_of_time
|
|
70
|
+
)
|
|
45
71
|
end
|
|
46
72
|
|
|
47
73
|
# Build a preloader at the +as_of_time+ of this relation.
|
|
@@ -49,10 +75,29 @@ module ChronoModel
|
|
|
49
75
|
#
|
|
50
76
|
def build_preloader
|
|
51
77
|
ActiveRecord::Associations::Preloader.new(
|
|
52
|
-
model:
|
|
78
|
+
model: model, as_of_time: as_of_time
|
|
53
79
|
)
|
|
54
80
|
end
|
|
55
|
-
end
|
|
56
81
|
|
|
82
|
+
def find_nth(*)
|
|
83
|
+
return super unless try(:history?)
|
|
84
|
+
|
|
85
|
+
with_hid_pkey { super }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def last(*)
|
|
89
|
+
return super unless try(:history?)
|
|
90
|
+
|
|
91
|
+
with_hid_pkey { super }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def ordered_relation
|
|
97
|
+
return super unless try(:history?)
|
|
98
|
+
|
|
99
|
+
with_hid_pkey { super }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
57
102
|
end
|
|
58
103
|
end
|
data/lib/chrono_model/patches.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'chrono_model/patches/as_of_time_holder'
|
|
2
4
|
require 'chrono_model/patches/as_of_time_relation'
|
|
3
5
|
|
|
@@ -5,4 +7,4 @@ require 'chrono_model/patches/join_node'
|
|
|
5
7
|
require 'chrono_model/patches/relation'
|
|
6
8
|
require 'chrono_model/patches/preloader'
|
|
7
9
|
require 'chrono_model/patches/association'
|
|
8
|
-
require 'chrono_model/patches/
|
|
10
|
+
require 'chrono_model/patches/batches'
|