Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bindings/ffi_bindings.ml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ module Types (F: Ctypes.TYPE) = struct
let date = constant "MYSQL_TYPE_DATE" int
let datetime = constant "MYSQL_TYPE_DATETIME" int
let timestamp = constant "MYSQL_TYPE_TIMESTAMP" int
let json = constant "MYSQL_TYPE_JSON" int
end

module Stmt_attr = struct
Expand Down
1 change: 1 addition & 0 deletions examples/async/nonblocking_async_example.ml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ let print_row row =
(M.Time.hour t)
(M.Time.minute t)
(M.Time.second t)
| `Json j -> printf "%s\n%!" j
| `Null -> printf "NULL\n%!")
row
();
Expand Down
1 change: 1 addition & 0 deletions examples/blocking/blocking_example.ml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let print_row row =
(M.Time.hour t)
(M.Time.minute t)
(M.Time.second t)
| `Json j -> printf "%s\n%!" j
| `Null -> printf "NULL\n%!")
row

Expand Down
1 change: 1 addition & 0 deletions examples/lwt/nonblocking_lwt_example.ml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ let print_row row =
(M.Time.hour t)
(M.Time.minute t)
(M.Time.second t)
| `Json j -> Lwt_io.printf "%s\n%!" j
| `Null -> Lwt_io.printf "NULL\n%!")
row
Lwt.return_unit
Expand Down
1 change: 1 addition & 0 deletions examples/select/nonblocking_select_example.ml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ let print_row row =
(M.Time.hour t)
(M.Time.minute t)
(M.Time.second t)
| `Json j -> printf "%s\n%!" j
| `Null -> printf "NULL\n%!")
row;
return ()
Expand Down
13 changes: 13 additions & 0 deletions lib/bind.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type buffer_type =
| `Date
| `Datetime
| `Timestamp
| `Json
]

let buffer_type_of_int i =
Expand All @@ -61,6 +62,7 @@ let buffer_type_of_int i =
else if i = date then `Date
else if i = datetime then `Datetime
else if i = timestamp then `Timestamp
else if i = json then `Json
else invalid_arg @@ "unknown buffer type " ^ (string_of_int i)

let yes = '\001'
Expand Down Expand Up @@ -187,3 +189,14 @@ let time b param ~at =
~mysql_type:(type_of_time_kind param.Time.kind)
~unsigned:no
~at

let json b param ~at =
let len = String.length param in
let p = allocate_n char ~count:len in
String.iteri (fun i c -> (p +@ i) <-@ c) param;
bind b
~buffer:(coerce (ptr char) (ptr void) p)
~size:len
~mysql_type:T.Type.string
~unsigned:no
~at
5 changes: 3 additions & 2 deletions lib/common.ml
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ module Stmt = struct
| `Float x -> Bind.float b x ~at
| `String s -> Bind.string b s ~at
| `Bytes s -> Bind.blob b s ~at
| `Time t -> Bind.time b t ~at)
| `Time t -> Bind.time b t ~at
| `Json j -> Bind.json b j ~at)
params;
if B.mysql_stmt_bind_param stmt.raw b.Bind.bind then
`Ok stmt
Expand All @@ -354,7 +355,7 @@ module Stmt = struct
| `Int24 | `Long | `Float -> 4
| `Long_long | `Double -> 8
| `Decimal | `New_decimal | `String | `Var_string
| `Tiny_blob | `Blob | `Medium_blob | `Long_blob | `Bit -> -1
| `Tiny_blob | `Blob | `Medium_blob | `Long_blob | `Bit | `Json -> -1
| `Time | `Date | `Datetime | `Timestamp -> Ctypes.sizeof T.Time.t

let malloc count =
Expand Down
15 changes: 14 additions & 1 deletion lib/field.ml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type value =
| `String of string
| `Bytes of bytes
| `Time of Time.t
| `Json of string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not be better to use the `String case for JSON, as well? I didn't write the original code, but the intention seem to me that the value type only contains the cases which have different representation on the OCaml side, e.g. omitting the various integer variants.

]

type t =
Expand Down Expand Up @@ -65,7 +66,7 @@ let to_time field kind =
; kind
}

