Coveralls logob
Coveralls logo
  • Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

arenadotio / ocaml-mssql / 158

6 May 2020 - 15:59 coverage: 75.584% (-2.05%) from 77.635%
158

Pull #40

circleci

Brendan Long
Add CHANGES.md
Pull Request #40: Add CHANGES.md

291 of 385 relevant lines covered (75.58%)

418.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.06
/src/client.ml
1
open Core
2×
2
open Async
3
open Freetds
4
open Mssql_error
5

6
type t =
7
  { (* dbprocess will be set to None when closed to prevent null pointer crashes *)
8
    (* The sequencer prevents concurrent use of the DB connection, and also
9
     prevent queries during unrelated transactions. *)
10
    mutable conn : Dblib.dbprocess Sequencer.t option
11
        (* ID used to detect deadlocks when attempting to use an outer DB handle
12
     inside of with_transaction *)
13
  ; transaction_id : Bigint.t
14
        (* Months are sometimes 0-based and sometimes 1-based. See:
15
     http://www.pymssql.org/en/stable/freetds_and_dates.html *)
16
  ; month_offset : int
17
  }
18

19
let next_transaction_id =
20
  let next = ref Bigint.zero in
21
  fun () ->
22
    let current = !next in
46×
23
    (next := Bigint.(one + current));
24
    current
25
;;
26

27
let parent_transactions_key =
28
  Univ_map.Key.create ~name:"mssql_parent_transactions" [%sexp_of: Bigint.Set.t]
1×
29
;;
30

31
let sequencer_enqueue t f =
32
  match t.conn with
279×
33
  | None -> failwith [%here] "Attempt to use closed DB"
!
34
  | Some conn ->
279×
35
    Scheduler.find_local parent_transactions_key
279×
36
    |> (function
37
    | Some parent_transactions when Set.mem parent_transactions t.transaction_id ->
13×
38
      failwith
1×
39
        [%here]
40
        "Attempted to use outer DB handle inside of with_transaction. This would have \
41
         lead to a deadlock."
42
    | _ -> Throttle.enqueue conn f)
278×
43
;;
44

45
let format_query query params =
46
  let params_formatted = List.map params ~f:Db_field.to_string_escaped |> Array.of_list in
377×
47
  let lexbuf = Lexing.from_string query in
377×
48
  Query_parser.main Query_lexer.token lexbuf
377×
49
  |> List.map
374×
50
       ~f:
51
         (let open Query_parser_types in
52
         function
53
         | Other s -> s
16,434×
54
         | Param n ->
328×
55
           (* $1 is the first param *)
56
           let i = n - 1 in
57
           if i < 0
58
           then
59
             failwithf
!
60
               [%here]
61
               ~query
62
               ~params
63
               "Query has param $%d but params should start at $1."
64
               n;
65
           let len = Array.length params_formatted in
327×
66
           if i >= len
327×
67
           then
68
             failwithf
!
69
               [%here]
70
               ~query
71
               ~params
72
               "Query has param $%d but there are only %d params."
73
               n
74
               len;
75
           params_formatted.(i))
325×
76
  |> String.concat ~sep:""
77
;;
78

79
let execute' ?params ~query ~formatted_query ({ month_offset; _ } as t) ~f =
80
  sequencer_enqueue t
275×
81
  @@ fun conn ->
82
  Logger.debug !"Executing query: %s" formatted_query;
274×
83
  In_thread.run
274×
84
  @@ fun () ->
85
  Mssql_error.with_wrap ~query ?params ~formatted_query [%here] (fun () ->
274×
86
      Dblib.cancel conn;
274×
87
      Dblib.sqlexec conn formatted_query;
274×
88
      Iter.from_fun (fun () ->
272×
89
          if Dblib.results conn
772×
90
          then (
500×
91
            let colnames =
92
              Dblib.numcols conn
500×
93
              |> List.range 0
500×
94
              |> List.map ~f:(fun i -> Dblib.colname conn (i + 1))
204×
95
            in
96
            Iter.from_fun (fun () ->
500×
97
                try
880×
98
                  let row = Dblib.nextrow conn in
99
                  let row = Row.create_exn ~month_offset ~colnames row in
382×
100
                  Some row
382×
101
                with
102
                | Caml.Not_found -> None)
498×
103
            |> Option.some)
104
          else None)
270×
105
      |> f)
106
;;
107

108
let execute_multi_result' ?(params = []) conn query =
146×
109
  let formatted_query = format_query query params in
277×
110
  execute' conn ~query ~params ~formatted_query
274×
111
;;
112

113
let execute_multi_result ?params conn query =
114
  execute_multi_result' ?params conn query ~f:(fun result_set ->
186×
115
      IterLabels.map result_set ~f:Iter.to_list |> Iter.to_list)
184×
116
;;
117

118
(* Execute [f iter] for the first result set iterator and throw an exception if there is more than
119
   one result set *)
120
let execute_f' ?params ~f conn query =
121
  let f = Scheduler.preserve_execution_context' f |> Staged.unstage in
