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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +19 -20
  3. data/README.md +73 -62
  4. data/lib/active_record/connection_adapters/chronomodel_adapter.rb +14 -14
  5. data/lib/active_record/tasks/chronomodel_database_tasks.rb +40 -39
  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/stable.rb +41 -0
  10. data/lib/chrono_model/adapter/tsrange.rb +20 -5
  11. data/lib/chrono_model/adapter/upgrade.rb +89 -91
  12. data/lib/chrono_model/adapter.rb +59 -31
  13. data/lib/chrono_model/chrono.rb +17 -0
  14. data/lib/chrono_model/conversions.rb +14 -8
  15. data/lib/chrono_model/db_console.rb +5 -0
  16. data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
  17. data/lib/chrono_model/patches/as_of_time_relation.rb +3 -13
  18. data/lib/chrono_model/patches/association.rb +15 -12
  19. data/lib/chrono_model/patches/batches.rb +13 -0
  20. data/lib/chrono_model/patches/db_console.rb +20 -4
  21. data/lib/chrono_model/patches/join_node.rb +4 -4
  22. data/lib/chrono_model/patches/preloader.rb +41 -11
  23. data/lib/chrono_model/patches/relation.rb +51 -8
  24. data/lib/chrono_model/patches.rb +3 -1
  25. data/lib/chrono_model/railtie.rb +13 -27
  26. data/lib/chrono_model/time_gate.rb +3 -3
  27. data/lib/chrono_model/time_machine/history_model.rb +65 -31
  28. data/lib/chrono_model/time_machine/time_query.rb +65 -49
  29. data/lib/chrono_model/time_machine/timeline.rb +52 -28
  30. data/lib/chrono_model/time_machine.rb +57 -25
  31. data/lib/chrono_model/utilities.rb +3 -3
  32. data/lib/chrono_model/version.rb +3 -1
  33. data/lib/chrono_model.rb +31 -36
  34. metadata +24 -263
  35. data/.gitignore +0 -21
  36. data/.rspec +0 -2
  37. data/.travis.yml +0 -41
  38. data/Gemfile +0 -4
  39. data/README.sql +0 -161
  40. data/Rakefile +0 -25
  41. data/chrono_model.gemspec +0 -33
  42. data/gemfiles/rails_5.0.gemfile +0 -6
  43. data/gemfiles/rails_5.1.gemfile +0 -6
  44. data/gemfiles/rails_5.2.gemfile +0 -6
  45. data/lib/chrono_model/json.rb +0 -28
  46. data/spec/aruba/dbconsole_spec.rb +0 -25
  47. data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
  48. data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
  49. data/spec/aruba/fixtures/empty_structure.sql +0 -27
  50. data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
  51. data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
  52. data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
  53. data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
  54. data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
  55. data/spec/aruba/migrations_spec.rb +0 -48
  56. data/spec/aruba/rake_task_spec.rb +0 -71
  57. data/spec/chrono_model/adapter/base_spec.rb +0 -157
  58. data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
  59. data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
  60. data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
  61. data/spec/chrono_model/conversions_spec.rb +0 -43
  62. data/spec/chrono_model/history_models_spec.rb +0 -32
  63. data/spec/chrono_model/json_ops_spec.rb +0 -59
  64. data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
  65. data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
  66. data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
  67. data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
  68. data/spec/chrono_model/time_machine/history_spec.rb +0 -104
  69. data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
  70. data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
  71. data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
  72. data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
  73. data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
  74. data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
  75. data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
  76. data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
  77. data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
  78. data/spec/config.travis.yml +0 -5
  79. data/spec/config.yml.example +0 -9
  80. data/spec/spec_helper.rb +0 -33
  81. data/spec/support/adapter/helpers.rb +0 -53
  82. data/spec/support/adapter/structure.rb +0 -44
  83. data/spec/support/aruba.rb +0 -44
  84. data/spec/support/connection.rb +0 -70
  85. data/spec/support/matchers/base.rb +0 -56
  86. data/spec/support/matchers/column.rb +0 -99
  87. data/spec/support/matchers/function.rb +0 -79
  88. data/spec/support/matchers/index.rb +0 -69
  89. data/spec/support/matchers/schema.rb +0 -39
  90. data/spec/support/matchers/table.rb +0 -275
  91. data/spec/support/time_machine/helpers.rb +0 -47
  92. data/spec/support/time_machine/structure.rb +0 -111
  93. data/sql/json_ops.sql +0 -56
  94. data/sql/uninstall-json_ops.sql +0 -24
@@ -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
- [:primary_key, :indexes, :default_sequence_name].each do |method|
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
- on_schema(TEMPORAL_SCHEMA + ',' + self.schema_search_path, recurse: :ignore) { super(table_name) }
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 = self.schema_search_path
123
+ old_path = schema_search_path
105
124
 
106
125
  count_recursions do
107
- if recurse == :follow or Thread.current['recursions'] == 1
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
- @connection.transaction_status == PG::Connection::PQTRANS_INERROR
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}") if data_source_exists?(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 %[ COMMENT ON VIEW #{view_name} IS #{quote(comment)} ]
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
- yield
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
- ensure
169
- Thread.current['recursions'] -= 1
170
- end
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
- # 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
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
- 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
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
- 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_fs(:db) << '.' << format('%06d', time.usec)
17
24
  end
18
25
  end
19
-
20
26
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'chrono_model/patches/db_console'
4
+
5
+ Rails::DBConsole.prepend ChronoModel::Patches::DBConsole::DbConfig
@@ -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,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
- 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChronoModel
4
+ module Patches
5
+ module Batches
6
+ def in_batches(**)
7
+ return super unless try(:history?)
8
+
9
+ with_hid_pkey { super }
10
+ end
11
+ end
12
+ end
13
+ 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,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 = ChronoModel.history_models[join.left.table_name]
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: self.model, as_of_time: as_of_time
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
@@ -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'
@@ -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
- rake_tasks do
7
- if Rails.application.config.active_record.schema_format != :sql
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
- # Make schema:dump and schema:load invoke structure:dump and structure:load
18
- Rake::Task['db:schema:dump'].clear.enhance(['environment']) do
19
- Rake::Task['db:structure:dump'].invoke
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 "Dumps database into db/data.NOW.sql or file specified via DUMP="
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 "Loads database dump from file specified via DUMP="
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, "Invoke as rake db:data:load DUMP=/path/to/data.sql"
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
- module ChronoModel
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: self.id).first!
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