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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +19 -20
  3. data/README.md +62 -40
  4. data/lib/active_record/connection_adapters/chronomodel_adapter.rb +17 -11
  5. data/lib/active_record/tasks/chronomodel_database_tasks.rb +64 -23
  6. data/lib/chrono_model/adapter/ddl.rb +168 -153
  7. data/lib/chrono_model/adapter/indexes.rb +99 -94
  8. data/lib/chrono_model/adapter/migrations.rb +81 -104
  9. data/lib/chrono_model/adapter/migrations_modules/legacy.rb +41 -0
  10. data/lib/chrono_model/adapter/migrations_modules/stable.rb +41 -0
  11. data/lib/chrono_model/adapter/tsrange.rb +20 -5
  12. data/lib/chrono_model/adapter/upgrade.rb +89 -91
  13. data/lib/chrono_model/adapter.rb +64 -31
  14. data/lib/chrono_model/chrono.rb +17 -0
  15. data/lib/chrono_model/conversions.rb +15 -9
  16. data/lib/chrono_model/db_console.rb +9 -0
  17. data/lib/chrono_model/json.rb +9 -6
  18. data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
  19. data/lib/chrono_model/patches/as_of_time_relation.rb +2 -2
  20. data/lib/chrono_model/patches/association.rb +15 -12
  21. data/lib/chrono_model/patches/batches.rb +17 -0
  22. data/lib/chrono_model/patches/db_console.rb +20 -4
  23. data/lib/chrono_model/patches/join_node.rb +4 -4
  24. data/lib/chrono_model/patches/preloader.rb +41 -11
  25. data/lib/chrono_model/patches/relation.rb +53 -8
  26. data/lib/chrono_model/patches.rb +3 -1
  27. data/lib/chrono_model/railtie.rb +29 -24
  28. data/lib/chrono_model/time_gate.rb +3 -3
  29. data/lib/chrono_model/time_machine/history_model.rb +65 -31
  30. data/lib/chrono_model/time_machine/time_query.rb +65 -49
  31. data/lib/chrono_model/time_machine/timeline.rb +52 -28
  32. data/lib/chrono_model/time_machine.rb +66 -25
  33. data/lib/chrono_model/utilities.rb +3 -3
  34. data/lib/chrono_model/version.rb +3 -1
  35. data/lib/chrono_model.rb +31 -36
  36. metadata +39 -136
  37. data/.gitignore +0 -21
  38. data/.rspec +0 -2
  39. data/.travis.yml +0 -41
  40. data/Gemfile +0 -4
  41. data/README.sql +0 -161
  42. data/Rakefile +0 -25
  43. data/chrono_model.gemspec +0 -33
  44. data/gemfiles/rails_5.0.gemfile +0 -6
  45. data/gemfiles/rails_5.1.gemfile +0 -6
  46. data/gemfiles/rails_5.2.gemfile +0 -6
  47. data/spec/aruba/dbconsole_spec.rb +0 -25
  48. data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
  49. data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
  50. data/spec/aruba/fixtures/empty_structure.sql +0 -27
  51. data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
  52. data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
  53. data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
  54. data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
  55. data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
  56. data/spec/aruba/migrations_spec.rb +0 -48
  57. data/spec/aruba/rake_task_spec.rb +0 -71
  58. data/spec/chrono_model/adapter/base_spec.rb +0 -157
  59. data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
  60. data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
  61. data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
  62. data/spec/chrono_model/conversions_spec.rb +0 -43
  63. data/spec/chrono_model/history_models_spec.rb +0 -32
  64. data/spec/chrono_model/json_ops_spec.rb +0 -59
  65. data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
  66. data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
  67. data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
  68. data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
  69. data/spec/chrono_model/time_machine/history_spec.rb +0 -104
  70. data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
  71. data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
  72. data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
  73. data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
  74. data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
  75. data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
  76. data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
  77. data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
  78. data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
  79. data/spec/config.travis.yml +0 -5
  80. data/spec/config.yml.example +0 -9
  81. data/spec/spec_helper.rb +0 -33
  82. data/spec/support/adapter/helpers.rb +0 -53
  83. data/spec/support/adapter/structure.rb +0 -44
  84. data/spec/support/aruba.rb +0 -44
  85. data/spec/support/connection.rb +0 -70
  86. data/spec/support/matchers/base.rb +0 -56
  87. data/spec/support/matchers/column.rb +0 -99
  88. data/spec/support/matchers/function.rb +0 -79
  89. data/spec/support/matchers/index.rb +0 -69
  90. data/spec/support/matchers/schema.rb +0 -39
  91. data/spec/support/matchers/table.rb +0 -275
  92. data/spec/support/time_machine/helpers.rb +0 -47
  93. data/spec/support/time_machine/structure.rb +0 -111
  94. data/sql/json_ops.sql +0 -56
  95. data/sql/uninstall-json_ops.sql +0 -24