91×
122
  execute_multi_result' ?params conn query ~f:(fun result_sets ->
91×
123
      let result =
87×
124
        let input =
125
          IterLabels.head result_sets |> Option.value ~default:IterLabels.empty
87×
126
        in
127
        Thread_safe.block_on_async_exn (fun () -> f input)
87×
128
      in
129
      match IterLabels.length result_sets with
130
      | 0 -> result
85×
UNCOV
131
      | n ->
!
132
        failwithf
133
          [%here]
134
          ~query
135
          ?params
136
          "Mssql.execute expected one result set but got %d result sets"
137
          (n + 1))
138
;;
139

140
let execute_f ?params ~f conn query =
141
  execute_f' ?params conn query ~f:(fun result_set -> f result_set |> return)
90×
142
;;
143

144
let execute ?params conn query = execute_f ?params ~f:Iter.to_list conn query
76×
145

146
let execute_iter ?params ~f conn query =
UNCOV
147
  execute_f ?params ~f:(IterLabels.iter ~f) conn query
!
148
;;
149

150
let execute_fold ?params ~init ~f conn query =
UNCOV
151
  let acc = ref init in
!
UNCOV
152
  execute_iter ?params conn query ~f:(fun row -> acc := f !acc row) >>| fun () -> !acc
!
153
;;
154

155
let execute_map ?params ~f conn query =
156
  execute_f ?params ~f:(Fn.compose Iter.to_list (IterLabels.map ~f)) conn query
14×
157
;;
158

159
let execute_pipe ?params conn query =
160
  Pipe.create_reader ~close_on_exception:false
1×
161
  @@ fun writer ->
162
  Monitor.protect
1×
163
    (fun () ->
164
      execute_f' ?params conn query ~f:(fun rows ->
1×
165
          IterLabels.fold rows ~init:Deferred.unit ~f:(fun acc row ->
1×
166
              acc >>= fun () -> Pipe.write_if_open writer row)))
100×
167
    ~finally:(fun () ->
168
      Pipe.close writer;
1×
169
      Deferred.unit)
1×
170
;;
171

172
let execute_unit ?params conn query =
173
  let%map results = execute_multi_result ?params conn query in
142×
174
  List.iteri results ~f:(fun i ->
141×
175
    function
176
    | [] -> ()
140×
177
    | rows ->
1×
178
      failwithf
179
        [%here]
180
        ~query
181
        ?params
182
        ~results
183
        "Mssql.execute_unit expected no rows but result set %d has %d rows"
184
        i
185
        (List.length rows))
1×
186
;;
187

188
let execute_single ?params conn query =
189
  execute ?params conn query
64×
190
  >>| function
UNCOV
191
  | [] -> None
!
192
  | [ row ] -> Some row
63×
193
  | rows ->
1×
194
    failwithf
195
      [%here]
196
      ~query
197
      ?params
198
      ~results:[ rows ]
199
      "Mssql.execute_single expected 0 or 1 results but got %d rows"
200
      (List.length rows)
1×
201
;;
202

203
let execute_many ~params conn query =
204
  let formatted_query =
1×
205
    List.map params ~f:(format_query query) |> String.concat ~sep:";"
1×
206
  in
207
  execute' conn ~query ~params:(List.concat params) ~formatted_query ~f:(fun result_set ->
1×
208
      IterLabels.map result_set ~f:Iter.to_list |> Iter.to_list)
1×
209
;;
210

211
let begin_transaction conn = execute_unit conn "BEGIN TRANSACTION"
6×
212
let commit conn = execute_unit conn "COMMIT"
4×
213
let rollback conn = execute_unit conn "ROLLBACK"
2×
214

215
let with_transaction' t f =
216
  (* Use the sequencer to prevent any other copies of this DB handle from
217
     executing during the transaction *)
218
  sequencer_enqueue t
4×
219
  @@ fun conn ->
220
  Scheduler.find_local parent_transactions_key
4×
221
  |> Option.value ~default:Bigint.Set.empty
4×
222
  |> Fn.flip Set.add t.transaction_id
4×
223
  |> Option.some
4×
224
  |> Scheduler.with_local parent_transactions_key ~f:(fun () ->
225
         (* Make a new sub-sequencer so our own queries can continue *)
226
         let t =
4×
227
           { t with
228
             conn = Sequencer.create ~continue_on_error:true conn |> Option.some
4×
229
           ; transaction_id = next_transaction_id ()
4×
230
           }
231
         in
232
         let%bind () = begin_transaction t in
4×
233
         let%bind res = f t in
4×
234
         let%map () =
4×
235
           match res with
236
           | Ok _ -> commit t
3×
237
           | Error _ -> rollback t
1×
238
         in
239
         res)
4×
240
;;
241

242
let with_transaction t f =
243
  with_transaction' t (fun t -> Monitor.try_with ~here:[%here] (fun () -> f t))
4×
244
  >>| function
245
  | Ok res -> res
3×
246
  | Error exn -> raise exn
1×
247
;;
248

