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,45 +1,50 @@
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
8
+
9
+ # Register our database tasks under our adapter name
10
+ if Rails.version < '5.2'
11
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS)
12
+ else
13
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, TASKS_CLASS.to_s)
14
+ end
5
15
 
6
16
  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!'
17
+ def task_config
18
+ if Rails.version < '6.1'
19
+ ActiveRecord::Tasks::DatabaseTasks.current_config.with_indifferent_access
20
+ else
21
+ ActiveRecord::Base.connection_db_config
22
+ end
9
23
  end
10
24
 
11
- tasks_class = ActiveRecord::Tasks::ChronomodelDatabaseTasks
25
+ if Rails.version < '6.1'
26
+ # Make schema:dump and schema:load invoke structure:dump and structure:load
27
+ Rake::Task['db:schema:dump'].clear.enhance(['environment']) do
28
+ Rake::Task['db:structure:dump'].invoke
29
+ end
12
30
 
13
- # Register our database tasks under our adapter name
14
- #
15
- ActiveRecord::Tasks::DatabaseTasks.register_task(/chronomodel/, tasks_class)
16
-
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
31
+ Rake::Task['db:schema:load'].clear.enhance(['environment']) do
32
+ Rake::Task['db:structure:load'].invoke
33
+ end
20
34
  end
21
35
 
22
- Rake::Task['db:schema:load'].clear.enhance(['environment']) do
23
- Rake::Task['db:structure:load'].invoke
24
- end
25
-
26
- desc "Dumps database into db/data.NOW.sql or file specified via DUMP="
36
+ desc 'Dumps database into db/data.NOW.sql or file specified via DUMP='
27
37
  task 'db:data:dump' => :environment do
28
- config = ActiveRecord::Tasks::DatabaseTasks.current_config
29
38
  target = ENV['DUMP'] || Rails.root.join('db', "data.#{Time.now.to_f}.sql")
30
-
31
- tasks_class.new(config).data_dump(target)
39
+ TASKS_CLASS.new(task_config).data_dump(target)
32
40
  end
33
41
 
34
- desc "Loads database dump from file specified via DUMP="
42
+ desc 'Loads database dump from file specified via DUMP='
35
43
  task 'db:data:load' => :environment do
36
- config = ActiveRecord::Tasks::DatabaseTasks.current_config
37
44
  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)
45
+ raise ArgumentError, 'Invoke as rake db:data:load DUMP=/path/to/data.sql'
46
+ TASKS_CLASS.new(task_config).data_load(source)
41
47
  end
42
48
  end
43
-
44
49
  end
45
50
  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
@@ -1,15 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  module TimeMachine
3
-
4
5
  module HistoryModel
5
6
  extend ActiveSupport::Concern
6
7
 
7
8
  included do
8
- self.table_name = [Adapter::HISTORY_SCHEMA, superclass.table_name].join('.')
9
+ self.table_name = "#{Adapter::HISTORY_SCHEMA}.#{superclass.table_name}"
9
10
 
10
11
  scope :chronological, -> { order(Arel.sql('lower(validity) ASC')) }
11
12
  end
12
13
 
14
+ # ACTIVE RECORD 7 does not call `class.find` but a new internal method called `_find_record`
15
+ def _find_record(options)
16
+ if options && options[:lock]
17
+ self.class.preload(strict_loaded_associations).lock(options[:lock]).find_by!(hid: hid)
18
+ else
19
+ self.class.preload(strict_loaded_associations).find_by!(hid: hid)
20
+ end
21
+ end
22
+
13
23
  # Methods that make up the history interface of the companion History
14
24
  # model, automatically built for each Model that includes TimeMachine
15
25
  #
@@ -21,11 +31,11 @@ module ChronoModel
21
31
  # setting it now and ensuring to reset it to the original one after
22
32
  # execution completes. FIXME
23
33
  #
24
- def with_hid_pkey(&block)
25
- old = self.primary_key
34
+ def with_hid_pkey
35
+ old = primary_key
26
36
  self.primary_key = :hid
