torque-postgresql 2.0.4 → 2.1.2

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.
@@ -9,7 +9,7 @@ module Torque
9
9
  module Builder
10
10
  def self.include_on(klass, method_name, builder_klass, **extra, &block)
11
11
  klass.define_singleton_method(method_name) do |*args, **options|
12
- return unless connection.table_exists?(table_name)
12
+ return unless table_exists?
13
13
 
14
14
  args.each do |attribute|
15
15
  begin
@@ -33,13 +33,13 @@ module Torque
33
33
  def values_methods
34
34
  return @values_methods if defined?(@values_methods)
35
35
 
36
- prefix = options.fetch(:prefix, nil).try(:<<, '_')
37
- suffix = options.fetch(:suffix, nil).try(:prepend, '_')
36
+ prefix = options.fetch(:prefix, nil)
37
+ suffix = options.fetch(:suffix, nil)
38
38
 
39
- prefix = attribute + '_' if prefix == true
40
- suffix = '_' + attribute if suffix == true
39
+ prefix = attribute if prefix == true
40
+ suffix = attribute if suffix == true
41
41
 
42
- base = "#{prefix}%s#{suffix}"
42
+ base = [prefix, '%s', suffix].compact.join('_')
43
43
 
44
44
  @values_methods = begin
45
45
  values.map do |val|
@@ -29,7 +29,7 @@ module Torque
29
29
  def include_on(klass, method_name = nil)
30
30
  method_name ||= Torque::PostgreSQL.config.enum.base_method
31
31
  Builder.include_on(klass, method_name, Builder::Enum) do |builder|
32
- defined_enums[builder.attribute.to_sym] = builder.subtype
32
+ defined_enums[builder.attribute.to_s] = builder.subtype.klass
33
33
  end
34
34
  end
35
35
 
@@ -32,7 +32,7 @@ module Torque
32
32
  def include_on(klass, method_name = nil)
33
33
  method_name ||= Torque::PostgreSQL.config.enum.set_method
34
34
  Builder.include_on(klass, method_name, Builder::Enum, set_features: true) do |builder|
35
- defined_enums[builder.attribute.to_sym] = builder.subtype
35
+ defined_enums[builder.attribute.to_s] = builder.subtype
36
36
  end
37
37
  end
38
38
 
@@ -4,38 +4,35 @@ module Torque
4
4
  module PostgreSQL
5
5
  module AutosaveAssociation
6
6
  module ClassMethods
7
+ # Since belongs to many is a collection, the callback would normally go
8
+ # to +after_create+. However, since it is a +belongs_to+ kind of
9
+ # association, it neds to be executed +before_save+
7
10
  def add_autosave_association_callbacks(reflection)
8
11
  return super unless reflection.macro.eql?(:belongs_to_many)
9
12
 
10
13
  save_method = :"autosave_associated_records_for_#{reflection.name}"
11
- define_non_cyclic_method(save_method) { save_belongs_to_many_array(reflection) }
12
-
13
- if PostgreSQL::AR610
14
- around_save(:around_save_collection_association)
15
- else
16
- before_save(:before_save_collection_association)
17
- after_save(:after_save_collection_association) if ::ActiveRecord::Base
18
- .instance_methods.include?(:after_save_collection_association)
14
+ define_non_cyclic_method(save_method) do
15
+ save_belongs_to_many_association(reflection)
19
16
  end
20
17
 
21
- before_create(save_method)
22
- before_update(save_method)
18
+ before_save(save_method)
23
19
 
24
20
  define_autosave_validation_callbacks(reflection)
25
21
  end
26
22
  end
27
23
 
28
- def save_belongs_to_many_array(reflection)
29
- save_collection_association(reflection)
24
+ # Ensure the right way to execute +save_collection_association+ and also
25
+ # keep it as a single change using +build_changes+
26
+ def save_belongs_to_many_association(reflection)
27
+ previously_new_record_before_save = (@new_record_before_save ||= false)
28
+ @new_record_before_save = new_record?
30
29
 
31
30
  association = association_instance_get(reflection.name)