249
let with_transaction_or_error t f =
UNCOV
250
  with_transaction' t (fun t ->
!
UNCOV
251
      Monitor.try_with_join_or_error ~here:[%here] (fun () -> f t))
!
252
;;
253

254
let rec connect ?(tries = 5) ~host ~db ~user ~password ?port () =
42×
255
  try
42×
256
    let conn =
257
      Dblib.connect
258
        ~user
259
        ~password (* We have issues with anything higher than this *)
260
        ~version:
261
          Dblib.V70
262
          (* Clifford gives FreeTDS conversion errors if we choose anything else,
263
           eg:
264
           ("Error(CONVERSION, \"Some character(s) could not be converted into
265
           client's character set.  Unconverted bytes were changed to question
266
           marks ('?')\")") *)
267
        ~charset:"CP1252"
268
        (* You set ports in FreeTDS by appending them to the host name:
269
           http://www.freetds.org/userguide/portoverride.htm *)
270
        (match port with
271
        | None -> host
42×
UNCOV
272
        | Some port -> sprintf "%s:%d" host port)
!
273
    in
274
    Dblib.use conn db;
42×
275
    conn
42×
276
  with
UNCOV
277
  | exn ->
!
278
    if tries = 0
UNCOV
279
    then raise exn
!
UNCOV
280
    else Logger.info "Retrying Mssql.connect due to exn: %s" (Exn.to_string exn);
!
281
    connect ~tries:(tries - 1) ~host ~db ~user ~password ?port ()
282
;;
283

284
(* These need to be on for some reason, eg: DELETE failed because the following
285
   SET options have incorrect settings: 'ANSI_NULLS, QUOTED_IDENTIFIER,
286
   CONCAT_NULL_YIELDS_NULL, ANSI_WARNINGS, ANSI_PADDING'. Verify that SET
287
   options are correct for use with indexed views and/or indexes on computed
288
   columns and/or filtered indexes and/or query notifications and/or XML data
289
   type methods and/or spatial index operations.*)
290
let init_conn c =
291
  execute_multi_result
42×
292
    c
293
    "SET QUOTED_IDENTIFIER ON\n\
294
    \     SET ANSI_NULLS ON\n\
295
    \     SET ANSI_WARNINGS ON\n\
296
    \     SET ANSI_PADDING ON\n\
297
    \     SET CONCAT_NULL_YIELDS_NULL ON"
298
  |> Deferred.ignore_m
299
;;
300

301
let close ({ conn; _ } as t) =
302
  match conn with
42×
303
  (* already closed *)
UNCOV
304
  | None -> Deferred.unit
!
305
  | Some conn ->
42×
306
    t.conn <- None;
307
    Throttle.enqueue conn @@ fun conn -> In_thread.run (fun () -> Dblib.close conn)
42×
308
;;
309

310
let create ~host ~db ~user ~password ?port () =
311
  let%bind conn =
42×
312
    let%map conn =
313
      In_thread.run (connect ~host ~db ~user ~password ?port)
42×
314
      >>| Sequencer.create ~continue_on_error:true
42×
315
    in
316
    { conn = Some conn; transaction_id = next_transaction_id (); month_offset = 0 }
42×
317
  in
318
  Monitor.try_with ~here:[%here] (fun () ->
42×
319
      (* Since FreeTDS won't tell us if it was compiled with 0-based month or
320
       1-based months, make a query to check when we first startup and keep
321
       track of the offset so we can correct it. *)
322
      let query = "SELECT CAST('2017-02-02' AS DATETIME) AS x" in
42×
323
      execute_single conn query
42×
324
      >>= function
325
      | Some row ->
42×
326
        let month_offset =
327
          Row.datetime_exn row "x"
42×
328
          |> Time.(to_date ~zone:Zone.utc)
42×
329
          |> Date.month
42×
330
          |> function
UNCOV
331
          | Month.Feb -> 0
!
332
          | Month.Jan -> 1
42×
UNCOV
333
          | month ->
!
334
            failwithf
335
              [%here]
336
              ~query
337
              "Expected month index workaround query to return February as either Jan or \
338
               Feb but got %s"
UNCOV
339
              (Month.to_string month)
!
340
        in
341
        let conn = { conn with month_offset } in
42×
342
        init_conn conn >>| fun () -> conn
42×
343
      | None ->
!
344
        failwith
345
          [%here]
346
          ~query
347
          "Expected month index workaround query to return one row but got none")
348
  >>= function
349
  | Ok res -> return res
42×
UNCOV
350
  | Error exn ->
!
UNCOV
351
    let%map () = close conn in
!
UNCOV
352
    raise exn
!
353
;;
354

355
let with_conn ~host ~db ~user ~password ?port f =
356
  let%bind conn = create ~host ~db ~user ~password ?port () in
42×
357
  Monitor.protect (fun () -> f conn) ~finally:(fun () -> close conn)
3×
358
;;
Troubleshooting · Open an Issue · Sales · Support · ENTERPRISE · CAREERS · STATUS
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2023 Coveralls, Inc