chrono_model 1.2.2 → 3.0.1
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 +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
data/lib/chrono_model/adapter.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
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
|
+
require 'chrono_model/adapter/migrations_modules/stable'
|
7
|
+
|
4
8
|
require 'chrono_model/adapter/ddl'
|
5
9
|
require 'chrono_model/adapter/indexes'
|
6
10
|
require 'chrono_model/adapter/tsrange'
|
7
11
|
require 'chrono_model/adapter/upgrade'
|
8
12
|
|
9
13
|
module ChronoModel
|
10
|
-
|
11
14
|
# This class implements all ActiveRecord::ConnectionAdapters::SchemaStatements
|
12
15
|
# methods adding support for temporal extensions. It inherits from the Postgres
|
13
16
|
# adapter for a clean override of its methods using super.
|
@@ -25,12 +28,28 @@ module ChronoModel
|
|
25
28
|
# The schema holding historical data
|
26
29
|
HISTORY_SCHEMA = 'history'
|
27
30
|
|
31
|
+
if ActiveRecord::VERSION::STRING >= '7.1'
|
32
|
+
def initialize(*)
|
33
|
+
super
|
34
|
+
|
35
|
+
connect!
|
36
|
+
|
37
|
+
unless chrono_supported?
|
38
|
+
raise ChronoModel::Error, 'Your database server is not supported by ChronoModel. ' \
|
39
|
+
'Currently, only PostgreSQL >= 9.3 is supported.'
|
40
|
+
end
|
41
|
+
|
42
|
+
chrono_setup!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
28
46
|
# Returns true whether the connection adapter supports our
|
29
47
|
# implementation of temporal tables. Currently, Chronomodel
|
30
|
-
# is supported starting with PostgreSQL 9.3
|
48
|
+
# is supported starting with PostgreSQL 9.3 (90300 in PostgreSQL's
|
49
|
+
# `PG_VERSION_NUM` numeric format).
|
31
50
|
#
|
32
51
|
def chrono_supported?
|
33
|
-
postgresql_version >= 90300
|
52
|
+
postgresql_version >= 90300 # rubocop:disable Style/NumericLiterals
|
34
53
|
end
|
35
54
|
|
36
55
|
def chrono_setup!
|
@@ -53,13 +72,13 @@ module ChronoModel
|
|
53
72
|
#
|
54
73
|
# NOTE: These methods are dynamically defined, see the source.
|
55
74
|
#
|
56
|
-
def primary_key(table_name)
|
57
|
-
end
|
75
|
+
def primary_key(table_name); end
|
58
76
|
|
59
|
-
[
|
77
|
+
%i[primary_key indexes default_sequence_name].each do |method|
|
60
78
|
define_method(method) do |*args|
|
61
79
|
table_name = args.first
|
62
80
|
return super(*args) unless is_chrono?(table_name)
|
81
|
+
|
63
82
|
on_schema(TEMPORAL_SCHEMA, recurse: :ignore) { super(*args) }
|
64
83
|
end
|
65
84
|
end
|
@@ -73,12 +92,12 @@ module ChronoModel
|
|
73
92
|
#
|
74
93
|
# NOTE: This method is dynamically defined, see the source.
|
75
94
|
#
|
76
|
-
def column_definitions
|
77
|
-
end
|
95
|
+
def column_definitions; end
|
78
96
|
|
79
97
|
define_method(:column_definitions) do |table_name|
|
80
98
|
return super(table_name) unless is_chrono?(table_name)
|
81
|
-
|
99
|
+
|
100
|
+
on_schema("#{TEMPORAL_SCHEMA},#{schema_search_path}", recurse: :ignore) { super(table_name) }
|
82
101
|
end
|
83
102
|
|
84
103
|
# Evaluates the given block in the temporal schema.
|
@@ -101,16 +120,15 @@ module ChronoModel
|
|
101
120
|
# See specs for examples and behaviour.
|
102
121
|
#
|
103
122
|
def on_schema(schema, recurse: :follow)
|
104
|
-
old_path =
|
123
|
+
old_path = schema_search_path
|
105
124
|
|
106
125
|
count_recursions do
|
107
|
-
if recurse == :follow
|
126
|
+
if (recurse == :follow) || (Thread.current['recursions'] == 1)
|
108
127
|
self.schema_search_path = schema
|
109
128
|
end
|
110
129
|
|
111
130
|
yield
|
112
131
|
end
|
113
|
-
|
114
132
|
ensure
|
115
133
|
# If the transaction is aborted, any execute() call will raise
|
116
134
|
# "transaction is aborted errors" - thus calling the Adapter's
|
@@ -121,7 +139,7 @@ module ChronoModel
|
|
121
139
|
# transaction ends.
|
122
140
|
#
|
123
141
|
transaction_aborted =
|
124
|
-
|
142
|
+
chrono_connection.transaction_status == PG::Connection::PQTRANS_INERROR
|
125
143
|
|
126
144
|
if transaction_aborted && Thread.current['recursions'] == 1
|
127
145
|
@schema_search_path = nil
|
@@ -143,7 +161,8 @@ module ChronoModel
|
|
143
161
|
def chrono_metadata_for(view_name)
|
144
162
|
comment = select_value(
|
145
163
|
"SELECT obj_description(#{quote(view_name)}::regclass)",
|
146
|
-
"ChronoModel metadata for #{view_name}"
|
164
|
+
"ChronoModel metadata for #{view_name}"
|
165
|
+
) if data_source_exists?(view_name)
|
147
166
|
|
148
167
|
MultiJson.load(comment || '{}').with_indifferent_access
|
149
168
|
end
|
@@ -153,29 +172,38 @@ module ChronoModel
|
|
153
172
|
def chrono_metadata_set(view_name, metadata)
|
154
173
|
comment = MultiJson.dump(metadata)
|
155
174
|
|
156
|
-
execute %
|
175
|
+
execute %( COMMENT ON VIEW #{view_name} IS #{quote(comment)} )
|
176
|
+
end
|
177
|
+
|
178
|
+
def valid_table_definition_options
|
179
|
+
super + %i[temporal journal no_journal full_journal]
|
157
180
|
end
|
158
181
|
|
159
182
|
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
183
|
|
166
|
-
|
184
|
+
# Rails 7.1 uses `@raw_connection`, older versions use `@connection`
|
185
|
+
#
|
186
|
+
def chrono_connection
|
187
|
+
@chrono_connection ||= @raw_connection || @connection
|
188
|
+
end
|
167
189
|
|
168
|
-
|
169
|
-
|
170
|
-
|
190
|
+
# Counts the number of recursions in a thread local variable
|
191
|
+
#
|
192
|
+
def count_recursions # yield
|
193
|
+
Thread.current['recursions'] ||= 0
|
194
|
+
Thread.current['recursions'] += 1
|
171
195
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
196
|
+
yield
|
197
|
+
ensure
|
198
|
+
Thread.current['recursions'] -= 1
|
199
|
+
end
|
200
|
+
|
201
|
+
# Create the temporal and history schemas, unless they already exist
|
202
|
+
#
|
203
|
+
def chrono_ensure_schemas
|
204
|
+
[TEMPORAL_SCHEMA, HISTORY_SCHEMA].each do |schema|
|
205
|
+
execute "CREATE SCHEMA #{schema}" unless schema_exists?(schema)
|
178
206
|
end
|
207
|
+
end
|
179
208
|
end
|
180
|
-
|
181
209
|
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
7
|
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/
|
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_fs(:db) << '.' << format('%06d', time.usec)
|
17
24
|
end
|
18
25
|
end
|
19
|
-
|
20
26
|
end
|
@@ -1,19 +1,9 @@
|
|
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
|
-
# With AR 5.2 a simple relation can be used, as the only required argument
|
8
|
-
# is the model. 5.0 and 5.1 require more arguments, that are passed here.
|
9
|
-
#
|
10
|
-
class AsOfTimeRelation < ActiveRecord::Relation
|
11
|
-
if ActiveRecord::VERSION::STRING.to_f < 5.2
|
12
|
-
def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
|
13
|
-
super(klass, table, predicate_builder, values)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
7
|
+
class AsOfTimeRelation < ActiveRecord::Relation; end
|
18
8
|
end
|
19
9
|
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
|
@@ -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,32 @@
|
|
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
|
+
def preload_associations(records) # :nodoc:
|
9
|
+
preload = preload_values
|
10
|
+
preload += includes_values unless eager_loading?
|
11
|
+
scope = StrictLoadingScope if strict_loading_value
|
12
|
+
|
13
|
+
preload.each do |associations|
|
14
|
+
ActiveRecord::Associations::Preloader.new(
|
15
|
+
records: records, associations: associations, scope: scope, model: model, as_of_time: as_of_time
|
16
|
+
).call
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def empty_scope?
|
21
|
+
return super unless @_as_of_time
|
22
|
+
|
23
|
+
@values == klass.unscoped.as_of(as_of_time).values
|
24
|
+
end
|
25
|
+
|
7
26
|
def load
|
8
27
|
return super unless @_as_of_time && !loaded?
|
9
28
|
|
10
|
-
super.each {|record| record.as_of_time!(@_as_of_time) }
|
29
|
+
super.each { |record| record.as_of_time!(@_as_of_time) }
|
11
30
|
end
|
12
31
|
|
13
32
|
def merge(*)
|
@@ -20,11 +39,9 @@ module ChronoModel
|
|
20
39
|
return super unless @_as_of_time
|
21
40
|
|
22
41
|
super.tap do |arel|
|
23
|
-
|
24
42
|
arel.join_sources.each do |join|
|
25
43
|
chrono_join_history(join)
|
26
44
|
end
|
27
|
-
|
28
45
|
end
|
29
46
|
end
|
30
47
|
|
@@ -37,11 +54,18 @@ module ChronoModel
|
|
37
54
|
#
|
38
55
|
return if join.left.respond_to?(:as_of_time)
|
39
56
|
|
40
|
-
model =
|
57
|
+
model =
|
58
|
+
if join.left.respond_to?(:table_name)
|
59
|
+
ChronoModel.history_models[join.left.table_name]
|
60
|
+
else
|
61
|
+
ChronoModel.history_models[join.left]
|
62
|
+
end
|
63
|
+
|
41
64
|
return unless model
|
42
65
|
|
43
66
|
join.left = ChronoModel::Patches::JoinNode.new(
|
44
|
-
join.left, model.history, @_as_of_time
|
67
|
+
join.left, model.history, @_as_of_time
|
68
|
+
)
|
45
69
|
end
|
46
70
|
|
47
71
|
# Build a preloader at the +as_of_time+ of this relation.
|
@@ -49,10 +73,29 @@ module ChronoModel
|
|
49
73
|
#
|
50
74
|
def build_preloader
|
51
75
|
ActiveRecord::Associations::Preloader.new(
|
52
|
-
model:
|
76
|
+
model: model, as_of_time: as_of_time
|
53
77
|
)
|
54
78
|
end
|
55
|
-
end
|
56
79
|
|
80
|
+
def find_nth(*)
|
81
|
+
return super unless try(:history?)
|
82
|
+
|
83
|
+
with_hid_pkey { super }
|
84
|
+
end
|
85
|
+
|
86
|
+
def last(*)
|
87
|
+
return super unless try(:history?)
|
88
|
+
|
89
|
+
with_hid_pkey { super }
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def ordered_relation
|
95
|
+
return super unless try(:history?)
|
96
|
+
|
97
|
+
with_hid_pkey { super }
|
98
|
+
end
|
99
|
+
end
|
57
100
|
end
|
58
101
|
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'
|
data/lib/chrono_model/railtie.rb
CHANGED
@@ -1,45 +1,31 @@
|
|
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
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
raise 'In order to use ChronoModel, config.active_record.schema_format must be :sql!'
|
9
|
-
end
|
10
|
-
|
11
|
-
tasks_class = ActiveRecord::Tasks::ChronomodelDatabaseTasks
|
12
|
-
|
13
|
-
# Register our database tasks under our adapter name
|
14
|
-
#
|
15
|
-
ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, tasks_class)
|
9
|
+
# Register our database tasks under our adapter name
|
10
|
+
ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS.to_s)
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
Rake::Task['db:schema:load'].clear.enhance(['environment']) do
|
23
|
-
Rake::Task['db:structure:load'].invoke
|
12
|
+
rake_tasks do
|
13
|
+
def task_config
|
14
|
+
ActiveRecord::Base.connection_db_config
|
24
15
|
end
|
25
16
|
|
26
|
-
desc
|
17
|
+
desc 'Dumps database into db/data.NOW.sql or file specified via DUMP='
|
27
18
|
task 'db:data:dump' => :environment do
|
28
|
-
config = ActiveRecord::Tasks::DatabaseTasks.current_config
|
29
19
|
target = ENV['DUMP'] || Rails.root.join('db', "data.#{Time.now.to_f}.sql")
|
30
|
-
|
31
|
-
tasks_class.new(config).data_dump(target)
|
20
|
+
TASKS_CLASS.new(task_config).data_dump(target)
|
32
21
|
end
|
33
22
|
|
34
|
-
desc
|
23
|
+
desc 'Loads database dump from file specified via DUMP='
|
35
24
|
task 'db:data:load' => :environment do
|
36
|
-
config = ActiveRecord::Tasks::DatabaseTasks.current_config
|
37
25
|
source = ENV['DUMP'].presence or
|
38
|
-
raise ArgumentError,
|
39
|
-
|
40
|
-
tasks_class.new(config).data_load(source)
|
26
|
+
raise ArgumentError, 'Invoke as rake db:data:load DUMP=/path/to/data.sql'
|
27
|
+
TASKS_CLASS.new(task_config).data_load(source)
|
41
28
|
end
|
42
29
|
end
|
43
|
-
|
44
30
|
end
|
45
31
|
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
|