32
- return unless association
33
-
34
- klass_attr = reflection.active_record_primary_key
35
- source_attr = reflection.foreign_key
36
-
37
- records = association.target.each_with_object(klass_attr)
38
- write_attribute(source_attr, records.map(&:read_attribute).compact)
31
+ association&.build_changes { save_collection_association(reflection) }
32
+ rescue ::ActiveRecord::RecordInvalid
33
+ throw(:abort)
34
+ ensure
35
+ @new_record_before_save = previously_new_record_before_save
39
36
  end
40
37
  end
41
38
 
@@ -200,11 +200,18 @@ module Torque
200
200
  # belongs_to_many :tags, dependent: :nullify
201
201
  # belongs_to_many :tags, required: true, touch: true
202
202
  # belongs_to_many :tags, default: -> { Tag.default }
203
- def belongs_to_many(name, scope = nil, **options)
204
- reflection = Associations::Builder::BelongsToMany.build(self, name, scope, options)
203
+ def belongs_to_many(name, scope = nil, **options, &extension)
204
+ klass = Associations::Builder::BelongsToMany
205
+ reflection = klass.build(self, name, scope, options, &extension)
205
206
  ::ActiveRecord::Reflection.add_reflection(self, name, reflection)
206
207
  end
207
208
 
209
+ # Allow extra keyword arguments to be sent to +InsertAll+
210
+ def upsert_all(attributes, **xargs)
211
+ xargs = xargs.merge(on_duplicate: :update)
212
+ ::ActiveRecord::InsertAll.new(self, attributes, **xargs).execute
213
+ end
214
+
208
215
  protected
209
216
 
210
217
  # Allow optional select attributes to be loaded manually when they are
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Torque
4
+ module PostgreSQL
5
+ module InsertAll
6
+ attr_reader :where
7
+
8
+ def initialize(*args, where: nil, **xargs)
9
+ super(*args, **xargs)
10
+
11
+ @where = where
12
+ end
13
+ end
14
+
15
+ module InsertAll::Builder
16
+ delegate :where, to: :insert_all
17
+
18
+ def where_condition?
19
+ !where.nil?
20
+ end
21
+ end
22
+
23
+ ActiveRecord::InsertAll.prepend InsertAll
24
+ ActiveRecord::InsertAll::Builder.include InsertAll::Builder
25
+ end
26
+ end
@@ -103,29 +103,6 @@ module Torque
103
103
 
104
104
  ::Arel::Nodes::NamedFunction.new('ANY', [source_attr])
105
105
  end
106
-
107
- # returns either +nil+ or the inverse association name that it finds.
108
- def automatic_inverse_of
109
- return super unless connected_through_array?
110
-
111
- if can_find_inverse_of_automatically?(self)
112
- inverse_name = options[:as] || active_record.name.demodulize
113
- inverse_name = ActiveSupport::Inflector.underscore(inverse_name)
114
- inverse_name = ActiveSupport::Inflector.pluralize(inverse_name)
115
- inverse_name = inverse_name.to_sym
116
-
117
- begin
118
- reflection = klass._reflect_on_association(inverse_name)
119
- rescue NameError
120
- # Give up: we couldn't compute the klass type so we won't be able
121
- # to find any associations either.
122
- reflection = false
123
- end
124
-
125
- return inverse_name if reflection.connected_through_array? &&
126
- valid_inverse_reflection?(reflection)
127
- end
128
- end
129
106
  end
130
107
 
131
108
  ::ActiveRecord::Reflection::AbstractReflection.prepend(AbstractReflection)
@@ -24,6 +24,28 @@ module Torque
24
24
  result
25
25
  end
26
26
 
27
+ # returns either +nil+ or the inverse association name that it finds.
28
+ def automatic_inverse_of
29
+ return super unless connected_through_array?
30
+
31
+ if can_find_inverse_of_automatically?(self)
32
+ inverse_name = options[:as] || active_record.name.demodulize
33
+ inverse_name = ActiveSupport::Inflector.underscore(inverse_name)
34
+ inverse_name = ActiveSupport::Inflector.pluralize(inverse_name)
35
+ inverse_name = inverse_name.to_sym
36
+
37
+ begin
38
+ reflection = klass._reflect_on_association(inverse_name)
39
+ rescue NameError
40
+ # Give up: we couldn't compute the klass type so we won't be able
41
+ # to find any associations either.
42
+ reflection = false
43
+ end
44
+
45
+ return inverse_name if valid_inverse_reflection?(reflection)
46
+ end
47
+ end
48
+
27
49
  end