27
37
 
28
- block.call
38
+ yield
29
39
  ensure
30
40
  self.primary_key = old
31
41
  end
@@ -61,9 +71,12 @@ module ChronoModel
61
71
  end
62
72
 
63
73
  def virtual_table_at(time, table_name: nil)
64
- virtual_name = table_name ?
65
- connection.quote_table_name(table_name) :
66
- superclass.quoted_table_name
74
+ virtual_name =
75
+ if table_name
76
+ connection.quote_table_name(table_name)
77
+ else
78
+ superclass.quoted_table_name
79
+ end
67
80
 
68
81
  "(#{at(time).to_sql}) #{virtual_name}"
69
82
  end
@@ -77,7 +90,7 @@ module ChronoModel
77
90
  # Returns the history sorted by recorded_at
78
91
  #
79
92
  def sorted
80
- all.order(Arel.sql(%[ #{quoted_table_name}."recorded_at" ASC, #{quoted_table_name}."hid" ASC ]))
93
+ all.order(Arel.sql(%( #{quoted_table_name}."recorded_at" ASC, #{quoted_table_name}."hid" ASC )))
81
94
  end
82
95
 
83
96
  # Fetches the given +object+ history, sorted by history record time
@@ -96,9 +109,7 @@ module ChronoModel
96
109
  #
97
110
  # As such it is overriden here to return the same contents that
98
111
  # the parent would have returned.
99
- def sti_name
100
- superclass.sti_name
101
- end
112
+ delegate :sti_name, to: :superclass
102
113
 
103
114
  # For STI to work, the history model needs to have the exact same
104
115
  # semantics as the model it inherits from. However given it is
@@ -107,18 +118,17 @@ module ChronoModel
107
118
  # same exact hierarchy location as its parent, thus this is defined in
108
119
  # this override.
109
120
  #
110
- def descends_from_active_record?
111
- superclass.descends_from_active_record?
112
- end
121
+ delegate :descends_from_active_record?, to: :superclass
113
122
 
114
123
  private
115
- # STI fails when a Foo::History record has Foo as type in the
116
- # inheritance column; AR expects the type to be an instance of the
117
- # current class or a descendant (or self).
118
- #
119
- def find_sti_class(type_name)
120
- super(type_name + "::History")
121
- end
124
+
125
+ # STI fails when a Foo::History record has Foo as type in the
126
+ # inheritance column; AR expects the type to be an instance of the
127
+ # current class or a descendant (or self).
128
+ #
129
+ def find_sti_class(type_name)
130
+ super("#{type_name}::History")
131
+ end
122
132
  end
123
133
 
124
134
  # The history id is `hid`, but this cannot set as primary key
@@ -134,15 +144,15 @@ module ChronoModel
134
144
  end
135
145
 
136
146
  def save(*)
137
- self.class.with_hid_pkey { super }
147
+ with_hid_pkey { super }
138
148
  end
139
149
 
140
150
  def save!(*)
141
- self.class.with_hid_pkey { super }
151
+ with_hid_pkey { super }
142
152
  end
143
153
 
144
154
  def update_columns(*)
145
- self.class.with_hid_pkey { super }
155
+ with_hid_pkey { super }
146
156
  end
147
157
 
148
158
  def historical?
@@ -153,7 +163,7 @@ module ChronoModel
153
163
  # is the first one.
154
164
  #
155
165
  def pred
156
- return if self.valid_from.nil?
166
+ return if valid_from.nil?
157
167
 
158
168
  if self.class.timeline_associations.empty?
159
169
  self.class.where('id = ? AND upper(validity) = ?', rid, valid_from).first
@@ -166,7 +176,7 @@ module ChronoModel
166
176
  # last one.
167
177
  #
168
178
  def succ
169
- return if self.valid_to.nil?
179
+ return if valid_to.nil?
170
180
 
171
181
  if self.class.timeline_associations.empty?
172
182
  self.class.where('id = ? AND lower(validity) = ?', rid, valid_to).first
@@ -174,7 +184,7 @@ module ChronoModel
174
184
  super(id: rid, after: valid_to, table: self.class.superclass.quoted_table_name)
175
185
  end
176
186
  end
177
- alias :next :succ
187
+ alias next succ
178
188
 
179
189
  # Returns the first history entry
180
190
  #
@@ -194,9 +204,9 @@ module ChronoModel
194
204
  self.class.superclass.find(rid)
195
205
  end
196
206
 
197
- def record #:nodoc:
207
+ def record # :nodoc:
198
208
  ActiveSupport::Deprecation.warn '.record is deprecated in favour of .current_version'
199
- self.current_version
209
+ current_version
200
210
  end
201
211
 
202
212
  def valid_from
@@ -211,7 +221,31 @@ module ChronoModel
211
221
  def recorded_at
212
222
  ChronoModel::Conversions.string_to_utc_time attributes_before_type_cast['recorded_at']
213
223
  end
214
- end
215
224
 
225
+ # Starting from Rails 6.0, `.read_attribute` will use the memoized
226
+ # `primary_key` if it detects that the attribute name is `id`.
227
+ #
228
+ # Since the `primary key` may have been changed to `hid` because of
229
+ # `.find` overload, the new behavior may break relations where `id` is
230
+ # still the correct attribute to read
231
+ #
232
+ # Ref: ifad/chronomodel#181
233
+ def read_attribute(attr_name, &block)
234
+ return super unless attr_name.to_s == 'id' && @primary_key.to_s == 'hid'
235
+
236
+ _read_attribute('id', &block)
237
+ end
238
+
239
+ private
240
+
241
+ def with_hid_pkey(&block)
242
+ old_primary_key = @primary_key
243
+ @primary_key = :hid
244
+
245
+ self.class.with_hid_pkey(&block)
246
+ ensure
247
+ @primary_key = old_primary_key
248
+ end
249
+ end
216
250
  end
217
251
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  module TimeMachine
3
-
4
5
  #
5
6
  # TODO Documentation
6
7
  #
@@ -12,75 +13,90 @@ module ChronoModel
12
13
  end
13
14
 
14
15
  private
15
- def time_query_sql(match, time, range, options)
16
- case match
17
- when :at
18
- build_time_query_at(time, range)
19
16
 
20
- when :not
21
- "NOT (#{build_time_query_at(time, range)})"
17
+ def time_query_sql(match, time, range, options)
18
+ case match
19
+ when :at
20
+ build_time_query_at(time, range)
22
21
 
23
- when :before
24
- op = options.fetch(:inclusive, true) ? '&&' : '@>'
25
- build_time_query(['NULL', time_for_time_query(time, range)], range, op)
22
+ when :not
23
+ "NOT (#{build_time_query_at(time, range)})"
26
24
 
27
- when :after
28
- op = options.fetch(:inclusive, true) ? '&&' : '@>'
29
- build_time_query([time_for_time_query(time, range), 'NULL'], range, op)
25
+ when :before
26
+ op =
27
+ if options.fetch(:inclusive, true)
28
+ '&&'
29
+ else
30
+ '@>'
31
+ end
32
+ build_time_query(['NULL', time_for_time_query(time, range)], range, op)
30
33
 
31
- else
32
- raise ChronoModel::Error, "Invalid time_query: #{match}"
33
- end
34
+ when :after
35
+ op =
36
+ if options.fetch(:inclusive, true)
37
+ '&&'
38
+ else
39
+ '@>'
40
+ end
41
+ build_time_query([time_for_time_query(time, range), 'NULL'], range, op)
42
+
43
+ else
44
+ raise ChronoModel::Error, "Invalid time_query: #{match}"
34
45
  end
46
+ end
35
47
 
36
- def time_for_time_query(t, column)
37
- if t == :now || t == :today
38
- now_for_column(column)
39
- else
40
- quoted_t = connection.quote(connection.quoted_date(t))
41
- [quoted_t, primitive_type_for_column(column)].join('::')
42
- end
48
+ def time_for_time_query(t, column)
49
+ if t == :now || t == :today
50
+ now_for_column(column)
51
+ else
52
+ quoted_t = connection.quote(connection.quoted_date(t))
53
+ "#{quoted_t}::#{primitive_type_for_column(column)}"
43
54
  end
55
+ end
44
56
 
45
- def now_for_column(column)
46
- case column.type
47
- when :tsrange, :tstzrange then "timezone('UTC', current_timestamp)"
48
- when :daterange then "current_date"
49
- else raise "Cannot generate 'now()' for #{column.type} column #{column.name}"
50
- end
57
+ def now_for_column(column)
58
+ case column.type
59
+ when :tsrange, :tstzrange then "timezone('UTC', current_timestamp)"
60
+ when :daterange then 'current_date'
61
+ else raise "Cannot generate 'now()' for #{column.type} column #{column.name}"
51
62
  end
63
+ end
52
64
 
53
- def primitive_type_for_column(column)
54
- case column.type
55
- when :tsrange then :timestamp
56
- when :tstzrange then :timestamptz
57
- when :daterange then :date
58
- else raise "Don't know how to map #{column.type} column #{column.name} to a primitive type"
59
- end
65
+ def primitive_type_for_column(column)
66
+ case column.type
67
+ when :tsrange then :timestamp
68
+ when :tstzrange then :timestamptz
69
+ when :daterange then :date
70
+ else raise "Don't know how to map #{column.type} column #{column.name} to a primitive type"
60
71
  end
72
+ end
61
73
 
62
- def build_time_query_at(time, range)
63
- time = if time.kind_of?(Array)
64
- time.map! {|t| time_for_time_query(t, range)}
74
+ def build_time_query_at(time, range)
75
+ time =
76
+ if time.is_a?(Array)
77
+ time.map! { |t| time_for_time_query(t, range) }
65
78
 
66
79
  # If both edges of the range are the same the query fails using the '&&' operator.
67
80
  # The correct solution is to use the <@ operator.
68
- time.first == time.last ? time.first : time
81
+ if time.first == time.last
82
+ time.first
83
+ else
84
+ time
85
+ end
69
86
  else
70
87
  time_for_time_query(time, range)
71
88
  end
72
89
 
73
- build_time_query(time, range)
74
- end
90
+ build_time_query(time, range)
91
+ end
75
92
 
76
- def build_time_query(time, range, op = '&&')
77
- if time.kind_of?(Array)
78
- Arel.sql %[ #{range.type}(#{time.first}, #{time.last}) #{op} #{table_name}.#{range.name} ]
79
- else
80
- Arel.sql %[ #{time} <@ #{table_name}.#{range.name} ]
81
- end
93
+ def build_time_query(time, range, op = '&&')
94
+ if time.is_a?(Array)
95
+ Arel.sql %[ #{range.type}(#{time.first}, #{time.last}) #{op} #{table_name}.#{range.name} ]
96
+ else
97
+ Arel.sql %( #{time} <@ #{table_name}.#{range.name} )
82
98
  end
99
+ end
83
100
  end
84
-
85
101
  end
86
102
  end
@@ -1,59 +1,87 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  module TimeMachine
3
-
4
5
  module Timeline
5
6
  # Returns an Array of unique UTC timestamps for which at least an
6
7
  # history record exists. Takes temporal associations into account.
7
8
  #
8
9
  def timeline(record = nil, options = {})
9
- rid = record.respond_to?(:rid) ? record.rid : record.id if record
10
+ rid =
11
+ if record
12
+ if record.respond_to?(:rid)
13
+ record.rid
14
+ else
15
+ record.id
16
+ end
17
+ end
10
18
 
11
- assocs = options.key?(:with) ?
12
- timeline_associations_from(options[:with]) : timeline_associations
19
+ assocs =
20
+ if options.key?(:with)
21
+ timeline_associations_from(options[:with])
22
+ else
23
+ timeline_associations
24
+ end
13
25
 
14
26
  models = []
15
- models.push self if self.chrono?
16
- models.concat(assocs.map {|a| a.klass.history})
27
+ models.push self if chrono?
28
+ models.concat(assocs.map { |a| a.klass.history })
17
29
 
18
30
  return [] if models.empty?
19
31
 
20
- fields = models.inject([]) {|a,m| a.concat m.quoted_history_fields}
32
+ fields = models.inject([]) { |a, m| a.concat m.quoted_history_fields }
21
33
 
22
- relation = self.except(:order).
23
- select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
34
+ relation = except(:order)
35
+ .select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")
24
36
 
25
37
  if assocs.present?
26
- relation = relation.joins(*assocs.map(&:name))
38
+ assocs.each do |ass|
39
+ association_quoted_table_name = connection.quote_table_name(ass.table_name)
40
+ # `join` first, then use `where`s
41
+ relation =
42
+ if ass.belongs_to?
43
+ relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.association_primary_key} = #{quoted_table_name}.#{ass.association_foreign_key}")
44
+ else
45
+ relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.foreign_key} = #{quoted_table_name}.#{primary_key}")
46
+ end
47
+ end
27
48
  end
28
49
 
29
- relation = relation.
30
- order('ts ' << (options[:reverse] ? 'DESC' : 'ASC'))
50
+ relation_order =
51
+ if options[:reverse]
52
+ 'DESC'
53
+ else
54
+ 'ASC'
55
+ end
56
+
57
+ relation = relation.order("ts #{relation_order}")
31
58
 
32
- relation = relation.from(%["public".#{quoted_table_name}]) unless self.chrono?
59
+ relation = relation.from("public.#{quoted_table_name}") unless chrono?
33
60
  relation = relation.where(id: rid) if rid
34
61
 
35
- sql = "SELECT ts FROM ( #{relation.to_sql} ) foo WHERE ts IS NOT NULL"
62
+ sql = "SELECT ts FROM ( #{relation.to_sql} ) AS foo WHERE ts IS NOT NULL".dup
36
63
 
37
64
  if options.key?(:before)
38
65
  sql << " AND ts < '#{Conversions.time_to_utc_string(options[:before])}'"
39
66
  end
40
67
 
41
68
  if options.key?(:after)
42
- sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after ])}'"
69
+ sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after])}'"
43
70
  end
44
71
 
45
72
  if rid && !options[:with]
46
- sql << (self.chrono? ? %{
47
- AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} )
48
- } : %[ AND ts < NOW() ])
73
+ sql <<
74
+ if chrono?
75
+ %{ AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} ) }
76
+ else
77
+ %[ AND ts < NOW() ]
78
+ end
49
79
  end
