Skip to content

Commit 5690858

Browse files
authored
Merge pull request #72 from jongleb/prepare-not-prepare
Add exec for queries without prepared statements
2 parents e5ae0de + c8bc14c commit 5690858

8 files changed

Lines changed: 219 additions & 7 deletions

File tree

bindings/ffi_bindings.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,18 @@ module Functions (F : Ctypes.FOREIGN) = struct
230230
let mysql_num_fields = foreign "mysql_num_fields"
231231
(res @-> returning int)
232232

233+
let mysql_affected_rows = foreign "mysql_affected_rows"
234+
(mysql @-> returning ullong)
235+
236+
let mysql_insert_id = foreign "mysql_insert_id"
237+
(mysql @-> returning ullong)
238+
239+
let mysql_field_count = foreign "mysql_field_count"
240+
(mysql @-> returning uint)
241+
242+
let mysql_use_result = foreign "mysql_use_result"
243+
(mysql @-> returning res_opt)
244+
233245
let mysql_errno = foreign "mysql_errno"
234246
(mysql @-> returning int)
235247

lib/binding_wrappers.ml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ let mysql_stmt_affected_rows stmt =
5151
let mysql_stmt_insert_id stmt =
5252
Unsigned.ULLong.to_int @@ B.mysql_stmt_insert_id stmt
5353

54+
let mysql_affected_rows mysql =
55+
Unsigned.ULLong.to_int @@ B.mysql_affected_rows mysql
56+
57+
let mysql_insert_id mysql =
58+
Unsigned.ULLong.to_int @@ B.mysql_insert_id mysql
59+
60+
let mysql_field_count mysql =
61+
Unsigned.UInt.to_int @@ B.mysql_field_count mysql
62+
5463
(* Blocking API *)
5564

5665
let mysql_real_connect mysql host user pass db port socket flags =
@@ -229,9 +238,8 @@ let mysql_stmt_next_result_start stmt =
229238
let mysql_stmt_next_result_cont stmt status =
230239
handle_int (fun err -> B.mysql_stmt_next_result_cont err stmt status)
231240

232-
let mysql_real_query_start mysql query =
233-
let len = Unsigned.ULong.of_int (String.length query) in
234-
let query = char_ptr_buffer_of_string query in
241+
let mysql_real_query_start mysql query len =
242+
let len = Unsigned.ULong.of_int len in
235243
handle_int (fun err -> B.mysql_real_query_start err mysql query len)
236244

237245
let mysql_real_query_cont mysql status =

lib/blocking.ml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ type client_option = Common.client_option =
7171
type server_option = Common.server_option =
7272
| Multi_statements of bool
7373

74+
type exec_result = Common.exec_result =
75+
{ affected_rows : int
76+
; insert_id : int
77+
}
78+
7479
let close mariadb =
7580
B.mysql_close mariadb.Common.raw
7681

@@ -158,6 +163,18 @@ let prepare mariadb query =
158163
let start_txn mariadb =
159164
wrap_unit mariadb (B.mysql_real_query mariadb.Common.raw "START TRANSACTION")
160165