28
50
 
29
51
  ::ActiveRecord::Reflection::AssociationReflection.prepend(AssociationReflection)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Torque
4
4
  module PostgreSQL
5
- VERSION = '2.0.4'
5
+ VERSION = '2.1.2'
6
6
  end
7
7
  end
@@ -0,0 +1,5 @@
1
+ FactoryBot.define do
2
+ factory :item do
3
+ name { Faker::Lorem.sentence }
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class Item < ActiveRecord::Base
2
+ belongs_to_many :tags
3
+ end
data/spec/schema.rb CHANGED
@@ -11,7 +11,7 @@
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
13
  begin
14
- version = 62
14
+ version = 70
15
15
 
16
16
  raise SystemExit if ActiveRecord::Migrator.current_version == version
17
17
  ActiveRecord::Schema.define(version: version) do
@@ -74,6 +74,7 @@ begin
74
74
  create_table "comments", force: :cascade do |t|
75
75
  t.integer "user_id", null: false
76
76
  t.integer "comment_id"
77
+ t.integer "video_id"
77
78
  t.text "content", null: false
78
79
  t.string "kind"
79
80
  t.index ["user_id"], name: "index_comments_on_user_id", using: :btree
@@ -101,6 +102,13 @@ begin
101
102
  t.index ["author_id"], name: "index_posts_on_author_id", using: :btree
102
103
  end
103
104
 
105
+ create_table "items", force: :cascade do |t|
106
+ t.string "name"
107
+ t.bigint "tag_ids", array: true, default: "{1}"
108
+ t.datetime "created_at", null: false
109
+ t.datetime "updated_at", null: false
110
+ end
111
+
104
112
  create_table "users", force: :cascade do |t|
105
113
  t.string "name", null: false
106
114
  t.enum "role", subtype: :roles, default: :visitor
@@ -26,9 +26,13 @@ RSpec.describe 'BelongsToMany' do
26
26
  let(:other) { Tag }
27
27
  let(:initial) { FactoryBot.create(:tag) }
28
28
 
29
- before { Video.belongs_to_many :tags }
29
+ before { Video.belongs_to_many(:tags) }
30
30
  subject { Video.create(title: 'A') }
31
- after { Video._reflections = {} }
31
+
32
+ after do
33
+ Video.reset_callbacks(:save)
34
+ Video._reflections = {}
35
+ end
32
36
 
33
37
  it 'has the method' do
34
38
  expect(subject).to respond_to(:tags)
@@ -93,13 +97,80 @@ RSpec.describe 'BelongsToMany' do
93
97
  expect(records.map(&:id).sort).to be_eql(ids.sort)
94
98
  end
95
99
 
