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.
- checksums.yaml +4 -4
- data/lib/torque/postgresql.rb +1 -1
- data/lib/torque/postgresql/adapter.rb +16 -0
- data/lib/torque/postgresql/adapter/database_statements.rb +1 -15
- data/lib/torque/postgresql/adapter/schema_dumper.rb +6 -2
- data/lib/torque/postgresql/associations/association.rb +10 -3
- data/lib/torque/postgresql/associations/belongs_to_many_association.rb +165 -48
- data/lib/torque/postgresql/associations/builder/belongs_to_many.rb +6 -5
- data/lib/torque/postgresql/associations/preloader/association.rb +1 -0
- data/lib/torque/postgresql/attributes/builder.rb +1 -1
- data/lib/torque/postgresql/attributes/builder/enum.rb +5 -5
- data/lib/torque/postgresql/attributes/enum.rb +1 -1
- data/lib/torque/postgresql/attributes/enum_set.rb +1 -1
- data/lib/torque/postgresql/autosave_association.rb +16 -19
- data/lib/torque/postgresql/base.rb +9 -2
- data/lib/torque/postgresql/insert_all.rb +26 -0
- data/lib/torque/postgresql/reflection/abstract_reflection.rb +0 -23
- data/lib/torque/postgresql/reflection/association_reflection.rb +22 -0
- data/lib/torque/postgresql/version.rb +1 -1
- data/spec/factories/item.rb +5 -0
- data/spec/models/item.rb +3 -0
- data/spec/schema.rb +9 -1
- data/spec/tests/belongs_to_many_spec.rb +149 -13
- data/spec/tests/has_many_spec.rb +14 -0
- data/spec/tests/insert_all_spec.rb +89 -0
- metadata +17 -13
- data/lib/torque/postgresql/coder.rb +0 -133
- data/spec/tests/coder_spec.rb +0 -367
@@ -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
|
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)
|
37
|
-
suffix = options.fetch(:suffix, nil)
|
36
|
+
prefix = options.fetch(:prefix, nil)
|
37
|
+
suffix = options.fetch(:suffix, nil)
|
38
38
|
|
39
|
-
prefix = attribute
|
40
|
-
suffix =
|
39
|
+
prefix = attribute if prefix == true
|
40
|
+
suffix = attribute if suffix == true
|
41
41
|
|
42
|
-
base =
|
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.
|
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.
|
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)
|
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
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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)
|
data/spec/models/item.rb
ADDED
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 =
|
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
|
29
|
+
before { Video.belongs_to_many(:tags) }
|
30
30
|
subject { Video.create(title: 'A') }
|
31
|
-
|
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.
|
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
|
135
|
-
|
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
|
-
|
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.
|
189
|
-
expect(subject.tags.
|
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
|
-
|
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
|
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
|
-
|
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)
|