50
80
 
51
81
  sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)
52
82
 
53
- sql.gsub! 'INNER JOIN', 'LEFT OUTER JOIN'
54
-
55
83
  connection.on_schema(Adapter::HISTORY_SCHEMA) do
56
- connection.select_values(sql, "#{self.name} periods").map! do |ts|
84
+ connection.select_values(sql, "#{name} periods").map! do |ts|
57
85
  Conversions.string_to_utc_time ts
58
86
  end
59
87
  end
@@ -74,21 +102,17 @@ module ChronoModel
74
102
  def timeline_associations_from(names)
75
103
  Array.wrap(names).map do |name|
76
104
  reflect_on_association(name) or raise ArgumentError,
77
- "No association found for name `#{name}'"
105
+ "No association found for name `#{name}'"
78
106
  end
79
107
  end
80
108
 
81
109
  def quoted_history_fields
82
110
  @quoted_history_fields ||= begin
83
- validity =
84
- [connection.quote_table_name(table_name),
85
- connection.quote_column_name('validity')
86
- ].join('.')
111
+ validity = "#{quoted_table_name}.#{connection.quote_column_name('validity')}"
87
112
 
88
- [:lower, :upper].map! {|func| "#{func}(#{validity})"}
113
+ %w[lower upper].map! { |func| "#{func}(#{validity})" }
89
114
  end
90
115
  end
91
116
  end
92
-
93
117
  end
94
118
  end