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