Skip to content

Commit afc9c3e

Browse files
committed
Retain bulk execution replies on errors
Expose an optional owned BSON output before propagating the driver error, including through collection sessions. AI disclosure: This work was prepared with assistance from OpenAI Codex.
1 parent d77ad72 commit afc9c3e

4 files changed

Lines changed: 138 additions & 9 deletions

File tree

‎docs/src/api.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ Mongoc.find_one_and_delete
6666
Mongoc.find_one_and_replace
6767
Mongoc.find_one_and_update
6868
Mongoc.replace_one
69+
Mongoc.insert_many
70+
Mongoc.execute!
6971
```
7072

7173
## Aggregation

‎src/collection.jl‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,22 +149,40 @@ function BulkOperationResult(reply::BSON, server_id::UInt32)
149149
BulkOperationResult(reply, server_id, Vector{Union{Nothing, BSONObjectId}}())
150150
end
151151

152-
function execute!(bulk_operation::BulkOperation) :: BulkOperationResult
152+
"""
153+
execute!(bulk_operation::BulkOperation; reply=nothing) :: BulkOperationResult
154+
155+
Execute a bulk operation once and release its native handle, including on failure.
156+
Errors throw `BSONError` even when some writes succeeded.
157+
158+
Pass `reply=Ref{BSON}()` to retain the driver's reply on success or failure. The
159+
reference is assigned after the driver returns, before checking for an error.
160+
It remains unchanged if execution is rejected before calling the driver. A reply
161+
may be empty on failure; it does not by itself indicate success. On success,
162+
`reply[]` is the same document as the returned result's `reply` field. The reply
163+
remains usable after the bulk operation's native handle is released.
164+
"""
165+
function execute!(bulk_operation::BulkOperation;
166+
reply::Union{Nothing, Ref{BSON}}=nothing) :: BulkOperationResult
153167
if bulk_operation.executed
154168
error("Bulk operation was already executed.")
155169
end
156170

157171
try
158-
reply = BSON()
172+
reply_bson = BSON()
159173
err_ref = Ref{BSONError}()
160174

161-
bulk_operation_result = mongoc_bulk_operation_execute(bulk_operation.handle,
162-
reply.handle, err_ref)
175+
bulk_operation_result = GC.@preserve bulk_operation reply_bson begin
176+
mongoc_bulk_operation_execute(bulk_operation.handle, reply_bson.handle, err_ref)
177+
end
178+
if reply !== nothing
179+
reply[] = reply_bson
180+
end
163181

164182
if bulk_operation_result == 0
165183
throw(err_ref[])
166184
end
167-
return BulkOperationResult(reply, bulk_operation_result)
185+
return BulkOperationResult(reply_bson, bulk_operation_result)
168186
finally
169187
destroy!(bulk_operation)
170188
end
@@ -222,8 +240,23 @@ function bulk_update_one!(
222240
end
223241

224242

243+
"""
244+
insert_many(collection, documents::Vector{BSON};
245+
bulk_options=nothing, insert_options=nothing, reply=nothing)
246+
247+
Insert documents into a `Collection` or `CollectionSession`, returning a
248+
`BulkOperationResult`. With `bulk_options=BSON("ordered" => false)`, the server
249+
continues after individual write errors, but this function still throws
250+
`BSONError` if any write fails.
251+
252+
Pass `reply=Ref{BSON}()` to retain the bulk reply when execution throws. The reply
253+
contains counts and `writeErrors` when available; error indices are zero-based
254+
positions in `documents`. Setup errors before bulk execution leave the reference
255+
unchanged. See [`execute!`](@ref) for the reply's lifetime and success behavior.
256+
"""
225257
function insert_many(collection::Collection, documents::Vector{BSON};
226-
bulk_options::Union{Nothing, BSON}=nothing, insert_options::Union{Nothing, BSON}=nothing)
258+
bulk_options::Union{Nothing, BSON}=nothing, insert_options::Union{Nothing, BSON}=nothing,
259+
reply::Union{Nothing, Ref{BSON}}=nothing)
227260

228261
inserted_oids = Vector{Union{Nothing, BSONObjectId}}()
229262

@@ -233,7 +266,7 @@ function insert_many(collection::Collection, documents::Vector{BSON};
233266
bulk_insert!(bulk_operation, doc, options=insert_options)
234267
push!(inserted_oids, inserted_oid)
235268
end
236-
result = execute!(bulk_operation)
269+
result = execute!(bulk_operation; reply=reply)
237270
append!(result.inserted_oids, inserted_oids)
238271
return result
239272
end

‎src/session.jl‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ end
8282

8383
function insert_many(collection::CollectionSession, documents::Vector{BSON};
8484
bulk_options::Union{Nothing, BSON}=nothing,
85-
insert_options::Union{Nothing, BSON}=nothing)
85+
insert_options::Union{Nothing, BSON}=nothing,
86+
reply::Union{Nothing, Ref{BSON}}=nothing)
8687
bulk_options_with_session = _join(bulk_options, get_session(collection))
8788
insert_many(collection.collection, documents,
88-
bulk_options=bulk_options_with_session, insert_options=insert_options)
89+
bulk_options=bulk_options_with_session, insert_options=insert_options, reply=reply)
8990
end
9091

9192
function find(collection::CollectionSession, bson_filter::BSON=BSON();

‎test/mongodb_tests.jl‎

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,99 @@ const DB_NAME = "mongoc"
252252
Mongoc.drop(collection)
253253
end
254254

255+
@testset "Bulk replies on success and failure" begin
256+
for ordered in (true, false), direct in (true, false)
257+
collection = client[DB_NAME]["bulk_reply_$(ordered)_$(direct)"]
258+
bulk = nothing
259+
reply = Ref{Mongoc.BSON}()
260+
try
261+
Mongoc.insert_one(collection, Mongoc.BSON("_id" => 1, "original" => true))
262+
documents = [Mongoc.BSON("_id" => i) for i in 1:3]
263+
err = try
264+
if direct
265+
bulk = Mongoc.BulkOperation(collection; options=Mongoc.BSON("ordered" => ordered))
266+
for document in documents
267+
Mongoc.bulk_insert!(bulk, document)
268+
end
269+
Mongoc.execute!(bulk; reply=reply)
270+
else
271+
Mongoc.insert_many(collection, documents;
272+
bulk_options=Mongoc.BSON("ordered" => ordered), reply=reply)
273+
end
274+
nothing
275+
catch caught
276+
caught
277+
end
278+
@test err isa Mongoc.BSONError
279+
@test err.code == 11000
280+
@test isassigned(reply)
281+
GC.gc()
282+
@test reply[]["nInserted"] == (ordered ? 0 : 2)
283+
errors = reply[]["writeErrors"]
284+
@test length(errors) == 1
285+
@test errors[1]["index"] == 0
286+
@test errors[1]["code"] == 11000
287+
@test Mongoc.count_documents(collection) == (ordered ? 1 : 3)
288+
@test Mongoc.find_one(collection, Mongoc.BSON("_id" => 1))["original"] === true
289+
if direct
290+
@test bulk.executed
291+
@test bulk.handle == C_NULL
292+
retained_reply = reply[]
293+
@test_throws ErrorException Mongoc.execute!(bulk; reply=reply)
294+
@test reply[] === retained_reply
295+
end
296+
@test_throws Mongoc.BSONError Mongoc.insert_many(collection, [Mongoc.BSON("_id" => 1)])
297+
finally
298+
bulk === nothing || Mongoc.destroy!(bulk)
299+
Mongoc.drop(collection)
300+
Mongoc.destroy!(collection)
301+
end
302+
end
303+
304+
collection = client[DB_NAME]["bulk_reply_success"]
305+
try
306+
previous = Mongoc.BSON("original" => true)
307+
reply = Ref(previous)
308+
result = Mongoc.insert_many(collection, [Mongoc.BSON("_id" => 1)]; reply=reply)
309+
@test reply[] === result.reply
310+
@test reply[] !== previous
311+
@test previous["original"] === true
312+
@test reply[]["nInserted"] == 1
313+
@test isempty(reply[]["writeErrors"])
314+
bulk = Mongoc.BulkOperation(collection)
315+
empty_reply = Ref{Mongoc.BSON}()
316+
@test_throws Mongoc.BSONError Mongoc.execute!(bulk; reply=empty_reply)
317+
@test isassigned(empty_reply)
318+
@test isempty(empty_reply[])
319+
@test bulk.handle == C_NULL
320+
finally
321+
Mongoc.drop(collection)
322+
Mongoc.destroy!(collection)
323+
end
324+
325+
options = Mongoc.SessionOptions()
326+
session = Mongoc.Session(client; options=options)
327+
collection = session[DB_NAME]["bulk_reply_session"]
328+
try
329+
reply = Ref{Mongoc.BSON}()
330+
result = Mongoc.insert_many(collection, [Mongoc.BSON("_id" => 1)]; reply=reply)
331+
@test reply[] === result.reply
332+
@test reply[]["nInserted"] == 1
333+
@test_throws Mongoc.BSONError Mongoc.insert_many(collection,
334+
[Mongoc.BSON("_id" => 1), Mongoc.BSON("_id" => 2)];
335+
bulk_options=Mongoc.BSON("ordered" => false), reply=reply)
336+
@test reply[]["nInserted"] == 1
337+
@test reply[]["writeErrors"][1]["index"] == 0
338+
@test Mongoc.count_documents(collection) == 2
339+
finally
340+
Mongoc.drop(collection)
341+
Mongoc.destroy!(collection.collection)
342+
Mongoc.destroy!(collection.database_session.database)
343+
Mongoc.destroy!(session)
344+
Mongoc.destroy!(options)
345+
end
346+
end
347+
255348
@testset "bulk_replace_one" begin
256349
collection = client[DB_NAME]["replace_many"]
257350
push!(collection, Mongoc.BSON("""{ "x": 1, "y": 100 }"""))

0 commit comments

Comments
 (0)