torque-postgresql 2.0.4 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)