type to_string = [`Decimal | `New_decimal | `String | `Var_string | `Bit]
type to_string = [`Decimal | `New_decimal | `String | `Var_string | `Bit | `Json]
type to_blob = [`Tiny_blob | `Blob | `Medium_blob | `Long_blob]
type to_time = [`Time | `Date | `Datetime | `Timestamp]

Expand All @@ -85,6 +86,7 @@ let convert field typ unsigned =
| `Long_long, false -> `Int (Int64.to_int (cast_to int64_t field))
| `Float, _ -> `Float (cast_to float field)
| `Double, _ -> `Float (cast_to double field)
| `Json, _ -> `Json (Bytes.to_string (to_bytes field))
| #to_string, _ -> `String (Bytes.to_string (to_bytes field))
| #to_blob, _ -> `Bytes (to_bytes field)
| #to_time as t, _ -> `Time (to_time field t)
Expand Down Expand Up @@ -124,6 +126,11 @@ let time field =
| `Time t -> t
| _ -> err field ~info:"a time value"

let json field =
match value field with
| `Json j -> j
| _ -> err field ~info:"a json value"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the test case you take into account the possibility that the database server returns a string (or bytes, as I've when testing). That would be needed here, as well. But if you agree to the above, this definition can be omitted.

let int_opt field =
match value field with
| `Int i -> Some i
Expand Down Expand Up @@ -153,3 +160,9 @@ let time_opt field =
| `Time t -> Some t
| `Null -> None
| _ -> err field ~info:"a nullable time value"

let json_opt field =
match value field with
| `Json j -> Some j
| `Null -> None
| _ -> err field ~info:"a nullable json value"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

3 changes: 3 additions & 0 deletions lib/mariadb.ml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module type S = sig
| `String of string
| `Bytes of bytes
| `Time of Time.t
| `Json of string
]

val name : t -> string
Expand All @@ -45,12 +46,14 @@ module type S = sig
val string : t -> string
val bytes : t -> bytes
val time : t -> Time.t
val json : t -> string

val int_opt : t -> int option
val float_opt : t -> float option
val string_opt : t -> string option
val bytes_opt : t -> bytes option
val time_opt : t -> Time.t option
val json_opt : t -> string option
end

module Row : sig
Expand Down
6 changes: 6 additions & 0 deletions lib/mariadb.mli
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ module type S = sig
| `String of string
| `Bytes of bytes
| `Time of Time.t
| `Json of string
]

val name : t -> string
Expand Down Expand Up @@ -85,12 +86,14 @@ module type S = sig
val string : t -> string
val bytes : t -> bytes
val time : t -> Time.t
val json : t -> string

val int_opt : t -> int option
val float_opt : t -> float option
val string_opt : t -> string option
val bytes_opt : t -> bytes option
val time_opt : t -> Time.t option
val json_opt : t -> string option
end

(** A module representing database rows. Rows can be retrieved as different
Expand Down Expand Up @@ -401,6 +404,7 @@ module Nonblocking : sig
| `String of string
| `Bytes of bytes
| `Time of Time.t
| `Json of string
]

val name : t -> string
Expand All @@ -413,12 +417,14 @@ module Nonblocking : sig
val string : t -> string
val bytes : t -> bytes
val time : t -> Time.t
val json : t -> string

val int_opt : t -> int option
val float_opt : t -> float option
val string_opt : t -> string option
val bytes_opt : t -> bytes option
val time_opt : t -> Time.t option
val json_opt : t -> string option
end

module Row : sig
Expand Down
3 changes: 3 additions & 0 deletions lib/nonblocking.ml
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ module type S = sig
| `String of string
| `Bytes of bytes
| `Time of Time.t
| `Json of string
]

val name : t -> string
Expand All @@ -425,12 +426,14 @@ module type S = sig
val string : t -> string
val bytes : t -> bytes
val time : t -> Time.t
val json : t -> string

val int_opt : t -> int option
val float_opt : t -> float option
val string_opt : t -> string option
val bytes_opt : t -> bytes option
val time_opt : t -> Time.t option
val json_opt : t -> string option
end

module Row : sig
Expand Down
95 changes: 95 additions & 0 deletions tests/nonblocking/nonblocking_testsuite.ml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ struct
| `String s -> sprintf "(%S : string)" s
| `Bytes s -> sprintf "(%S : bytes)" (Bytes.to_string s)
| `Time t -> string_of_timestamp t
| `Json j -> sprintf "(%S : json)" j

let equal_float x x' =
abs_float (x -. x') /. (abs_float (x +. x') +. epsilon_float) < 1e-6
Expand All @@ -117,6 +118,8 @@ struct
| `Bytes s, `Bytes s' -> s = s'
| `Bytes _, _ | _, `Bytes _ -> false
| `Time t, `Time t' -> equal_time t t'
| `Json j, `Json j' -> j = j'
| `Json _, _ | _, `Json _ -> false

let assert_field_equal v v' =
if not (equal_field v v') then begin
Expand Down Expand Up @@ -345,10 +348,102 @@ struct
in
(test_integer, test_bigint)

let test_json () =
connect () >>= or_die "connect" >>= fun dbh ->

(* Create a test table with JSON column *)
M.prepare dbh
"CREATE TEMPORARY TABLE ocaml_mariadb_json_test (id integer PRIMARY KEY AUTO_INCREMENT, data JSON)"
>>= or_die "prepare create json table"
>>= fun create_table_stmt ->
execute_no_data create_table_stmt >>= fun () ->

(* Test inserting JSON data *)
M.prepare dbh "INSERT INTO ocaml_mariadb_json_test (data) VALUES (?)"
>>= or_die "prepare insert json"
>>= fun insert_stmt ->

(* Test various JSON types *)
let test_cases = [
{|{"name": "John", "age": 30}|};
{|[1, 2, 3, "four"]|};
{|"simple string"|};
{|42|};
{|true|};
{|null|}
] in

(* Insert all test cases *)
iter_s_list (fun json_data ->
M.Stmt.execute insert_stmt [| `Json json_data |] >>= or_die "insert json"
>|= fun _ -> ()
) test_cases >>= fun () ->

