From 8b904a2c1d5260a23f9556e6d2a03499e202a50d Mon Sep 17 00:00:00 2001 From: Nathan Sculli Date: Sun, 8 Jul 2018 00:22:18 -0700 Subject: [PATCH 1/6] All the Things - Add an internal ID type to hide the Primary ID column type so we don't run into issues with BIGINT values being used as identifiers. - Remap the module interface to be more idiomatic. --- __tests__/Test_mysql2_error.re | 54 ++++++------ __tests__/parse_response.re | 14 ++-- __tests__/query.re | 118 ++++++++++++++++++-------- __tests__/with_params.re | 22 ++--- examples/prepared_statements.re | 17 ++-- examples/simple.re | 9 +- package.json | 4 +- src/MySql2.re | 141 ++++++-------------------------- src/MySql2.rei | 111 +++++++++++++++++++------ src/MySql2_connection.re | 22 +++++ src/MySql2_error.re | 20 +++-- src/MySql2_id.re | 24 ++++++ src/MySql2_id.rei | 9 ++ src/MySql2_options.re | 18 ++++ src/MySql2_params.re | 5 ++ src/MySql2_response.re | 91 +++++++++++++++++++++ yarn.lock | 4 + 17 files changed, 442 insertions(+), 241 deletions(-) create mode 100644 src/MySql2_connection.re create mode 100644 src/MySql2_id.re create mode 100644 src/MySql2_id.rei create mode 100644 src/MySql2_options.re create mode 100644 src/MySql2_params.re create mode 100644 src/MySql2_response.re diff --git a/__tests__/Test_mysql2_error.re b/__tests__/Test_mysql2_error.re index 6f9ab58..c26e567 100644 --- a/__tests__/Test_mysql2_error.re +++ b/__tests__/Test_mysql2_error.re @@ -1,23 +1,23 @@ open Jest; let connect = () => - MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); + MySql2.Connection.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); describe("MySql2 Error Handling", () => { let conn = connect(); - afterAll(() => MySql2.close(conn)); + afterAll(() => MySql2.Connection.close(conn)); let accessDeniedTest = "Should respond with an access denied error"; testAsync( accessDeniedTest, finish => { - let c = MySql2.connect(~password="s0m3 g@rb@g3 pw", ()); + let c = MySql2.Connection.connect(~password="s0m3 g@rb@g3 pw", ()); let sql = "SELECT 1+1 AS result"; MySql2.execute(c, sql, None, res => switch (res) { - | `Select(_, _) => fail("unexpected_select_result") |> finish - | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish + | `Select(_) => fail("unexpected_select_result") |> finish + | `Mutation(_) => fail("unexpected_mutation_result") |> finish | `Error(e) => Expect.expect(() => raise(e) @@ -36,8 +36,8 @@ describe("MySql2 Error Handling", () => { let sql = "SELECT invalid, AS result"; MySql2.execute(conn, sql, None, res => switch (res) { - | `Select(_, _) => fail("unexpected_select_result") |> finish - | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish + | `Select(_) => fail("unexpected_select_result") |> finish + | `Mutation(_) => fail("unexpected_mutation_result") |> finish | `Error(e) => Expect.expect(() => raise(e) @@ -51,41 +51,47 @@ describe("MySql2 Error Handling", () => { test("Should parse out an empty error with defaults", () => { /* Use raw JS here toe retrieve a garbage object */ - let e = [%raw {| (function () { return { message: "IDKWTM" } })() |} ]; + let e = [%raw {| (function () { return { message: "IDKWTM" } })() |}]; let actual = MySql2_error.fromJs(e); - Expect.expect(() => raise(actual)) + Expect.expect(() => + raise(actual) + ) |> Expect.toThrowMessage("Failure,-2,UNKNOWN - 99999 (99999) - IDKWTM"); }); test("Should return a defaulted error", () => { /* Use raw JS here toe retrieve a garbage object */ - let e = [%raw {| (function () { return {} })()|} ]; + let e = [%raw {| (function () { return {} })()|}]; let actual = MySql2_error.fromJs(e); - Expect.expect(() => raise(actual)) + Expect.expect(() => + raise(actual) + ) |> Expect.toThrowMessage("UNKNOWN - 99999 (99999) - EMPTY_MESSAGE"); }); - test("should give appropriate message when only a sqlState is given", () =>{ + test("should give appropriate message when only a sqlState is given", () => { /* Use raw JS here toe retrieve a garbage object */ - let e = [%raw {| (function () { return { sqlState: "test" } })()|} ]; + let e = [%raw {| (function () { return { sqlState: "test" } })()|}]; let actual = MySql2_error.fromJs(e); - - Expect.expect(() => raise(actual)) + + Expect.expect(() => + raise(actual) + ) |> Expect.toThrowMessage( - "UNKNOWN - 99999 (99999) - EMPTY_MESSAGE - (test)" - ); + "UNKNOWN - 99999 (99999) - EMPTY_MESSAGE - (test)", + ); }); - test("should give appropriate message when only a sqlMessage is given", () =>{ + test("should give appropriate message when only a sqlMessage is given", () => { /* Use raw JS here toe retrieve a garbage object */ - let e = [%raw {| (function () { return { sqlMessage: "test" } })()|} ]; + let e = [%raw {| (function () { return { sqlMessage: "test" } })()|}]; let actual = MySql2_error.fromJs(e); - - Expect.expect(() => raise(actual)) - |> Expect.toThrowMessage( - "UNKNOWN - 99999 (99999) - EMPTY_MESSAGE - test" - ); + + Expect.expect(() => + raise(actual) + ) + |> Expect.toThrowMessage("UNKNOWN - 99999 (99999) - EMPTY_MESSAGE - test"); }); }); diff --git a/__tests__/parse_response.re b/__tests__/parse_response.re index 349546c..f702ac9 100644 --- a/__tests__/parse_response.re +++ b/__tests__/parse_response.re @@ -1,12 +1,14 @@ open Jest; +module Response = MySql2_response; + describe("MySql2.parseResponse", () => { test("Should return an error when given an unexpected boolean.", () => { let invalid = Js.Json.boolean(true); let message = - switch (MySql2.parseResponse(invalid, [||])) { - | `Select(_, _) => Failure("invalid_select_result") - | `Mutation(_, _) => Failure("invalid_mutation_result") + switch (Response.fromDriverResponse(invalid, [||])) { + | `Select(_) => Failure("unexpected_select_result") + | `Mutation(_) => Failure("unexpected_mutation_result") | `Error(e) => e }; Expect.expect(() => @@ -17,9 +19,9 @@ describe("MySql2.parseResponse", () => { test("Should return an error when given an unexpected string", () => { let invalid = Js.Json.string("invalid"); let message = - switch (MySql2.parseResponse(invalid, [||])) { - | `Select(_, _) => Failure("invalid_select_result") - | `Mutation(_, _) => Failure("invalid_mutation_result") + switch (Response.fromDriverResponse(invalid, [||])) { + | `Select(_) => Failure("invalid_select_result") + | `Mutation(_) => Failure("invalid_mutation_result") | `Error(e) => e }; Expect.expect(() => diff --git a/__tests__/query.re b/__tests__/query.re index 02f2963..61311ce 100644 --- a/__tests__/query.re +++ b/__tests__/query.re @@ -1,7 +1,10 @@ open Jest; +[@bs.val] [@bs.scope "Number"] +external max_safe_integer : int = "MAX_SAFE_INTEGER"; + let connect = () => - MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); + MySql2.Connection.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); type insert = { affected_rows: int, @@ -19,27 +22,27 @@ let onSelect = (next, fn, res) => switch (res) { | `Error(e) => raise(e) | `Mutation(_) => fail("unexpected_mutation_result") |> next - | `Select(rows, meta) => fn(rows, meta, next) + | `Select(select) => fn(select, next) }; let onMutation = (next, fn, res) => switch (res) { | `Error(e) => raise(e) - | `Mutation(count, id) => fn(count, id, next) - | `Select(_, _) => fail("unexpected_select_result") |> next + | `Mutation(mutation) => fn(mutation, next) + | `Select(_) => fail("unexpected_select_result") |> next }; describe("Raw SQL Query Test", () => { let conn = connect(); - afterAll(() => MySql2.close(conn)); + afterAll(() => MySql2.Connection.close(conn)); testAsync("Expect a test database to be listed", finish => MySql2.execute( conn, "SHOW DATABASES", None, - onSelect(finish, (rows, _, next) => - rows - |> Js.Array.map(Json.Decode.dict(Json.Decode.string)) + onSelect(finish, (select, next) => + select + |. MySql2.Select.mapDecoder(Json.Decode.dict(Json.Decode.string)) |> Js.Array.map(x => Js.Dict.unsafeGet(x, "Database")) |> Expect.expect |> Expect.toContain("test") @@ -62,35 +65,36 @@ describe("Raw SQL Query Test Sequence", () => { MySql2.execute(conn, "DROP TABLE IF EXISTS `test`.`simple`", None, res => switch (res) { | `Error(e) => raiseError(e) - | `Mutation(_, _) => next() - | `Select(_, _) => failwith("unexpected_select_result") + | `Mutation(_) => next() + | `Select(_) => failwith("unexpected_select_result") } ); let create = next => MySql2.execute(conn, table_sql, None, res => switch (res) { | `Error(e) => raiseError(e) - | `Mutation(_, _) => next() - | `Select(_, _) => failwith("unexpected_select_result") + | `Mutation(_) => next() + | `Select(_) => failwith("unexpected_select_result") } ); beforeAllAsync(finish => drop(() => create(finish))); - afterAll(() => MySql2.close(conn)); + afterAll(() => MySql2.Connection.close(conn)); testAsync("Expect a mutation result for an INSERT query", finish => { let sql = "INSERT INTO `test`.`simple` (`code`) VALUES ('foo')"; MySql2.execute( conn, sql, None, - onMutation( - finish, - (count, id, next) => { - let countIsOne = count == 1; - let idIsOne = id == 1; - Expect.expect([|countIsOne, idIsOne|]) - |> Expect.toBeSupersetOf([|true, true|]) - |> next; - }, + onMutation(finish, (mutation, next) => + ( + MySql2.Mutation.insertId(mutation) + |. Belt.Option.getExn + |. MySql2.Id.toString, + MySql2.Mutation.affectedRows(mutation), + ) + |> Expect.expect + |> Expect.toEqual(("1", 1)) + |> next ), ); }); @@ -100,15 +104,14 @@ describe("Raw SQL Query Test Sequence", () => { conn, sql, None, - onMutation( - finish, - (count, id, next) => { - let countIsOne = count == 1; - let idIsZero = id == 0; - Expect.expect([|countIsOne, idIsZero|]) - |> Expect.toBeSupersetOf([|true, true|]) - |> next; - }, + onMutation(finish, (mutation, next) => + ( + MySql2.Mutation.insertId(mutation), + MySql2.Mutation.affectedRows(mutation), + ) + |> Expect.expect + |> Expect.toEqual((None, 1)) + |> next ), ); }); @@ -119,8 +122,9 @@ describe("Raw SQL Query Test Sequence", () => { conn, sql, None, - onSelect(finish, (rows, _, next) => - Belt_Array.map(rows, decoder) + onSelect(finish, (select, next) => + select + |. MySql2.Select.mapDecoder(decoder) |> Expect.expect |> Expect.toHaveLength(0) |> next @@ -143,8 +147,9 @@ describe("Raw SQL Query Test Sequence", () => { conn, sql, None, - onSelect(finish, (rows, _, next) => - Belt_Array.map(rows, decoder) + onSelect(finish, (select, next) => + select + |. MySql2.Select.mapDecoder(decoder) |> first_row |> Expect.expect |> Expect.toBeSupersetOf([|true, true|]) @@ -152,4 +157,47 @@ describe("Raw SQL Query Test Sequence", () => { ), ); }); + testAsync("Expect a JS representable BIGINT be a string", finish => { + let sql = {j| + INSERT INTO `test`.`simple` (`id`,`code`) + VALUES + (9007199254740991, '2^53-1') + |j}; + + MySql2.execute( + conn, + sql, + None, + onMutation(finish, (mutation, next) => + MySql2.Mutation.insertId(mutation) + |. Belt.Option.getExn + |. MySql2.Id.toString + |> Expect.expect + |> Expect.toBe(max_safe_integer |. string_of_int) + |> next + ), + ); + }); + testAsync("Expect a non-JS representable BIGINT be a string", finish => { + let sql = {j| + INSERT INTO `test`.`simple` (`id`,`code`) + VALUES + (9007199254740993, '2^53-1') + |j}; + + MySql2.execute( + conn, + sql, + None, + onMutation(finish, (mutation, next) => + mutation + |. MySql2.Mutation.insertId + |. Belt.Option.getExn + |. MySql2.Id.toString + |> Expect.expect + |> Expect.toBe("9007199254740993") + |> next + ), + ); + }); }); diff --git a/__tests__/with_params.re b/__tests__/with_params.re index 999ecee..0d6ffe8 100644 --- a/__tests__/with_params.re +++ b/__tests__/with_params.re @@ -1,7 +1,7 @@ open Jest; let connect = () => - MySql2.connect( + MySql2.Connection.connect( ~host="127.0.0.1", ~port=3306, ~user="root", @@ -15,13 +15,13 @@ type result = {result: int}; describe("Test parameter interpolation", () => { let conn = connect(); let decoder = json => Json.Decode.{result: json |> field("result", int)}; - afterAll(() => MySql2.close(conn)); + afterAll(() => MySql2.Connection.close(conn)); describe("Standard (positional) parameters", () => testAsync("Expect parameters to be substituted properly", finish => { let sql = "SELECT 1 + ? + ? AS result"; let params = Some( - `Positional( + MySql2.Params.positional( Belt_Array.map([|5, 6|], Json.Encode.int) |> Json.Encode.jsonArray, ), @@ -29,9 +29,10 @@ describe("Test parameter interpolation", () => { MySql2.execute(conn, sql, params, res => switch (res) { | `Error(e) => raise(e) - | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish - | `Select(rows, _) => - Belt_Array.map(rows, decoder) + | `Mutation(_) => fail("unexpected_mutation_result") |> finish + | `Select(select) => + select + |. MySql2.Select.flatMap((row, _) => row |. decoder) |> Belt_Array.map(_, x => x.result) |> Expect.expect |> Expect.toBeSupersetOf([|12|]) @@ -45,7 +46,7 @@ describe("Test parameter interpolation", () => { let sql = "SELECT :x + :y AS result"; let params = Some( - `Named( + MySql2.Params.named( Json.Encode.object_([ ("x", Json.Encode.int(1)), ("y", Json.Encode.int(2)), @@ -55,9 +56,10 @@ describe("Test parameter interpolation", () => { MySql2.execute(conn, sql, params, res => switch (res) { | `Error(e) => raise(e) - | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish - | `Select(rows, _) => - Belt_Array.map(rows, decoder) + | `Mutation(_) => fail("unexpected_mutation_result") |> finish + | `Select(select) => + select + |. MySql2.Select.mapDecoder(decoder) |> Belt_Array.map(_, x => x.result) |> Expect.expect |> Expect.toBeSupersetOf([|3|]) diff --git a/examples/prepared_statements.re b/examples/prepared_statements.re index ab7c64a..186d06b 100644 --- a/examples/prepared_statements.re +++ b/examples/prepared_statements.re @@ -1,8 +1,9 @@ -let conn = MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); +let conn = + MySql2.Connection.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); let positional = Some( - `Positional( + MySql2.Params.positional( Belt_Array.map([|5, 6|], Json.Encode.int) |> Json.Encode.jsonArray, ), ); @@ -10,14 +11,14 @@ let positional = MySql2.execute(conn, "SELECT 1 + ? + ? AS result", positional, res => switch (res) { | `Error(e) => Js.log2("ERROR: ", e) - | `Mutation(count, id) => Js.log3("Mutation: ", count, id) - | `Select(rows, meta) => Js.log3("Select: ", rows, meta) + | `Mutation(mutation) => Js.log2("Mutation: ", mutation) + | `Select(select) => Js.log2("Select: ", select) } ); let named = Some( - `Named( + MySql2.Params.named( Json.Encode.object_([ ("x", Json.Encode.int(1)), ("y", Json.Encode.int(2)), @@ -28,9 +29,9 @@ let named = MySql2.execute(conn, "SELECT :x + :y AS result", named, res => switch (res) { | `Error(e) => Js.log2("ERROR: ", e) - | `Mutation(count, id) => Js.log3("Mutation: ", count, id) - | `Select(rows, meta) => Js.log3("Select: ", rows, meta) + | `Mutation(mutation) => Js.log2("Mutation: ", mutation) + | `Select(select) => Js.log2("Select: ", select) } ); -MySql2.close(conn); +MySql2.Connection.close(conn); diff --git a/examples/simple.re b/examples/simple.re index 184e83a..bb3e5db 100644 --- a/examples/simple.re +++ b/examples/simple.re @@ -1,10 +1,11 @@ -let conn = MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); +let conn = + MySql2.Connection.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); let test_handler = fun | `Error(e) => Js.log2("ERROR: ", e) - | `Select(rows, meta) => Js.log3("SELECT: ", rows, meta) - | `Mutation(count, id) => Js.log3("MUTATION: ", count, id); + | `Select(select) => Js.log2("SELECT: ", select) + | `Mutation(mutation) => Js.log2("MUTATION: ", mutation); let _ = MySql2.execute(conn, "SHOW DATABASES", None, test_handler); @@ -36,4 +37,4 @@ let _ = let _ = MySql2.execute(conn, "SELECT * FROM test.simple", None, test_handler); -let _ = MySql2.close(conn); +let _ = MySql2.Connection.close(conn); diff --git a/package.json b/package.json index 1f2df77..17c91b2 100644 --- a/package.json +++ b/package.json @@ -43,12 +43,10 @@ }, "devDependencies": { "@glennsl/bs-jest": "^0.4.2", + "bs-platform": "^3.1.5", "coveralls": "^3.0.1", "husky": "^0.14.3", "lint-staged": "^7.1.0", "nyc": "^11.8.0" - }, - "peerDependencies": { - "bs-platform": "^3.1.4" } } diff --git a/src/MySql2.re b/src/MySql2.re index 8423d20..776261e 100644 --- a/src/MySql2.re +++ b/src/MySql2.re @@ -1,103 +1,26 @@ -module Error = MySql2_error; +module Connection = MySql2_connection; -module Connection = { - type t; +module Exn = MySql2_error; - [@bs.deriving abstract] - type config = { - [@bs.optional] - host: string, - [@bs.optional] - port: int, - [@bs.optional] - user: string, - [@bs.optional] - password: string, - [@bs.optional] - database: string, - }; +module Id = MySql2_id; - [@bs.module "mysql2"] external createConnection : config => t = ""; - [@bs.send] external close : t => unit = "end"; +module Params = MySql2_params; - let make = (~host=?, ~port=?, ~user=?, ~password=?, ~database=?, _) => - config(~host?, ~port?, ~user?, ~password?, ~database?, ()) - |> createConnection; -}; +module Mutation = MySql2_response.Mutation; -module Options = { - [@bs.deriving abstract] - type t = { - sql: string, - values: Js.Nullable.t(Js.Json.t), - namedPlaceholders: bool, - }; +module Select = MySql2_response.Select; - let fromParams = (sql, params) => - switch (params) { - | None => t(~sql, ~values=Js.Nullable.null, ~namedPlaceholders=false) - | Some(p) => - switch (p) { - | `Named(json) => - t(~sql, ~values=Js.Nullable.return(json), ~namedPlaceholders=true) - | `Positional(json) => - t(~sql, ~values=Js.Nullable.return(json), ~namedPlaceholders=false) - } - }; -}; +module Response = MySql2_response; + +module Options = MySql2_options; -type connection = Connection.t; -type metaRecord = { - catalog: string, - schema: string, - name: string, - orgName: string, - table: string, - orgTable: string, - characterSet: int, - columnLength: int, - columnType: int, - flags: int, - decimals: int, -}; -type meta = array(metaRecord); -type params = option([ | `Named(Js.Json.t) | `Positional(Js.Json.t)]); -type rows = array(Js.Json.t); type response = [ | `Error(exn) - | `Mutation(int, int) - | `Select(rows, meta) + | `Mutation(Mutation.t) + | `Select(Select.t) ]; -type callback = response => unit; - -let decodeMetaRecord = json => - Json.Decode.{ - catalog: json |> field("catalog", string), - schema: json |> field("schema", string), - name: json |> field("name", string), - orgName: json |> field("orgName", string), - table: json |> field("table", string), - orgTable: json |> field("orgTable", string), - characterSet: json |> field("characterSet", int), - columnLength: json |> field("columnLength", int), - columnType: json |> field("columnType", int), - flags: json |> field("flags", int), - decimals: json |> field("decimals", int), - }; -let decodeResultMutation = json => { - open Json.Decode; - let changes = json |> field("affectedRows", withDefault(0, int)); - let last_id = json |> field("insertId", withDefault(0, int)); - - `Mutation((changes, last_id)); -}; - -let decodeResultSelect = (rows, meta) => - `Select((rows, Belt.Array.map(meta, decodeMetaRecord))); - -let close = Connection.close; -let connect = Connection.make; +type callback = Response.t => unit; [@bs.send] external execute : @@ -119,37 +42,19 @@ external query : unit = "query"; -let parseResponse = (json, meta) => - switch (json |> Js.Json.classify) { - | Js.Json.JSONObject(_) => decodeResultMutation(json) - | Js.Json.JSONArray(rows) => decodeResultSelect(rows, meta) - | _ => - `Error( - Failure( - {| MySql2Error - (UNKNOWN_RESPONSE_TYPE) - invalid_driver_result|}, - ), - ) - }; +let handler = (callback, exn, res, meta) => + ( + switch (exn |. Js.Nullable.toOption) { + | Some(e) => `Error(e |. Exn.fromJs) + | None => Response.fromDriverResponse(res, meta) + } + ) + |. callback; let execute = (conn, sql, params, callback) => { let options = Options.fromParams(sql, params); - let fn = - if (Options.namedPlaceholders(options)) { - execute; - } else { - query; - }; - fn( - conn, - options, - (exn, res, meta) => { - let response = - switch (Js.Nullable.toOption(exn)) { - | Some(e) => `Error(Error.fromJs(e)) - | None => parseResponse(res, meta) - }; - callback(response); - }, - ); + options |. Options.namedPlaceholders ? + execute(conn, options, handler(callback)) : + query(conn, options, handler(callback)); }; diff --git a/src/MySql2.rei b/src/MySql2.rei index 5fef9e3..5cf4de6 100644 --- a/src/MySql2.rei +++ b/src/MySql2.rei @@ -1,35 +1,96 @@ -type params = option([ | `Named(Js.Json.t) | `Positional(Js.Json.t)]); -type rows = array(Js.Json.t); -type metaRecord; -type meta = array(metaRecord); -type connection; +module Connection: { + type t; + let connect: + ( + ~host: string=?, + ~port: int=?, + ~user: string=?, + ~password: string=?, + ~database: string=?, + unit + ) => + t; -module Error: { - let fromJs: Js.Json.t => exn; + let close: t => unit; +}; + +module Exn: {let fromJs: Js.Json.t => exn;}; + +module Id: { + type t = MySql2_id.t; + + let fromJson: Js.Json.t => t; + + let toJson: t => Js.Json.t; + + let toString: t => string; +}; + +module Mutation: { + type t; + + let insertId: t => option(MySql2_id.t); + + let fieldCount: t => int; + + let affectedRows: t => int; + + let info: t => string; + + let serverStatus: t => int; + + let warningStatus: t => int; +}; + +module Params: { + type t; + + let named: Js.Json.t => t; + + let positional: Js.Json.t => t; +}; + +module Select: { + type t; + + module Meta: { + type t; + + let catalog: t => string; + + let schema: t => string; + + let name: t => string; + + let orgName: t => string; + + let table: t => string; + + let characterSet: t => int; + + let columnLength: t => int; + + let columnType: t => int; + + let flags: t => int; + + let decimals: t => int; + }; + + let count: t => int; + + let flatMap: (t, (Js.Json.t, array(Meta.t)) => 'a) => array('a); + + let mapDecoder: (t, Js.Json.t => 'a) => array('a); }; type response = [ | `Error(exn) - | `Mutation(int, int) - | `Select(rows, meta) + | `Mutation(Mutation.t) + | `Select(Select.t) ]; type callback = response => unit; -let close: connection => unit; - -let connect: - ( - ~host: string=?, - ~port: int=?, - ~user: string=?, - ~password: string=?, - ~database: string=?, - unit - ) => - connection; - -let execute: (connection, string, params, callback) => unit; - -let parseResponse: (Js.Json.t, array(Js.Json.t)) => response; +let execute: (Connection.t, string, option(Params.t), callback) => unit; diff --git a/src/MySql2_connection.re b/src/MySql2_connection.re new file mode 100644 index 0000000..bbc1d59 --- /dev/null +++ b/src/MySql2_connection.re @@ -0,0 +1,22 @@ +type t; + +[@bs.deriving abstract] +type config = { + [@bs.optional] + host: string, + [@bs.optional] + port: int, + [@bs.optional] + user: string, + [@bs.optional] + password: string, + [@bs.optional] + database: string, +}; + +[@bs.module "mysql2"] external createConnection : config => t = ""; +[@bs.send] external close : t => unit = "end"; + +let connect = (~host=?, ~port=?, ~user=?, ~password=?, ~database=?, _) => + config(~host?, ~port?, ~user?, ~password?, ~database?, ()) + |> createConnection; diff --git a/src/MySql2_error.re b/src/MySql2_error.re index 7f47570..b6e6b85 100644 --- a/src/MySql2_error.re +++ b/src/MySql2_error.re @@ -1,20 +1,24 @@ -let fromJs = (json) => { +let fromJs = json => { open Json.Decode; let name = json |> withDefault("UNKNOWN", field("name", string)); - let msg = json |> withDefault("EMPTY_MESSAGE", field("message", string)); + let msg = json |> withDefault("EMPTY_MESSAGE", field("message", string)); let code = json |> withDefault("99999", field("code", string)); let errno = json |> withDefault(99999, field("errno", int)); let sqlState = json |> optional(field("sqlState", string)); let sqlMessage = json |> optional(field("sqlMessage", string)); - switch((sqlState, sqlMessage)) { + switch (sqlState, sqlMessage) { | (Some(state), Some(message)) => - Failure({j|$name - $code ($errno) - $msg - ($state) $message|j}) + Failure({j|$name - $code ($errno) - $msg - ($state) $message|j}) | (Some(state), None) => - Failure({j|$name - $code ($errno) - $msg - ($state)|j}) + Failure({j|$name - $code ($errno) - $msg - ($state)|j}) | (None, Some(message)) => - Failure({j|$name - $code ($errno) - $msg - $message|j}) - | (None, None) => - Failure({j|$name - $code ($errno) - $msg|j}) + Failure({j|$name - $code ($errno) - $msg - $message|j}) + | (None, None) => Failure({j|$name - $code ($errno) - $msg|j}) }; }; + +let invalidResponseType = () => + Failure( + {| MySql2Error - (UNKNOWN_RESPONSE_TYPE) - invalid_driver_result |}, + ); diff --git a/src/MySql2_id.re b/src/MySql2_id.re new file mode 100644 index 0000000..c2650e3 --- /dev/null +++ b/src/MySql2_id.re @@ -0,0 +1,24 @@ +type t = string; + +let fromJson = json => { + Js.log2("ID JSON: ", json); + ( + switch (json |. Js.Json.classify) { + | Js.Json.JSONNumber(float) => float |. Js.String.make + | Js.Json.JSONString(string) => string + | _ => failwith("unexpected_identifier_value") + } + ) + |> ( + x => { + Js.log2("PARSED ID: ", x); + x; + } + ); +}; + +let toJson = Js.Json.string; + +let toString = t => t; + +let isZero = t => t == "0"; diff --git a/src/MySql2_id.rei b/src/MySql2_id.rei new file mode 100644 index 0000000..b36d489 --- /dev/null +++ b/src/MySql2_id.rei @@ -0,0 +1,9 @@ +type t; + +let fromJson: Js.Json.t => t; + +let toJson: t => Js.Json.t; + +let toString: t => string; + +let isZero: t => bool; diff --git a/src/MySql2_options.re b/src/MySql2_options.re new file mode 100644 index 0000000..79031c4 --- /dev/null +++ b/src/MySql2_options.re @@ -0,0 +1,18 @@ +[@bs.deriving abstract] +type t = { + sql: string, + values: Js.Nullable.t(Js.Json.t), + namedPlaceholders: bool, +}; + +let fromParams = (sql, params) => + switch (params) { + | None => t(~sql, ~values=Js.Nullable.null, ~namedPlaceholders=false) + | Some(p) => + switch (p) { + | `Named(json) => + t(~sql, ~values=Js.Nullable.return(json), ~namedPlaceholders=true) + | `Positional(json) => + t(~sql, ~values=Js.Nullable.return(json), ~namedPlaceholders=false) + } + }; diff --git a/src/MySql2_params.re b/src/MySql2_params.re new file mode 100644 index 0000000..3ba3a2f --- /dev/null +++ b/src/MySql2_params.re @@ -0,0 +1,5 @@ +type t = [ | `Named(Js.Json.t) | `Positional(Js.Json.t)]; + +let named = json => `Named(json); + +let positional = json => `Positional(json); diff --git a/src/MySql2_response.re b/src/MySql2_response.re new file mode 100644 index 0000000..da57f12 --- /dev/null +++ b/src/MySql2_response.re @@ -0,0 +1,91 @@ +module Select = { + module Meta = { + [@bs.deriving abstract] + type t = { + catalog: string, + schema: string, + name: string, + orgName: string, + table: string, + orgTable: string, + characterSet: int, + columnLength: int, + columnType: int, + flags: int, + decimals: int, + }; + + let make = json => + Json.Decode.( + t( + ~catalog=json |> field("catalog", string), + ~schema=json |> field("schema", string), + ~name=json |> field("name", string), + ~orgName=json |> field("orgName", string), + ~table=json |> field("table", string), + ~orgTable=json |> field("orgTable", string), + ~characterSet=json |> field("characterSet", int), + ~columnLength=json |> field("columnLength", int), + ~columnType=json |> field("columnType", int), + ~flags=json |> field("flags", int), + ~decimals=json |> field("decimals", int), + ) + ); + }; + + type t = { + rows: array(Js.Json.t), + meta: array(Meta.t), + }; + + let make = (rows, meta) => { + rows, + meta: meta |. Belt.Array.map(Meta.make), + }; + + let flatMap = (t, fn) => Belt.Array.map(t.rows, row => fn(row, t.meta)); + + let mapDecoder = (t, decoder) => Belt.Array.map(t.rows, decoder); + + let count = t => Belt.Array.length(t.rows); +}; + +module Mutation = { + [@bs.deriving abstract] + type t = { + insertId: option(MySql2_id.t), + fieldCount: int, + affectedRows: int, + info: string, + serverStatus: int, + warningStatus: int, + }; + + let parseId = json => + Json.Decode.( + json + |> field("insertId", optional(MySql2_id.fromJson)) + |. Belt.Option.flatMap(id => id |. MySql2_id.isZero ? None : Some(id)) + ); + + let make = json => + Json.Decode.( + t( + ~insertId=json |> parseId, + ~fieldCount=json |> field("fieldCount", int), + ~affectedRows=json |> field("affectedRows", int), + ~info=json |> field("info", string), + ~serverStatus=json |> field("serverStatus", int), + ~warningStatus=json |> field("warningStatus", int), + ) + ); +}; + +type t = [ | `Select(Select.t) | `Mutation(Mutation.t) | `Error(exn)]; + +let fromDriverResponse = (response, meta) => + switch (response |. Js.Json.classify) { + | Js.Json.JSONArray(rows) => `Select(Select.make(rows, meta)) + | Js.Json.JSONObject(_) => `Mutation(Mutation.make(response)) + | _ => `Error(MySql2_error.invalidResponseType()) + }; diff --git a/yarn.lock b/yarn.lock index 3350570..4a03bd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -430,6 +430,10 @@ browser-resolve@^1.11.2: dependencies: resolve "1.1.7" +bs-platform@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-3.1.5.tgz#fb34ee4702bc9163848d5537096c4f31ebaeed40" + bser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" From 170b1b4b46c2f6fa3cf4c6df6f9a91363fd74c0c Mon Sep 17 00:00:00 2001 From: Nathan Sculli Date: Sun, 8 Jul 2018 00:28:48 -0700 Subject: [PATCH 2/6] remove peer dependencies --- .travis.yml | 2 -- package.json | 1 - 2 files changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b82888d..b3bfbdc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,3 @@ before_install: - sudo apt-get -qq update - sudo apt-get install -y jq - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' -before_script: - - yarn install:peers diff --git a/package.json b/package.json index 17c91b2..c20810b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "clean": "bsb -clean-world", "coverage": "nyc report --temp-directory=coverage --reporter=text-lcov | coveralls", "format": "yarn bsrefmt --in-place", - "install:peers": "yarn add $(jq -r '.peerDependencies|keys|join(\" \")' package.json)", "precommit": "lint-staged", "start": "bsb -make-world -w", "test": "yarn run clean; yarn run build; jest --coverage && yarn run coverage", From d45dab1ad637c0c3fd9360807c31a47ec738a040 Mon Sep 17 00:00:00 2001 From: Nathan Sculli Date: Sun, 8 Jul 2018 01:38:36 -0700 Subject: [PATCH 3/6] update the README examples to match the new interface. --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d4ee713..3897b5f 100644 --- a/README.md +++ b/README.md @@ -54,13 +54,13 @@ and [Named Placeholders](#named-placeholders). #### Standard Query Method ```reason let conn - = MySql2.connect(~host=127.0.0.1, ~port=3306, ~user="root", ()); + = MySql2.Connection.connect(~host=127.0.0.1, ~port=3306, ~user="root", ()); MySql2.execute(conn, "SHOW DATABASES", None, res => { switch res { | `Error(e) => Js.log2("ERROR: ", e) - | `Select(rows, meta) => Js.log3("SELECT: ", rows, meta) - | `Mutation(count, id) => Js.log3("MUTATION: ", count, id) + | `Select(select) => Js.log2("SELECT: ", select) + | `Mutation(mutation) => Js.log2("MUTATION: ", mutation) } MySql2.close(conn); }); @@ -72,9 +72,9 @@ MySql2.execute(conn, "SHOW DATABASES", None, res => { ##### Named Placeholders ```reason let conn - = MySql2.connect(~host=127.0.0.1, ~port=3306, ~user="root", ()); + = MySql2.Connect.connect(~host=127.0.0.1, ~port=3306, ~user="root", ()); -let named = `Named( +let named = MySql2.Params.named( Json.Encode.object_([ ("x", Json.Encode.int(1)), ("y", Json.Encode.int(2)), @@ -84,8 +84,8 @@ let named = `Named( MySql2.execute(conn, "SELECT :x + :y AS result", Some(named), res => { switch res { | `Error(e) => Js.log2("ERROR: ", e) - | `Select(rows, meta) => Js.log3("SELECT: ", rows, meta) - | `Mutation(count, id) => Js.log3("MUTATION: ", count, id) + | `Select(select) => Js.log2("SELECT: ", select) + | `Mutation(mutation) => Js.log2("MUTATION: ", mutation) } } MySql2.close(conn); @@ -95,17 +95,17 @@ MySql2.execute(conn, "SELECT :x + :y AS result", Some(named), res => { ##### Unnamed Placeholders ```reason let conn - = MySql2.connect(~host=127.0.0.1, ~port=3306, ~user="root", ()); + = MySql2.Connection.connect(~host=127.0.0.1, ~port=3306, ~user="root", ()); -let positional = `Positional( +let positional = MySql2.Params.positional( Belt_Array.map([|5, 6|], Json.Encode.int) |> Json.Encode.jsonArray ); MySql2.execute(conn, "SELECT 1 + ? + ? AS result", Some(positional), res => { switch res { | `Error(e) => Js.log2("ERROR: ", e) - | `Select(rows, meta) => Js.log3("SELECT: ", rows, meta) - | `Mutation(count, id) => Js.log3("MUTATION: ", count, id) + | `Select(rows, meta) => Js.log2("SELECT: ", rows, meta) + | `Mutation(count, id) => Js.log2("MUTATION: ", count, id) } } MySql2.close(conn); From 18464cf0c3d8cc945daa2c06524d87b1fb0da976 Mon Sep 17 00:00:00 2001 From: Nathan Sculli Date: Sun, 8 Jul 2018 01:46:38 -0700 Subject: [PATCH 4/6] Use Js.Float.toString for float conversion. - clean up debug logs --- src/MySql2_id.re | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/MySql2_id.re b/src/MySql2_id.re index c2650e3..162ca1d 100644 --- a/src/MySql2_id.re +++ b/src/MySql2_id.re @@ -1,21 +1,11 @@ type t = string; -let fromJson = json => { - Js.log2("ID JSON: ", json); - ( - switch (json |. Js.Json.classify) { - | Js.Json.JSONNumber(float) => float |. Js.String.make - | Js.Json.JSONString(string) => string - | _ => failwith("unexpected_identifier_value") - } - ) - |> ( - x => { - Js.log2("PARSED ID: ", x); - x; - } - ); -}; +let fromJson = json => + switch (json |. Js.Json.classify) { + | Js.Json.JSONNumber(float) => float |. Js.Float.toString + | Js.Json.JSONString(string) => string + | _ => failwith("unexpected_identifier_value") + }; let toJson = Js.Json.string; From a026d00d8d1b2bab1bfc472a56ef8a82d16b0ce8 Mon Sep 17 00:00:00 2001 From: Nathan Sculli Date: Sun, 8 Jul 2018 02:27:21 -0700 Subject: [PATCH 5/6] raise the coverage bar - add tests for MySql2_id - add tests for MySql2_response --- __tests__/Test_mysql2_id.re | 30 ++++++++++++++++++++++++++++++ __tests__/Test_mysql2_response.re | 31 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 __tests__/Test_mysql2_id.re create mode 100644 __tests__/Test_mysql2_response.re diff --git a/__tests__/Test_mysql2_id.re b/__tests__/Test_mysql2_id.re new file mode 100644 index 0000000..f982881 --- /dev/null +++ b/__tests__/Test_mysql2_id.re @@ -0,0 +1,30 @@ +open Jest; + +describe("MySql2_id", () => { + describe("toJson", () => + test("it should convert an id to a JSON string", () => + MySql2_id.fromJson(42.0 |. Js.Json.number) + |. MySql2_id.toJson + |. Js.Json.decodeString + |. Belt.Option.getExn + |> Expect.expect + |> Expect.toBe("42") + ) + ); + + describe("fromJson", () => { + test("should fail when given a JSON boolean", () => { + let fn = () => Js.Json.boolean(true) |. MySql2_id.fromJson; + + Expect.expect(fn) + |> Expect.toThrowMessage("unexpected_identifier_value"); + }); + + test("should fail when given a JSON array", () => { + let fn = () => Js.Json.array([||]) |. MySql2_id.fromJson; + + Expect.expect(fn) + |> Expect.toThrowMessage("unexpected_identifier_value"); + }); + }); +}); diff --git a/__tests__/Test_mysql2_response.re b/__tests__/Test_mysql2_response.re new file mode 100644 index 0000000..496854f --- /dev/null +++ b/__tests__/Test_mysql2_response.re @@ -0,0 +1,31 @@ +open Jest; + +describe("MySql2_response", () => + describe("Select", () => { + let test_meta = + Json.Encode.( + object_([ + ("catalog", "catalog" |. string), + ("schema", "schema" |. string), + ("name", "name" |. string), + ("orgName", "orgName" |. string), + ("table", "table" |. string), + ("orgTable", "orgTable" |. string), + ("characterSet", 1 |. int), + ("columnLength", 2 |. int), + ("columnType", 3 |. int), + ("flags", 4 |. int), + ("decimals", 5 |. int), + ]) + ); + + describe("count", () => + test("count should return 0 when rows is empty", () => + MySql2_response.Select.make([||], [|test_meta|]) + |. MySql2_response.Select.count + |. Expect.expect + |> Expect.toBe(0) + ) + ); + }) +); From 88548537dcc64cc2cf6428b8fb9f0264ca6d420f Mon Sep 17 00:00:00 2001 From: Nathan Sculli Date: Sun, 8 Jul 2018 18:12:11 -0700 Subject: [PATCH 6/6] Test coverage to 100% --- __tests__/Test_mysql2.re | 226 +++++++++++++++++++++++++++++++++++++++ src/MySql2.rei | 4 + src/MySql2_response.re | 27 +++-- 3 files changed, 250 insertions(+), 7 deletions(-) create mode 100644 __tests__/Test_mysql2.re diff --git a/__tests__/Test_mysql2.re b/__tests__/Test_mysql2.re new file mode 100644 index 0000000..8a76a8f --- /dev/null +++ b/__tests__/Test_mysql2.re @@ -0,0 +1,226 @@ +open Jest; + +let connect = () => + MySql2.Connection.connect( + ~host="127.0.0.1", + ~port=3306, + ~user="root", + ~password="", + ~database="test", + (), + ); + +let table_sql = {| + CREATE TABLE IF NOT EXISTS `test`.`mysql2` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT + , `code` varchar(32) NOT NULL + , `display` varchar(140) DEFAULT NULL + , PRIMARY KEY(`id`) + ) +|}; + +let seed_sql = {| + INSERT IGNORE INTO `test`.`mysql2` + (`id`, `code`, `display`) + VALUES + (1, "batman", "I am, Bat-man"), + (2, "superman", "Truth, Justice and the American Way"), + (3, "wonder_woman", "Suffering Sappho!") +|}; + +let mutationPromise = (db, sql, params) => + Js.Promise.make((~resolve, ~reject) => + MySql2.execute( + db, + sql, + params, + fun + | `Error(e) => reject(. e) + | `Select(_) => reject(. "unexpected_select_result" |. Failure) + | `Mutation(m) => resolve(. m), + ) + |. ignore + ); + +let selectPromise = (db, sql, params) => + Js.Promise.make((~resolve, ~reject) => + MySql2.execute( + db, + sql, + params, + fun + | `Error(e) => reject(. e) + | `Mutation(_) => reject(. "unexpected_mutation_result" |. Failure) + | `Select(select) => resolve(. select), + ) + |. ignore + ); + +describe("MySql2", () => { + let db = ref(None); + + let getDb = () => db^ |. Belt.Option.getExn; + + beforeAllPromise(() => { + db := connect() |. Some; + + mutationPromise(getDb(), "DROP TABLE IF EXISTS `test`.`mysql2`", None) + |> Js.Promise.then_(_ => mutationPromise(getDb(), table_sql, None)) + |> Js.Promise.then_(_ => mutationPromise(getDb(), seed_sql, None)); + }); + + afterAll(() => getDb() |. MySql2.Connection.close); + + describe("Mutation", () => { + let result = ref(None); + + let getResult = () => result^ |. Belt.Option.getExn; + + beforeAllPromise(() => { + let sql = {| + INSERT INTO `test`.`mysql2` + (`id`, `code`, `display`) + VALUES + (4, "flash", "I am the fastest man alive") + |}; + + mutationPromise(getDb(), sql, None) + |> Js.Promise.then_(mutation => { + result := Some(mutation); + Js.Promise.resolve(mutation); + }); + }); + + let makeTest = (label, fn, expected) => + test(label, () => + getResult() |. fn |. Expect.expect |> Expect.toBe(expected) + ); + + describe("fieldCount", () => + makeTest("should return zero", MySql2.Mutation.fieldCount, 0) + ); + + describe("info", () => + makeTest("should return empty string", MySql2.Mutation.info, "") + ); + + describe("serverStatus", () => + makeTest("should return 2", MySql2.Mutation.serverStatus, 2) + ); + + describe("warningStatus", () => + makeTest("should return 0", MySql2.Mutation.warningStatus, 0) + ); + }); + + describe("Select", () => { + let result = ref(None); + + let getResult = () => result^ |. Belt.Option.getExn; + + beforeAllPromise(() => + selectPromise(getDb(), "SELECT * FROM `test`.`mysql2`", None) + |> Js.Promise.then_(select => { + result := Some(select); + Js.Promise.resolve(select); + }) + ); + + let makeMetaTest = (label, fn, expected) => + test(label, () => + getResult() + |. MySql2.Select.meta + |. Belt.Array.map(fn) + |. Expect.expect + |> Expect.toEqual(expected) + ); + + describe("catalog", () => + makeMetaTest( + "should return the string 'def'", + MySql2.Select.Meta.catalog, + [|"def", "def", "def"|], + ) + ); + + describe("schema", () => + makeMetaTest( + "should return the string 'test'", + MySql2.Select.Meta.schema, + [|"test", "test", "test"|], + ) + ); + + describe("name", () => + makeMetaTest( + "should return the column name string", + MySql2.Select.Meta.name, + [|"id", "code", "display"|], + ) + ); + + describe("orgName", () => + makeMetaTest( + "should return the schema defined column name string", + MySql2.Select.Meta.orgName, + [|"id", "code", "display"|], + ) + ); + + describe("table", () => + makeMetaTest( + "should return the table name string", + MySql2.Select.Meta.table, + [|"mysql2", "mysql2", "mysql2"|], + ) + ); + + describe("orgTable", () => + makeMetaTest( + "should return the schema defined table name string", + MySql2.Select.Meta.orgTable, + [|"mysql2", "mysql2", "mysql2"|], + ) + ); + + describe("characterSet", () => + makeMetaTest( + "should return the column character sets", + MySql2.Select.Meta.characterSet, + [|63, 224, 224|], + ) + ); + + describe("columnLength", () => + makeMetaTest( + "should return the length of each column", + MySql2.Select.Meta.columnLength, + [|20, 128, 560|], + ) + ); + + describe("columnType", () => + makeMetaTest( + "should return the internal type code of each column", + MySql2.Select.Meta.columnType, + [|8, 253, 253|], + ) + ); + + describe("flags", () => + makeMetaTest( + "should return the flag set for each column", + MySql2.Select.Meta.flags, + [|16899, 4097, 0|], + ) + ); + + describe("decimals", () => + makeMetaTest( + "should return the decimal count of each column", + MySql2.Select.Meta.decimals, + [|0, 0, 0|], + ) + ); + }); +}); diff --git a/src/MySql2.rei b/src/MySql2.rei index 5cf4de6..ba068be 100644 --- a/src/MySql2.rei +++ b/src/MySql2.rei @@ -67,6 +67,8 @@ module Select: { let table: t => string; + let orgTable: t => string; + let characterSet: t => int; let columnLength: t => int; @@ -78,6 +80,8 @@ module Select: { let decimals: t => int; }; + let meta: t => array(Meta.t); + let count: t => int; let flatMap: (t, (Js.Json.t, array(Meta.t)) => 'a) => array('a); diff --git a/src/MySql2_response.re b/src/MySql2_response.re index da57f12..bd0dc3d 100644 --- a/src/MySql2_response.re +++ b/src/MySql2_response.re @@ -1,4 +1,11 @@ module Select = { + /* https://mariadb.com/kb/en/library/packet_resultset/ */ + /** + * There will be a meta data packet for each column within the + * response. + * @TODO - determine the meaning of the characterSet field + * @TODO - determine the meaning of the flags field. + */ module Meta = { [@bs.deriving abstract] type t = { @@ -33,23 +40,29 @@ module Select = { ); }; + [@bs.deriving abstract] type t = { rows: array(Js.Json.t), meta: array(Meta.t), }; - let make = (rows, meta) => { - rows, - meta: meta |. Belt.Array.map(Meta.make), - }; + let make = (rows, meta) => + t(~rows, ~meta=meta |. Belt.Array.map(Meta.make)); - let flatMap = (t, fn) => Belt.Array.map(t.rows, row => fn(row, t.meta)); + let flatMap = (t, fn) => + Belt.Array.map(t |. rows, row => fn(row, t |. meta)); - let mapDecoder = (t, decoder) => Belt.Array.map(t.rows, decoder); + let mapDecoder = (t, decoder) => Belt.Array.map(t |. rows, decoder); - let count = t => Belt.Array.length(t.rows); + let count = t => Belt.Array.length(t |. rows); }; +/* https://github.com/mysqljs/mysql#getting-the-number-of-changed-rows */ +/* + * @TODO - Add changed rows + * @TODO - determine the meaning of fieldCount + * @TODO - determine the meaning of info + */ module Mutation = { [@bs.deriving abstract] type t = {