1+ require " ../spec_helper"
2+
3+ describe Repo do
4+ before_each do
5+ # Clean up database before each test to ensure isolation
6+ begin
7+ Repo .delete_all(User )
8+ Repo .delete_all(UserRequired )
9+ Repo .delete_all(Post )
10+ Repo .delete_all(Address )
11+ Repo .delete_all(Project )
12+ Repo .delete_all(UserProject )
13+ rescue ex
14+ # Ignore cleanup errors
15+ end
16+ end
17+ describe " insert_all" do
18+ describe " basic functionality" do
19+ it " inserts multiple records with model instances" do
20+ user1 = User .new
21+ user1.name = " Alice"
22+ user1.things = 25
23+
24+ user2 = User .new
25+ user2.name = " Bob"
26+ user2.things = 30
27+
28+ user3 = User .new
29+ user3.name = " Charlie"
30+ user3.things = 35
31+
32+ users = [user1, user2, user3]
33+ result = Repo .insert_all(User , users)
34+
35+ result.should be_a(Crecto ::BulkResult )
36+ result.total_count.should eq(3 )
37+ result.successful_count.should eq(3 )
38+ result.failed_count.should eq(0 )
39+ result.successful?.should be_true
40+ result.inserted_ids.size.should eq(3 )
41+ result.duration_ms.should be > 0
42+
43+ # Verify records were actually inserted
44+ inserted_users = Repo .all(User , Query .where(name: [" Alice" , " Bob" , " Charlie" ]))
45+ inserted_users.size.should eq(3 )
46+ end
47+
48+ it " inserts multiple records with hash data" do
49+ user_data = [
50+ {" name" => " Dave" , " things" => 28 },
51+ {" name" => " Eve" , " things" => 32 }
52+ ]
53+
54+ result = Repo .insert_all(User , user_data)
55+
56+ result.total_count.should eq(2 )
57+ result.successful_count.should eq(2 )
58+ result.failed_count.should eq(0 )
59+ result.successful?.should be_true
60+ result.inserted_ids.size.should eq(2 )
61+ end
62+
63+ it " inserts multiple records with NamedTuple data" do
64+ user_data = [
65+ {name: " Frank" , things: 27 },
66+ {name: " Grace" , things: 33 }
67+ ]
68+
69+ result = Repo .insert_all(User , user_data)
70+
71+ result.total_count.should eq(2 )
72+ result.successful_count.should eq(2 )
73+ result.failed_count.should eq(0 )
74+ result.successful?.should be_true
75+ result.inserted_ids.size.should eq(2 )
76+ end
77+
78+ it " handles empty arrays gracefully" do
79+ expect_raises(ArgumentError , " Records array cannot be empty" ) do
80+ Repo .insert_all(User , [] of User )
81+ end
82+ end
83+ end
84+
85+ describe " validation handling" do
86+ it " fails records with invalid data" do
87+ user1 = UserRequired .new
88+ user1.name = " Valid User"
89+ user1.age = 25
90+ user1.is_admin = false
91+
92+ user2 = UserRequired .new
93+ user2.name = " Invalid User"
94+ # user2.age is nil - should fail validation
95+ # user2.is_admin is nil - should fail validation
96+
97+ users = [user1, user2]
98+
99+ result = Repo .insert_all(UserRequired , users)
100+
101+ result.total_count.should eq(2 )
102+ result.successful_count.should eq(1 )
103+ result.failed_count.should eq(1 )
104+ result.successful?.should be_false
105+ result.partial_success?.should be_true
106+ result.errors.size.should eq(1 )
107+ result.errors.first.index.should eq(1 )
108+ result.errors.first.error_class.should contain(" ValidationError" )
109+ end
110+
111+ it " provides detailed error information" do
112+ user1 = UserRequired .new
113+ user1.name = " Another Valid User"
114+ user1.age = 30
115+ user1.is_admin = false
116+
117+ user2 = UserRequired .new
118+ user2.name = " User with missing fields"
119+ # user2.age is nil - should fail validation
120+ # user2.is_admin is nil - should fail validation
121+
122+ users = [user1, user2]
123+
124+ result = Repo .insert_all(UserRequired , users)
125+
126+ error = result.errors.first
127+ error.index.should eq(1 )
128+ error.error_message.should_not be_empty
129+ error.error_class.should_not be_empty
130+ error.timestamp.should be_a(Time )
131+ error.record_hash.should_not be_nil
132+ end
133+ end
134+
135+ describe " transaction handling" do
136+ it " handles atomic operations correctly" do
137+ # This test depends on the database adapter's behavior
138+ # PostgreSQL and MySQL should handle multi-row inserts atomically
139+ # SQLite3 uses transaction-wrapped individual inserts
140+
141+ user1 = User .new
142+ user1.name = " Transaction Test 1"
143+ user1.things = 25
144+
145+ user2 = User .new
146+ user2.name = " Transaction Test 2"
147+ user2.things = 30
148+
149+ users = [user1, user2]
150+ result = Repo .insert_all(User , users)
151+
152+ # Both should succeed in normal conditions
153+ result.successful_count.should eq(2 )
154+ result.failed_count.should eq(0 )
155+
156+ # Verify atomicity - either all or nothing should be inserted
157+ count = Repo .aggregate(User , :count , :id ,
158+ Query .where(" name LIKE ?" , " Transaction Test%" ))
159+ count.should eq(2 )
160+ end
161+
162+ it " works within existing transactions" do
163+ Repo .transaction! do |tx |
164+ user1 = User .new
165+ user1.name = " Transaction User 1"
166+ user1.things = 40
167+
168+ user2 = User .new
169+ user2.name = " Transaction User 2"
170+ user2.things = 45
171+
172+ users = [user1, user2]
173+ result = tx.insert_all(User , users)
174+ result.successful?.should be_true
175+ result.successful_count.should eq(2 )
176+ end
177+
178+ # Verify records were committed
179+ count = Repo .aggregate(User , :count , :id ,
180+ Query .where(" name LIKE ?" , " Transaction User%" ))
181+ count.should eq(2 )
182+ end
183+ end
184+
185+ describe " performance characteristics" do
186+ it " handles larger datasets efficiently" do
187+ # Create a reasonable number of test records
188+ users = Array (User ).new(50 ) do |i |
189+ user = User .new
190+ user.name = " Perf User #{ i } "
191+ user.things = 20 + (i % 50 )
192+ user
193+ end
194+
195+ start_time = Time .local
196+ result = Repo .insert_all(User , users)
197+ duration = Time .local - start_time
198+
199+ result.successful?.should be_true
200+ result.successful_count.should eq(50 )
201+ result.duration_ms.should be > 0
202+
203+ # Should complete in reasonable time (adjust threshold as needed)
204+ duration.total_seconds.should be < 5.0
205+
206+ # Verify all records were inserted
207+ count = Repo .aggregate(User , :count , :id ,
208+ Query .where(" name LIKE ?" , " Perf User%" ))
209+ count.should eq(50 )
210+ end
211+ end
212+
213+ describe " error recovery" do
214+ it " handles partial failures gracefully" do
215+ # Mix valid and invalid records
216+ user1 = UserRequired .new
217+ user1.name = " Valid 1"
218+ user1.age = 25
219+ user1.is_admin = false
220+
221+ user2 = UserRequired .new
222+ user2.name = " Invalid 1"
223+ # user2.age is nil - should fail validation
224+ # user2.is_admin is nil - should fail validation
225+
226+ user3 = UserRequired .new
227+ user3.name = " Valid 2"
228+ user3.age = 30
229+ user3.is_admin = false
230+
231+ user4 = UserRequired .new
232+ user4.name = " Invalid 2"
233+ # user4.age is nil - should fail validation
234+ # user4.is_admin is nil - should fail validation
235+
236+ user5 = UserRequired .new
237+ user5.name = " Valid 3"
238+ user5.age = 35
239+ user5.is_admin = false
240+
241+ users = [user1, user2, user3, user4, user5]
242+
243+ result = Repo .insert_all(UserRequired , users)
244+
245+ result.total_count.should eq(5 )
246+ result.successful_count.should eq(3 )
247+ result.failed_count.should eq(2 )
248+ result.partial_success?.should be_true
249+ result.successful?.should be_false
250+ result.errors.size.should eq(2 )
251+
252+ # Verify error indices are correct
253+ failed_indices = result.errors.map(& .index).sort
254+ failed_indices.should eq([1 , 3 ])
255+ end
256+
257+ it " provides recovery guidance for common errors" do
258+ # This tests the error classification logic
259+ user1 = UserRequired .new
260+ user1.name = " Test User"
261+ user1.age = 25
262+ user1.is_admin = false
263+
264+ user2 = UserRequired .new
265+ user2.name = " Invalid User"
266+ # user2.age is nil - should fail validation
267+ # user2.is_admin is nil - should fail validation
268+
269+ users = [user1, user2]
270+
271+ result = Repo .insert_all(UserRequired , users)
272+ error = result.errors.first
273+
274+ # Check error classification
275+ error.error_class.should_not be_empty
276+ error.error_message.should_not be_empty
277+
278+ # Should be able to determine error type
279+ error.data_type_error?.should be_true # Validation errors are data type errors
280+ error.constraint_violation?.should be_false
281+ error.connection_error?.should be_false
282+ end
283+ end
284+
285+ describe " insert_all! (strict version)" do
286+ it " raises exception on any failure" do
287+ user1 = UserRequired .new
288+ user1.name = " Valid User"
289+ user1.age = 25
290+ user1.is_admin = false
291+
292+ user2 = UserRequired .new
293+ user2.name = " Invalid User"
294+ # user2.age is nil - should fail validation
295+ # user2.is_admin is nil - should fail validation
296+
297+ users = [user1, user2]
298+
299+ expect_raises(Exception ) do
300+ Repo .insert_all!(UserRequired , users)
301+ end
302+ end
303+
304+ it " succeeds when all records are valid" do
305+ user1 = UserRequired .new
306+ user1.name = " Strict User 1"
307+ user1.age = 22
308+ user1.is_admin = false
309+
310+ user2 = UserRequired .new
311+ user2.name = " Strict User 2"
312+ user2.age = 28
313+ user2.is_admin = false
314+
315+ users = [user1, user2]
316+
317+ result = Repo .insert_all!(UserRequired , users)
318+ result.successful?.should be_true
319+ result.successful_count.should eq(2 )
320+ end
321+ end
322+ end
323+
324+ describe " bulk insert integration" do
325+ it " works with models that have associations" do
326+ # Test with Post model which has a belongs_to relationship
327+ post1 = Post .new
328+ post2 = Post .new
329+
330+ result = Repo .insert_all(Post , [post1, post2])
331+ result.successful?.should be_true
332+ result.successful_count.should eq(2 )
333+ end
334+
335+ it " handles different data types correctly" do
336+ # Test with various data types using User model
337+ user1 = User .new
338+ user1.name = " User 1"
339+ user1.things = 100
340+ user1.yep = true
341+
342+ user2 = User .new
343+ user2.name = " User 2"
344+ user2.things = 200
345+ user2.yep = false
346+
347+ result = Repo .insert_all(User , [user1, user2])
348+ result.successful?.should be_true
349+ result.successful_count.should eq(2 )
350+ end
351+ end
352+ end
0 commit comments