(* Select and verify we can retrieve JSON data *)
M.prepare dbh "SELECT id, data FROM ocaml_mariadb_json_test ORDER BY id"
>>= or_die "prepare select json"
>>= fun select_stmt ->
M.Stmt.execute select_stmt [||] >>= or_die "execute select json" >>= fun res ->

(* Verify we can fetch and access JSON fields *)
let rec verify_rows count =
M.Res.fetch (module M.Row.Array) res >>= or_die "fetch json row" >>= function
| Some row ->
assert (Array.length row = 2);
(* Test that we can access the JSON field using different methods *)
let json_value = match M.Field.value row.(1) with
| `Json j -> j
| `String s -> s (* TiDB/MySQL might return as string *)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also seeing `Bytes here, using MariaDB client/server version 10.11.13.

| _ -> failwith "Expected JSON or String field"
in
(* Verify we got some data back *)
assert (String.length json_value > 0);

(* Test accessor functions *)
let json_direct = M.Field.json row.(1) in
let json_opt = M.Field.json_opt row.(1) in
assert (json_opt = Some json_direct);
assert (String.length json_direct > 0);

verify_rows (count + 1)
| None ->
(* We should have retrieved all our test cases *)
assert (count = List.length test_cases);
return ()
in

verify_rows 0 >>= fun () ->

(* Test JSON functions if supported (optional) *)
(try
M.prepare dbh "SELECT JSON_TYPE(data) FROM ocaml_mariadb_json_test LIMIT 1"
>>= or_die "prepare json type"
>>= fun json_func_stmt ->
M.Stmt.execute json_func_stmt [||] >>= or_die "execute json type" >>= fun res ->
M.Res.fetch (module M.Row.Array) res >>= or_die "fetch json type" >>= function
| Some row ->
let json_type = match M.Field.value row.(0) with
| `Json j -> j
| `String s -> s
| _ -> failwith "Expected JSON or String from JSON_TYPE"
in
(* JSON_TYPE should return something like "OBJECT", "ARRAY", etc. *)
assert (String.length json_type > 0);
M.Stmt.close json_func_stmt >>= or_die "close json func stmt"
| None -> return ()
with
| _ -> return () (* JSON functions might not be supported in all versions *)
) >>= fun () ->

M.Stmt.close select_stmt >>= or_die "close select stmt" >>= fun () ->
M.Stmt.close insert_stmt >>= or_die "close insert stmt" >>= fun () ->
M.close dbh

let main () =
test_server_properties () >>= fun () ->
test_insert_id () >>= fun () ->
test_txn () >>= fun () ->
test_json () >>= fun () ->
test_many_select () >>= fun () ->
test_integer () >>= fun () -> test_bigint ()
end
Loading