166+
let exec mariadb query =
167+
match wrap_unit mariadb (B.mysql_real_query mariadb.Common.raw query) with
168+
| Error e -> Error e
169+
| Ok () ->
170+
match Common.query_result mariadb with
171+
| Ok result -> Ok result
172+
| Error (Some res) ->
173+
B.mysql_free_result res;
174+
Error (0, "exec: statement returned a result set, use prepare")
175+
| Error None ->
176+
Error (Common.error mariadb)
177+
161178
module Res = struct
162179
type t = [`Blocking] Common.Res.t
163180

lib/common.ml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,22 @@ type server_option =
8282

8383
type error = int * string
8484

85+
type exec_result =
86+
{ affected_rows : int
87+
; insert_id : int
88+
}
89+
8590
let error mariadb =
8691
(B.mysql_errno mariadb.raw, B.mysql_error mariadb.raw)
8792

93+
let query_result mariadb =
94+
if B.mysql_field_count mariadb.raw = 0 then
95+
Ok { affected_rows = B.mysql_affected_rows mariadb.raw
96+
; insert_id = B.mysql_insert_id mariadb.raw
97+
}
98+
else
99+
Error (B.mysql_use_result mariadb.raw)
100+
88101
let int_of_server_option = function
89102
| Multi_statements true -> T.Server_options.multi_statements_on
90103
| Multi_statements false -> T.Server_options.multi_statements_off

lib/mariadb.ml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ module type S = sig
174174
val commit : t -> unit result
175175
val rollback : t -> unit result
176176
val prepare : t -> string -> Stmt.t result
177+
178+
type exec_result = { affected_rows : int; insert_id : int }
179+
180+
val exec : t -> string -> exec_result result
177181
end
178182

179183
module B = Binding_wrappers

lib/mariadb.mli

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,15 @@ module type S = sig
300300
(** [prepare mariadb query] creates a prepared statement for [query]. The
301301
query may contain [?] as placeholders for parameters that can be bound
302302
by calling [Stmt.execute]. *)
303+
304+
type exec_result = { affected_rows : int; insert_id : int }
305+
(** The result of a statement executed by {!exec}. The fields have the
306+
same meaning as {!Res.affected_rows} and {!Res.insert_id}. *)
307+
308+
val exec : t -> string -> exec_result result
309+
(** [exec mariadb query] executes [query] using the text protocol, without
310+
creating a prepared statement on the server. [query] must be a single
311+
statement without [?] placeholders and must not return a result set. *)
303312
end
304313

305314
(** The module for blocking MariaDB API calls. It should be possible to call
@@ -550,6 +559,10 @@ module Nonblocking : sig
550559
val commit : t -> unit result future
551560
val rollback : t -> unit result future
552561
val prepare : t -> string -> Stmt.t result future
562+
563+
type exec_result = { affected_rows : int; insert_id : int }
564+
565+
val exec : t -> string -> exec_result result future
553566
end
554567

555568
(** Functor that generates a nonblocking database interface, given a

lib/nonblocking.ml

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,28 @@ let rollback_cont mariadb status =
177177
let rollback mariadb =
178178
(rollback_start mariadb, rollback_cont mariadb)
179179

180-
let start_txn_start mariadb =
181-
handle_int mariadb (B.mysql_real_query_start mariadb.Common.raw "START TRANSACTION")
180+
type text_query =
181+
{ query : char Ctypes.ptr
182+
; len : int
183+
}
184+
185+
let text_query query =
186+
{ query = char_ptr_buffer_of_string query
187+
; len = String.length query
188+
}
189+
190+
let real_query_start mariadb q =
191+
handle_int mariadb (B.mysql_real_query_start mariadb.Common.raw q.query q.len)
182192

183-
let start_txn_cont mariadb status =
193+
let real_query_cont mariadb _q status =
184194
handle_int mariadb (B.mysql_real_query_cont mariadb.Common.raw status)
185195

196+
let real_query mariadb query =
197+
let q = text_query query in
198+
(real_query_start mariadb q, real_query_cont mariadb q)
199+
186200
let start_txn mariadb =
187-
(start_txn_start mariadb, start_txn_cont mariadb)
201+
real_query mariadb "START TRANSACTION"
188202

189203
let build_stmt mariadb raw =
190204
`Ok (Common.Stmt.init mariadb raw)
@@ -554,6 +568,10 @@ module type S = sig
554568
val commit : t -> unit result future
555569
val rollback : t -> unit result future
556570
val prepare : t -> string -> Stmt.t result future
571+
572+
type exec_result = { affected_rows : int; insert_id : int }
573+
574+
val exec : t -> string -> exec_result result future
557575
end
558576

559577
module Make (W : Wait) : S with type 'a future = 'a W.IO.future = struct
@@ -764,4 +782,25 @@ module Make (W : Wait) : S with type 'a future = 'a W.IO.future = struct
764782
match prepare m q with
765783
| `Ok nb -> nonblocking m nb
766784
| `Error e -> return (Error e)
785+
786+
type exec_result = Common.exec_result =
787+
{ affected_rows : int
788+
; insert_id : int
789+
}
790+
791+
let handle_exec m =
792+
match Common.query_result m with
793+
| Ok result ->
794+
return (Ok result)
795+
| Error (Some res) ->
796+
nonblocking' m (Res.free res) >>= fun () ->
797+
return (Error (0, "exec: statement returned a result set, use prepare"))
798+
| Error None ->
799+
return (Error (Common.error m))
800+
801+
let exec m q =
802+
nonblocking m (real_query m q)
803+
>>= function
804+
| Ok () -> handle_exec m
805+
| Error _ as e -> return e
767806
end

tests/nonblocking/nonblocking_testsuite.ml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,110 @@ struct
219219

220220
M.close dbh
221221

222+
let test_exec () =
223+
connect () >>= or_die "connect" >>= fun dbh ->
224+
225+
M.exec dbh
226+
"CREATE TEMPORARY TABLE ocaml_mariadb_test \
227+
(id integer PRIMARY KEY AUTO_INCREMENT, v integer, s text)"
228+
>>= or_die "exec create" >>= fun res ->
229+
assert (res.M.affected_rows = 0);
230+
assert (res.M.insert_id = 0);
231+
232+
M.exec dbh "INSERT INTO ocaml_mariadb_test (v) VALUES (10), (20), (30)"
233+
>>= or_die "exec multi-row insert" >>= fun res ->
234+
assert (res.M.affected_rows = 3);
235+
assert (res.M.insert_id = 1);
236+
237+
M.exec dbh "INSERT INTO ocaml_mariadb_test (v) VALUES (40)"
238+
>>= or_die "exec single-row insert" >>= fun res ->
239+
assert (res.M.affected_rows = 1);
240+
assert (res.M.insert_id = 4);
241+
242+
M.exec dbh "UPDATE ocaml_mariadb_test SET v = v + 1 WHERE v >= 30"
243+
>>= or_die "exec update" >>= fun res ->
244+
assert (res.M.affected_rows = 2);
245+
assert (res.M.insert_id = 0);
246+
247+
M.exec dbh
248+
"INSERT INTO ocaml_mariadb_test (id, v) VALUES (2, 25) \
249+
ON DUPLICATE KEY UPDATE v = 25"
250+
>>= or_die "exec insert on duplicate key update" >>= fun res ->
251+
assert (res.M.affected_rows = 2);
252+
253+
M.exec dbh "DELETE FROM ocaml_mariadb_test WHERE v = 10"
254+
>>= or_die "exec delete" >>= fun res ->
255+
assert (res.M.affected_rows = 1);
256+
257+
M.exec dbh "SELECT v FROM ocaml_mariadb_test"
258+
>>= (function
259+
| Ok _ -> die_f "exec of a SELECT unexpectedly succeeded"
260+
| Error _ -> return ()) >>= fun () ->
261+
262+
M.prepare dbh "SELECT v FROM ocaml_mariadb_test WHERE id = 3"
263+
>>= or_die "prepare after rejected exec"
264+
>>= fun stmt ->
265+
M.Stmt.execute stmt [||] >>= or_die "execute" >>= fun res ->
266+
fetch_single_row res >>= fun row ->
267+
assert (Array.length row = 1 && M.Field.int row.(0) = 31);
268+
M.Stmt.close stmt >>= or_die "Stmt.close" >>= fun () ->
269+
270+
M.exec dbh "MALFORMED STATEMENT"
271+
>>= (function
272+
| Ok _ -> die_f "exec of a malformed statement unexpectedly succeeded"
273+
| Error (errno, _) ->
274+
assert (errno = 1064);
275+
return ()) >>= fun () ->
276+
277+
let n = 16 * 1024 in
278+
let pad = String.make 256 'x' in
279+
let buf = Buffer.create (n * 280) in
280+
Buffer.add_string buf "INSERT INTO ocaml_mariadb_test (v, s) VALUES ";
281+
for i = 0 to n - 1 do
282+
if i > 0 then Buffer.add_char buf ',';
283+
bprintf buf "(%d, '%s')" i pad
284+
done;
285+
M.exec dbh (Buffer.contents buf)
286+
>>= or_die "exec bulk insert" >>= fun res ->
287+
assert (res.M.affected_rows = n);
288+
assert (res.M.insert_id = 5);
289+
290+
M.close dbh
291+
292+
let test_exec_no_stmt_prepare () =
293+
connect () >>= or_die "connect" >>= fun dbh ->
294+
M.prepare dbh
295+
"SELECT VARIABLE_VALUE FROM information_schema.SESSION_STATUS \
296+
WHERE VARIABLE_NAME = 'Com_stmt_prepare'"
297+
>>= or_die "prepare status" >>= fun status_stmt ->
298+
let stmt_prepare_count () =
299+
M.Stmt.execute status_stmt [||] >>= or_die "execute status" >>= fun res ->
300+
if M.Res.num_rows res = 0 then return None else
301+
fetch_single_row res >|= fun row ->
302+
Some (int_of_string (M.Field.string row.(0)))
303+
in
304+
stmt_prepare_count () >>= (function
305+
| None -> return ()
306+
| Some count ->
307+
M.exec dbh "CREATE TEMPORARY TABLE ocaml_mariadb_test (v integer)"
308+
>>= or_die "exec create" >>= fun _ ->
309+
repeat 10
310+
(fun () ->
311+
M.exec dbh "INSERT INTO ocaml_mariadb_test (v) VALUES (1), (2)"
312+
>>= or_die "exec insert" >>= fun res ->
313+
assert (res.M.affected_rows = 2);
314+
return ()) >>= fun () ->
315+
stmt_prepare_count () >>= fun count' ->
316+
assert (count' = Some count);
317+
M.prepare dbh "SELECT v FROM ocaml_mariadb_test WHERE v = ?"
318+
>>= or_die "prepare control" >>= fun control_stmt ->
319+
stmt_prepare_count () >>= fun count'' ->
320+
assert (count'' = Some (count + 1));
321+
M.Stmt.close control_stmt >>= or_die "Stmt.close control")
322+
>>= fun () ->
323+
M.Stmt.close status_stmt >>= or_die "Stmt.close status" >>= fun () ->
324+
M.close dbh
325+
222326
(* Make sure the conversion between timestamps and strings are consistent
223327
* between MariaDB and OCaml. By sending timestamps to be compared as binary
224328
* and as string, this also verifies the MYSQL_TIME encoding. *)
@@ -449,6 +553,8 @@ struct
449553
test_server_properties () >>= fun () ->
450554
test_insert_id () >>= fun () ->
451555
test_txn () >>= fun () ->
556+
test_exec () >>= fun () ->
557+
test_exec_no_stmt_prepare () >>= fun () ->
452558
test_json () >>= fun () ->
453559
test_many_select () >>= fun () ->
454560
test_integer () >>= fun () -> test_bigint ()

0 commit comments

Comments
 (0)