@@ -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
- [:primary_key, :indexes, :default_sequence_name].each do |method|
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
- on_schema(TEMPORAL_SCHEMA + ',' + self.schema_search_path, recurse: :ignore) { super(table_name) }
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 = self.schema_search_path
128
+ old_path = schema_search_path
105
129
 
106
130
  count_recursions do
107
- if recurse == :follow or Thread.current['recursions'] == 1
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
- @connection.transaction_status == PG::Connection::PQTRANS_INERROR
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}") if data_source_exists?(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 %[ COMMENT ON VIEW #{view_name} IS #{quote(comment)} ]
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
- yield
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
- ensure
169
- Thread.current['recursions'] -= 1
170
- end
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
- # Create the temporal and history schemas, unless they already exist
173
- #
174
- def chrono_ensure_schemas
175
- [TEMPORAL_SCHEMA, HISTORY_SCHEMA].each do |schema|
176
- execute "CREATE SCHEMA #{schema}" unless schema_exists?(schema)
177
- end
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
- module ChronoModel
1
+ # frozen_string_literal: true
2
2
 
3
+ module ChronoModel
3
4
  module Conversions
4
- extend self
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
- if string =~ ISO_DATETIME
10
- usec = $7.nil? ? '000000' : $7.ljust(6, '0') # .1 is .100000, not .000001
11
- Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec.to_i
12
- end
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
- [time.to_s(:db), sprintf('%06d', time.usec)].join '.'
23
+ time.to_formatted_s(:db) << '.' << format('%06d', time.usec)
17
24
  end
18
25
  end
19
-
20
26
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'chrono_model/patches/db_console'
4
+
5
+ if Rails.version < '6.1'
6
+ Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::Config
7
+ else
8
+ Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::DbConfig
9
+ end
@@ -1,28 +1,31 @@
1
- module ChronoModel
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
- puts "ChronoModel: WARNING - JSON ops are deprecated. Please migrate to JSONB"
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 'json_ops.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 'uninstall-json_ops.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__) + '/../../sql/' + 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
  # Added to classes that need to carry the As-Of date around
5
6
  #
6
7
  module AsOfTimeHolder
@@ -18,6 +19,5 @@ module ChronoModel
18
19
  @_as_of_time
19
20
  end
20
21
  end
21
-
22
22
  end
23
23
  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
- return scope
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
- def _chrono_target?
42
- @_target_klass ||= reflection.options[:polymorphic] ?
43
- owner.public_send(reflection.foreign_type).constantize :
44
- reflection.klass
39
+ def _chrono_record?
40
+ owner.class.include?(ChronoModel::Patches::AsOfTimeHolder) && owner.as_of_time.present?
41
+ end
45
42
 
46
- @_target_klass.chrono?
47
- end
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
- def config
6
- super.dup.tap {|config| config['adapter'] = 'postgresql/chronomodel' }
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
- virtual_table_at(@as_of_time, table_name: @table_alias || @table_name)
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 :options
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
- @options = options.freeze
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
- if options[:as_of_time]
41
- preload_scope = given_preload_scope ||
42
- ChronoModel::Patches::AsOfTimeRelation.new(options[:model])
48
+ super(records, associations, chronomodel_scope(given_preload_scope))
49
+ end
50
+
51
+ private
43
52
 
44
- preload_scope.as_of_time!(options[:as_of_time])
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
- super records, associations, preload_scope
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
- return scope
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 = ChronoModel.history_models[join.left.table_name]
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: self.model, as_of_time: as_of_time
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
@@ -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/db_console'
10
+ require 'chrono_model/patches/batches'