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