100
+ it 'can create the owner record with direct set items' do
101
+ # Having another association would break this test due to how
102
+ # +@new_record_before_save+ is set on autosave association
103
+ Video.has_many(:comments)
104
+
105
+ record = Video.create(title: 'A', tags: [initial])
106
+ record.reload
107
+
108
+ expect(record.tags.size).to be_eql(1)
109
+ expect(record.tags.first.id).to be_eql(initial.id)
110
+ end
111
+
112
+ it 'can keep record changes accordingly' do
113
+ expect(subject.tags.count).to be_eql(0)
114
+
115
+ local_previous_changes = nil
116
+ local_saved_changes = nil
117
+
118
+ Video.after_commit do
119
+ local_previous_changes = self.previous_changes.dup
120
+ local_saved_changes = self.saved_changes.dup
121
+ end
122
+
123
+ subject.update(title: 'B')
124
+
125
+ expect(local_previous_changes).to include('title')
126
+ expect(local_saved_changes).to include('title')
127
+
128
+ subject.tags = FactoryBot.create_list(:tag, 5)
129
+ subject.update(title: 'C', url: 'X')
130
+ subject.reload
131
+
132
+ expect(local_previous_changes).to include('title', 'url')
133
+ expect(local_saved_changes).to include('title', 'url')
134
+ expect(local_previous_changes).not_to include('tag_ids')
135
+ expect(local_saved_changes).not_to include('tag_ids')
136
+ expect(subject.tag_ids.size).to be_eql(5)
137
+ expect(subject.tags.count).to be_eql(5)
138
+ end
139
+
140
+ it 'can assign the record ids during before callback' do
141
+ Video.before_save { self.tags = FactoryBot.create_list(:tag, 5) }
142
+
143
+ record = Video.create(title: 'A')
144
+
145
+ expect(Tag.count).to be_eql(5)
146
+ expect(record.tag_ids.size).to be_eql(5)
147
+ expect(record.tags.count).to be_eql(5)
148
+ end
149
+
150
+ it 'does not trigger after commit on the associated record' do
151
+ called = false
152
+
153
+ tag = FactoryBot.create(:tag)
154
+ Tag.after_commit { called = true }
155
+
156
+ expect(called).to be_falsey
157
+
158
+ subject.tags << tag
159
+
160
+ expect(subject.tag_ids).to be_eql([tag.id])
161
+ expect(called).to be_falsey
162
+
163
+ Tag.reset_callbacks(:commit)
164
+ end
165
+
96
166
  it 'can build an associated record' do
97
167
  record = subject.tags.build(name: 'Test')
98
168
  expect(record).to be_a(other)
99
169
  expect(record).not_to be_persisted
100
170
  expect(record.name).to be_eql('Test')
171
+ expect(subject.tags.target).to be_eql([record])
101
172
 
102
- expect(subject.save).to be_truthy
173
+ expect(subject.save && subject.reload).to be_truthy
103
174
  expect(subject.tag_ids).to be_eql([record.id])
104
175
  expect(subject.tags.size).to be_eql(1)
105
176
  end
@@ -120,7 +191,7 @@ RSpec.describe 'BelongsToMany' do
120
191
  expect(subject.tags.size).to be_eql(1)
121
192
 
122
193
  subject.tags.concat(other.new(name: 'Test'))
123
- subject.tags.reload
194
+ subject.reload
124
195
 
125
196
  expect(subject.tags.size).to be_eql(2)
126
197
  expect(subject.tag_ids.size).to be_eql(2)
@@ -131,10 +202,27 @@ RSpec.describe 'BelongsToMany' do
131
202
  subject.tags << FactoryBot.create(:tag)
132
203
  expect(subject.tags.size).to be_eql(1)
133
204
 
134
- subject.tags.replace([other.new(name: 'Test 1'), other.new(name: 'Test 2')])
135
- expect(subject.tags.size).to be_eql(2)
205
+ subject.tags = [other.new(name: 'Test 1')]
206
+ subject.reload
207
+
208
+ expect(subject.tags.size).to be_eql(1)
136
209
  expect(subject.tags[0].name).to be_eql('Test 1')
137
- expect(subject.tags[1].name).to be_eql('Test 2')
210
+
211
+ subject.tags.replace([other.new(name: 'Test 2'), other.new(name: 'Test 3')])
212
+ subject.reload
213
+
214
+ expect(subject.tags.size).to be_eql(2)
215
+ expect(subject.tags[0].name).to be_eql('Test 2')
216
+ expect(subject.tags[1].name).to be_eql('Test 3')
217
+ end
218
+
219
+ it 'can delete specific records' do
220
+ subject.tags << initial
221
+ expect(subject.tags.size).to be_eql(1)
222
+
223
+ subject.tags.delete(initial)
224
+ expect(subject.tags.size).to be_eql(0)
225
+ expect(subject.reload.tags.size).to be_eql(0)
138
226
  end
139
227
 
140
228
  it 'can delete all records' do
@@ -153,6 +241,17 @@ RSpec.describe 'BelongsToMany' do
153
241
  expect(subject.tags.size).to be_eql(0)
154
242
  end
155
243
 
