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
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
|