244
+ it 'can clear the array' do
245
+ record = Video.create(title: 'B', tags: [initial])
246
+ expect(record.tags.size).to be_eql(1)
247
+
248
+ record.update(tag_ids: [])
249
+ record.reload
250
+
251
+ expect(record.tag_ids).to be_nil
252
+ expect(record.tags.size).to be_eql(0)
253
+ end
254
+
156
255
  it 'can have sum operations' do
157
256
  records = FactoryBot.create_list(:tag, 5)
158
257
  subject.tags.concat(records)
@@ -182,11 +281,15 @@ RSpec.describe 'BelongsToMany' do
182
281
  it 'can check if a record is included on the list' do
183
282
  outside = FactoryBot.create(:tag)
184
283
  inside = FactoryBot.create(:tag)
284
+
285
+ expect(subject.tags).not_to be_include(inside)
286
+ expect(subject.tags).not_to be_include(outside)
287
+
185
288
  subject.tags << inside
186
289
 
187
290
  expect(subject.tags).to respond_to(:include?)
188
- expect(subject.tags.include?(inside)).to be_truthy
189
- expect(subject.tags.include?(outside)).to be_falsey
291
+ expect(subject.tags).to be_include(inside)
292
+ expect(subject.tags).not_to be_include(outside)
190
293
  end
191
294
 
192
295
  it 'can append records' do
@@ -194,6 +297,9 @@ RSpec.describe 'BelongsToMany' do
194
297
  expect(subject.tags.size).to be_eql(1)
195
298
 
196
299
  subject.tags << other.new(name: 'Test 2')
300
+ subject.update(title: 'B')
301
+ subject.reload
302
+
197
303
  expect(subject.tags.size).to be_eql(2)
198
304
  expect(subject.tags.last.name).to be_eql('Test 2')
199
305
  end
@@ -208,10 +314,18 @@ RSpec.describe 'BelongsToMany' do
208
314
 
209
315
  it 'can reload records' do
210
316
  expect(subject.tags.size).to be_eql(0)
211
- subject.tags << FactoryBot.create(:tag)
317
+ new_tag = FactoryBot.create(:tag)
318
+ subject.tags << new_tag
212
319
 
213
320
  subject.tags.reload
214
321
  expect(subject.tags.size).to be_eql(1)
322
+ expect(subject.tags.first.id).to be_eql(new_tag.id)
323
+
324
+ record = Video.create(title: 'B', tags: [new_tag])
325
+ record.reload
326
+
327
+ expect(record.tags.size).to be_eql(1)
328
+ expect(record.tags.first.id).to be_eql(new_tag.id)
215
329
  end
216
330
 
217
331
  it 'can preload records' do
@@ -231,11 +345,33 @@ RSpec.describe 'BelongsToMany' do
231
345
  expect { query.load }.not_to raise_error
232
346
  end
233
347
 
234
- context "When record is not persisted" do
348
+ context 'When the attribute has a default value' do
349
+ subject { FactoryBot.create(:item) }
350
+
351
+ it 'will always return the column default value' do
352
+ expect(subject.tag_ids).to be_a(Array)
353
+ expect(subject.tag_ids).to be_eql([1])
354
+ end
355
+
356
+ it 'will keep the value as an array even when the association is cleared' do
357
+ records = FactoryBot.create_list(:tag, 5)
358
+ subject.tags.concat(records)
359
+
360
+ subject.reload
361
+ expect(subject.tag_ids).to be_a(Array)
362
+ expect(subject.tag_ids).not_to be_eql([1, *records.map(&:id)])
363
+
364
+ subject.tags.clear
365
+ subject.reload
366
+ expect(subject.tag_ids).to be_a(Array)
367
+ expect(subject.tag_ids).to be_eql([1])
368
+ end
369
+ end
370
+
371
+ context 'When record is not persisted' do
235
372
  let(:initial) { FactoryBot.create(:tag) }
236
- before { Video.belongs_to_many :tags }
373
+
237
374
  subject { Video.new(title: 'A', tags: [initial]) }
238
- after { Video._reflections = {} }
239
375
 
240
376
  it 'loads associated records' do
241
377
  expect(subject.tags.load).to be_a(ActiveRecord::Associations::CollectionProxy)