1060 lines
37 KiB
OCaml
1060 lines
37 KiB
OCaml
|
|
(* Copyright (C) 2017--2025 Petter A. Urkedal <paurkedal@gmail.com>
|
||
|
|
*
|
||
|
|
* This library is free software; you can redistribute it and/or modify it
|
||
|
|
* under the terms of the GNU Lesser General Public License as published by
|
||
|
|
* the Free Software Foundation, either version 3 of the License, or (at your
|
||
|
|
* option) any later version, with the LGPL-3.0 Linking Exception.
|
||
|
|
*
|
||
|
|
* This library is distributed in the hope that it will be useful, but WITHOUT
|
||
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||
|
|
* License for more details.
|
||
|
|
*
|
||
|
|
* You should have received a copy of the GNU Lesser General Public License
|
||
|
|
* and the LGPL-3.0 Linking Exception along with this library. If not, see
|
||
|
|
* <http://www.gnu.org/licenses/> and <https://spdx.org>, respectively.
|
||
|
|
*)
|
||
|
|
|
||
|
|
[@@@alert "-caqti_private"]
|
||
|
|
open Caqti_template
|
||
|
|
open Caqti_platform
|
||
|
|
open Printf
|
||
|
|
module Pg = Postgresql
|
||
|
|
|
||
|
|
let ( |>? ) = Result.bind
|
||
|
|
let ( %>? ) f g x = match f x with Ok y -> g y | Error _ as r -> r
|
||
|
|
|
||
|
|
let pct_encoder =
|
||
|
|
Uri.pct_encoder ~query_value:(`Custom (`Query_value, "", "=")) ()
|
||
|
|
|
||
|
|
module Q = struct
|
||
|
|
open Caqti_template.Create
|
||
|
|
|
||
|
|
let start = static T.(unit -->. unit) "BEGIN"
|
||
|
|
let commit = static T.(unit -->. unit) "COMMIT"
|
||
|
|
let rollback = static T.(unit -->. unit) "ROLLBACK"
|
||
|
|
|
||
|
|
let type_oid =
|
||
|
|
static T.(string -->? int)
|
||
|
|
"SELECT oid FROM pg_catalog.pg_type WHERE typname = ?"
|
||
|
|
|
||
|
|
let set_timezone_to_utc =
|
||
|
|
direct T.(unit -->. unit) "SET TimeZone TO 'UTC'"
|
||
|
|
|
||
|
|
let set_statement_timeout t =
|
||
|
|
direct_gen T.(unit -->. unit) @@ fun _ ->
|
||
|
|
(* Parameters are not supported for SET. *)
|
||
|
|
Q.lit "SET statement_timeout TO " @++ Q.int t
|
||
|
|
end
|
||
|
|
|
||
|
|
type Caqti_error.msg +=
|
||
|
|
| Connect_error_msg of {
|
||
|
|
error: Pg.error;
|
||
|
|
}
|
||
|
|
| Connection_error_msg of {
|
||
|
|
error: Pg.error;
|
||
|
|
connection_status: Pg.connection_status;
|
||
|
|
}
|
||
|
|
| Result_error_msg of {
|
||
|
|
error_message: string;
|
||
|
|
sqlstate: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
let extract_connect_error error = Connect_error_msg {error}
|
||
|
|
|
||
|
|
let extract_communication_error connection error =
|
||
|
|
Connection_error_msg {
|
||
|
|
error;
|
||
|
|
connection_status = connection#status;
|
||
|
|
}
|
||
|
|
|
||
|
|
let extract_result_error result =
|
||
|
|
Result_error_msg {
|
||
|
|
error_message = result#error;
|
||
|
|
sqlstate = result#error_field Pg.Error_field.SQLSTATE;
|
||
|
|
}
|
||
|
|
|
||
|
|
let () =
|
||
|
|
let pp ppf = function
|
||
|
|
| Connect_error_msg {error; _} | Connection_error_msg {error; _} ->
|
||
|
|
Format.pp_print_string ppf (Pg.string_of_error error)
|
||
|
|
| Result_error_msg {error_message; _} ->
|
||
|
|
Format.pp_print_string ppf error_message
|
||
|
|
| _ ->
|
||
|
|
assert false
|
||
|
|
in
|
||
|
|
let cause = function
|
||
|
|
| Result_error_msg {sqlstate; _} ->
|
||
|
|
Postgresql_conv.cause_of_sqlstate sqlstate
|
||
|
|
| _ ->
|
||
|
|
assert false
|
||
|
|
in
|
||
|
|
Caqti_error.define_msg ~pp [%extension_constructor Connect_error_msg];
|
||
|
|
Caqti_error.define_msg ~pp [%extension_constructor Connection_error_msg];
|
||
|
|
Caqti_error.define_msg ~pp ~cause [%extension_constructor Result_error_msg]
|
||
|
|
|
||
|
|
let driver_info =
|
||
|
|
let dummy_dialect =
|
||
|
|
Dialect.create_pgsql
|
||
|
|
~server_version:(Version.of_string_unsafe "")
|
||
|
|
~client_library:`postgresql
|
||
|
|
()
|
||
|
|
in
|
||
|
|
Caqti_driver_info.of_dialect dummy_dialect
|
||
|
|
|
||
|
|
module Pg_ext = struct
|
||
|
|
include Postgresql_conv
|
||
|
|
|
||
|
|
(* Turns a constant into [(must_quote, encode)]. Note the slight difference
|
||
|
|
* from encoded parameters like booleans and quoting. *)
|
||
|
|
let query_string_of_value
|
||
|
|
: type a. Pg.connection -> a Field_type.t -> bool * (a -> string) =
|
||
|
|
fun db ->
|
||
|
|
let escape_string s = db#escape_string s in
|
||
|
|
(function
|
||
|
|
| Bool -> (false, string_of_bool)
|
||
|
|
| Int -> (false, string_of_int)
|
||
|
|
| Int16 -> (false, string_of_int)
|
||
|
|
| Int32 -> (false, Int32.to_string)
|
||
|
|
| Int64 -> (false, Int64.to_string)
|
||
|
|
| Float -> (false, Float.to_string)
|
||
|
|
| String -> (true, escape_string)
|
||
|
|
| Octets -> (true, escape_string)
|
||
|
|
| Pdate -> (true, Conv.iso8601_of_pdate)
|
||
|
|
| Ptime -> (true, pgstring_of_ptime)
|
||
|
|
| Ptime_span -> (true, pgstring_of_ptime_span)
|
||
|
|
| Enum _ -> (true, escape_string))
|
||
|
|
|
||
|
|
let query_string ~subst (db : Pg.connection) templ =
|
||
|
|
let buf = Buffer.create 64 in
|
||
|
|
let rec loop : Query.t -> _ = function
|
||
|
|
| L s -> Buffer.add_string buf s
|
||
|
|
| Q s ->
|
||
|
|
Buffer.add_char buf '\'';
|
||
|
|
Buffer.add_string buf (db#escape_string s);
|
||
|
|
Buffer.add_char buf '\''
|
||
|
|
| V (ft, v) ->
|
||
|
|
let quote, conv = query_string_of_value db ft in
|
||
|
|
if quote then Buffer.add_char buf '\'';
|
||
|
|
Buffer.add_string buf (conv v);
|
||
|
|
if quote then Buffer.add_char buf '\''
|
||
|
|
| P i -> bprintf buf "$%d" (i + 1)
|
||
|
|
| E _ -> assert false
|
||
|
|
| S frags -> List.iter loop frags
|
||
|
|
in
|
||
|
|
loop (Query.expand ~final:true subst templ);
|
||
|
|
Buffer.contents buf
|
||
|
|
|
||
|
|
let escaped_connvalue s =
|
||
|
|
let buf = Buffer.create (String.length s) in
|
||
|
|
let aux = function
|
||
|
|
| '\\' -> Buffer.add_string buf {|\\|}
|
||
|
|
| '\'' -> Buffer.add_string buf {|\'|}
|
||
|
|
| ch -> Buffer.add_char buf ch in
|
||
|
|
String.iter aux s;
|
||
|
|
Buffer.contents buf
|
||
|
|
|
||
|
|
let pop_uri_param present absent param uri =
|
||
|
|
(match Uri.get_query_param uri param with
|
||
|
|
| None ->
|
||
|
|
Ok (absent, uri)
|
||
|
|
| Some value_str ->
|
||
|
|
(match present value_str with
|
||
|
|
| value -> Ok (value, Uri.remove_query_param uri param)
|
||
|
|
| exception Failure msg ->
|
||
|
|
let msg = Caqti_error.Msg msg in
|
||
|
|
Error (Caqti_error.connect_rejected ~uri msg)))
|
||
|
|
|
||
|
|
let parse_notice_processing = function
|
||
|
|
| "quiet" -> `Quiet
|
||
|
|
| "stderr" -> `Stderr
|
||
|
|
| _ -> failwith "Invalid argument for notice_processing."
|
||
|
|
|
||
|
|
let parse_uri uri =
|
||
|
|
pop_uri_param parse_notice_processing `Quiet "notice_processing" uri
|
||
|
|
|>? fun (notice_processing, uri) ->
|
||
|
|
pop_uri_param bool_of_string false "use_single_row_mode" uri
|
||
|
|
|>? fun (use_single_row_mode, uri) ->
|
||
|
|
let conninfo =
|
||
|
|
if Uri.host uri <> None then Uri.to_string ~pct_encoder uri else
|
||
|
|
let mkparam k v = k ^ " = '" ^ escaped_connvalue v ^ "'" in
|
||
|
|
let mkparams (k, vs) = List.map (mkparam k) vs in
|
||
|
|
String.concat " " (List.flatten (List.map mkparams (Uri.query uri)))
|
||
|
|
in
|
||
|
|
Ok (conninfo, notice_processing, use_single_row_mode)
|
||
|
|
end
|
||
|
|
|
||
|
|
let bool_oid = Pg.oid_of_ftype Pg.BOOL
|
||
|
|
let int2_oid = Pg.oid_of_ftype Pg.INT2
|
||
|
|
let int4_oid = Pg.oid_of_ftype Pg.INT4
|
||
|
|
let int8_oid = Pg.oid_of_ftype Pg.INT8
|
||
|
|
let float8_oid = Pg.oid_of_ftype Pg.FLOAT8
|
||
|
|
let bytea_oid = Pg.oid_of_ftype Pg.BYTEA
|
||
|
|
let date_oid = Pg.oid_of_ftype Pg.DATE
|
||
|
|
let timestamp_oid = Pg.oid_of_ftype Pg.TIMESTAMPTZ
|
||
|
|
let interval_oid = Pg.oid_of_ftype Pg.INTERVAL
|
||
|
|
let unknown_oid = Pg.oid_of_ftype Pg.UNKNOWN
|
||
|
|
|
||
|
|
let init_param_types ~type_oid_cache =
|
||
|
|
let oid_of_field_type : type a. a Field_type.t -> _ = function
|
||
|
|
| Bool -> Ok bool_oid
|
||
|
|
| Int -> Ok int8_oid
|
||
|
|
| Int16 -> Ok int2_oid
|
||
|
|
| Int32 -> Ok int4_oid
|
||
|
|
| Int64 -> Ok int8_oid
|
||
|
|
| Float -> Ok float8_oid
|
||
|
|
| String -> Ok unknown_oid
|
||
|
|
| Octets -> Ok bytea_oid
|
||
|
|
| Pdate -> Ok date_oid
|
||
|
|
| Ptime -> Ok timestamp_oid
|
||
|
|
| Ptime_span -> Ok interval_oid
|
||
|
|
| Enum name -> Ok (Hashtbl.find type_oid_cache name)
|
||
|
|
in
|
||
|
|
let rec recurse : type a. _ -> _ -> a Row_type.t -> _ -> _
|
||
|
|
= fun pt bp -> function
|
||
|
|
| Field ft -> fun i ->
|
||
|
|
oid_of_field_type ft |>? fun oid ->
|
||
|
|
pt.(i) <- oid;
|
||
|
|
bp.(i) <- oid = bytea_oid;
|
||
|
|
Ok (i + 1)
|
||
|
|
| Option t ->
|
||
|
|
recurse pt bp t
|
||
|
|
| Product (_, prod) ->
|
||
|
|
let rec loop : type i. (i, a) Row_type.product -> _ = function
|
||
|
|
| Proj_end -> Result.ok
|
||
|
|
| Proj (t, _, prod) -> recurse pt bp t %>? loop prod
|
||
|
|
in
|
||
|
|
loop prod
|
||
|
|
| Annot (_, t0) ->
|
||
|
|
recurse pt bp t0
|
||
|
|
in
|
||
|
|
fun pt bp t ->
|
||
|
|
recurse pt bp t 0 |>? fun np ->
|
||
|
|
assert (np = Array.length pt);
|
||
|
|
assert (np = Array.length bp);
|
||
|
|
Ok ()
|
||
|
|
|
||
|
|
module type STRING_ENCODER = sig
|
||
|
|
val encode_string : string -> string
|
||
|
|
val encode_octets : string -> string
|
||
|
|
end
|
||
|
|
|
||
|
|
module Make_encoder (String_encoder : STRING_ENCODER) = struct
|
||
|
|
open String_encoder
|
||
|
|
|
||
|
|
let encode_field : type a. a Field_type.t -> a -> string =
|
||
|
|
fun field_type x ->
|
||
|
|
(match field_type with
|
||
|
|
| Bool -> Pg_ext.pgstring_of_bool x
|
||
|
|
| Int -> string_of_int x
|
||
|
|
| Int16 -> string_of_int x
|
||
|
|
| Int32 -> Int32.to_string x
|
||
|
|
| Int64 -> Int64.to_string x
|
||
|
|
| Float -> sprintf "%.17g" x
|
||
|
|
| String -> encode_string x
|
||
|
|
| Enum _ -> encode_string x
|
||
|
|
| Octets -> encode_octets x
|
||
|
|
| Pdate -> Conv.iso8601_of_pdate x
|
||
|
|
| Ptime -> Pg_ext.pgstring_of_ptime x
|
||
|
|
| Ptime_span -> Pg_ext.pgstring_of_ptime_span x)
|
||
|
|
|
||
|
|
let encode ~uri params t x =
|
||
|
|
let write_value ~uri:_ ft fv i =
|
||
|
|
let s = encode_field ft fv in
|
||
|
|
params.(i) <- s; i + 1
|
||
|
|
in
|
||
|
|
let write_null ~uri:_ _ i = i + 1 in
|
||
|
|
try
|
||
|
|
let n = Request_utils.encode_param ~uri {write_value; write_null} t x 0 in
|
||
|
|
assert (n = Array.length params);
|
||
|
|
Ok ()
|
||
|
|
with Caqti_error.Exn (#Caqti_error.call as err) -> Error err
|
||
|
|
end
|
||
|
|
|
||
|
|
module Param_encoder = Make_encoder (struct
|
||
|
|
let encode_string s = s
|
||
|
|
let encode_octets s = s
|
||
|
|
end)
|
||
|
|
|
||
|
|
let decode_field : type a. uri: Uri.t -> a Field_type.t -> string -> a =
|
||
|
|
fun ~uri field_type s ->
|
||
|
|
let wrap_conv_exn f s =
|
||
|
|
(try (f s) with
|
||
|
|
| _ ->
|
||
|
|
let msg = Caqti_error.Msg (sprintf "Invalid value %S." s) in
|
||
|
|
let typ = Row_type.field field_type in
|
||
|
|
Request_utils.raise_decode_rejected ~uri ~typ msg)
|
||
|
|
in
|
||
|
|
let wrap_conv_res f s =
|
||
|
|
(match f s with
|
||
|
|
| Ok y -> y
|
||
|
|
| Error msg ->
|
||
|
|
let msg = Caqti_error.Msg msg in
|
||
|
|
let typ = Row_type.field field_type in
|
||
|
|
Request_utils.raise_decode_rejected ~uri ~typ msg)
|
||
|
|
in
|
||
|
|
(match field_type with
|
||
|
|
| Bool -> wrap_conv_exn Pg_ext.bool_of_pgstring s
|
||
|
|
| Int -> wrap_conv_exn int_of_string s
|
||
|
|
| Int16 -> wrap_conv_exn int_of_string s
|
||
|
|
| Int32 -> wrap_conv_exn Int32.of_string s
|
||
|
|
| Int64 -> wrap_conv_exn Int64.of_string s
|
||
|
|
| Float -> wrap_conv_exn float_of_string s
|
||
|
|
| String -> s
|
||
|
|
| Enum _ -> s
|
||
|
|
| Octets -> Postgresql.unescape_bytea s
|
||
|
|
| Pdate -> wrap_conv_res Conv.pdate_of_iso8601 s
|
||
|
|
| Ptime -> wrap_conv_res Conv.ptime_of_rfc3339_utc s
|
||
|
|
| Ptime_span -> wrap_conv_res Pg_ext.ptime_span_of_pgstring s)
|
||
|
|
|
||
|
|
let decode_row ~uri row_type =
|
||
|
|
let read_value ~uri ft (resp, i, j) =
|
||
|
|
let y = decode_field ~uri ft (resp#getvalue i j) in
|
||
|
|
(y, (resp, i, j + 1))
|
||
|
|
in
|
||
|
|
let skip_null n (resp, i, j) =
|
||
|
|
let j' = j + n in
|
||
|
|
let rec check k = k = j' || resp#getisnull i k && check (k + 1) in
|
||
|
|
if check j then Some (resp, i, j') else None
|
||
|
|
in
|
||
|
|
let decode = Request_utils.decode_row ~uri {read_value; skip_null} row_type in
|
||
|
|
fun (resp, i) ->
|
||
|
|
(match decode (resp, i, 0) with
|
||
|
|
| (y, (_, _, j)) -> assert (j = Row_type.length row_type); Ok y
|
||
|
|
| exception Caqti_error.Exn (`Decode_rejected _ as err) -> Error err)
|
||
|
|
|
||
|
|
type request_info = {
|
||
|
|
query_name: string;
|
||
|
|
query: string;
|
||
|
|
param_length: int;
|
||
|
|
param_types: Pg.oid array;
|
||
|
|
binary_params: bool array;
|
||
|
|
}
|
||
|
|
|
||
|
|
module Pcache =
|
||
|
|
Request_cache.Make (struct type t = request_info let weight _ = 1 end)
|
||
|
|
|
||
|
|
module Connect_functor
|
||
|
|
(System : Caqti_platform.System_sig.S)
|
||
|
|
(System_unix : Caqti_platform_unix.System_sig.S
|
||
|
|
with type 'a fiber := 'a System.Fiber.t
|
||
|
|
and type stdenv := System.stdenv) =
|
||
|
|
struct
|
||
|
|
open System
|
||
|
|
open System.Fiber.Infix
|
||
|
|
open System_utils.Monad_syntax (System.Fiber)
|
||
|
|
open System_unix
|
||
|
|
module H = Connection_utils.Make_helpers (System)
|
||
|
|
|
||
|
|
let ( let/? ) m f = match m with Ok x -> f x | Error _ as r -> Fiber.return r
|
||
|
|
let ( >|>=? ) m f = m >|= function Ok x -> f x | Error _ as r -> r
|
||
|
|
let ( let+*? ) = ( >|>=? )
|
||
|
|
|
||
|
|
let driver_info = driver_info
|
||
|
|
|
||
|
|
module Pg_io = struct
|
||
|
|
|
||
|
|
let communicate ~stdenv db step =
|
||
|
|
let aux fd =
|
||
|
|
let rec loop = function
|
||
|
|
| Pg.Polling_reading ->
|
||
|
|
let* _ = Unix.poll ~stdenv ~read:true fd in
|
||
|
|
(match step () with
|
||
|
|
| exception Pg.Error msg -> Fiber.return (Error msg)
|
||
|
|
| ps -> loop ps)
|
||
|
|
| Pg.Polling_writing ->
|
||
|
|
let* _ = Unix.poll ~stdenv ~write:true fd in
|
||
|
|
(match step () with
|
||
|
|
| exception Pg.Error msg -> Fiber.return (Error msg)
|
||
|
|
| ps -> loop ps)
|
||
|
|
| Pg.Polling_failed | Pg.Polling_ok ->
|
||
|
|
Fiber.return (Ok ())
|
||
|
|
in
|
||
|
|
loop Pg.Polling_writing
|
||
|
|
in
|
||
|
|
(match db#socket with
|
||
|
|
| exception Pg.Error msg -> Fiber.return (Error msg)
|
||
|
|
| socket -> Unix.wrap_fd aux (Obj.magic socket))
|
||
|
|
|
||
|
|
let get_next_result ~stdenv ~uri ~query db =
|
||
|
|
let rec retry fd =
|
||
|
|
db#consume_input;
|
||
|
|
if db#is_busy then
|
||
|
|
Unix.poll ~stdenv ~read:true fd >>= (fun _ -> retry fd)
|
||
|
|
else
|
||
|
|
Fiber.return (Ok db#get_result)
|
||
|
|
in
|
||
|
|
try Unix.wrap_fd retry (Obj.magic db#socket)
|
||
|
|
with Pg.Error err ->
|
||
|
|
let msg = extract_communication_error db err in
|
||
|
|
Fiber.return (Error (Caqti_error.request_failed ~uri ~query msg))
|
||
|
|
|
||
|
|
let get_one_result ~stdenv ~uri ~query db =
|
||
|
|
get_next_result ~stdenv ~uri ~query db >>=? function
|
||
|
|
| None ->
|
||
|
|
let msg = Caqti_error.Msg "No response received after send." in
|
||
|
|
Fiber.return (Error (Caqti_error.request_failed ~uri ~query msg))
|
||
|
|
| Some result ->
|
||
|
|
Fiber.return (Ok result)
|
||
|
|
|
||
|
|
let get_final_result ~stdenv ~uri ~query db =
|
||
|
|
get_one_result ~stdenv ~uri ~query db >>=? fun result ->
|
||
|
|
get_next_result ~stdenv ~uri ~query db >>=? function
|
||
|
|
| None ->
|
||
|
|
Fiber.return (Ok result)
|
||
|
|
| Some _ ->
|
||
|
|
let msg = Caqti_error.Msg "More than one response received." in
|
||
|
|
Fiber.return (Error (Caqti_error.response_rejected ~uri ~query msg))
|
||
|
|
|
||
|
|
let check_query_result ~uri ~query ~row_mult ~single_row_mode result =
|
||
|
|
let reject msg =
|
||
|
|
let msg = Caqti_error.Msg msg in
|
||
|
|
Error (Caqti_error.response_rejected ~uri ~query msg)
|
||
|
|
in
|
||
|
|
let fail msg =
|
||
|
|
let msg = Caqti_error.Msg msg in
|
||
|
|
Error (Caqti_error.request_failed ~uri ~query msg)
|
||
|
|
in
|
||
|
|
(match result#status with
|
||
|
|
| Pg.Command_ok ->
|
||
|
|
(match Row_mult.expose row_mult with
|
||
|
|
| `Zero -> Ok ()
|
||
|
|
| (`One | `Zero_or_one | `Zero_or_more) ->
|
||
|
|
reject "Tuples expected for this query.")
|
||
|
|
| Pg.Tuples_ok ->
|
||
|
|
if single_row_mode then
|
||
|
|
if result#ntuples = 0 then Ok () else
|
||
|
|
reject "Tuples returned in single-row-mode."
|
||
|
|
else
|
||
|
|
(match Row_mult.expose row_mult with
|
||
|
|
| `Zero ->
|
||
|
|
if result#ntuples = 0 then Ok () else
|
||
|
|
reject "No tuples expected for this query."
|
||
|
|
| `One ->
|
||
|
|
if result#ntuples = 1 then Ok () else
|
||
|
|
ksprintf reject "Received %d tuples, expected one."
|
||
|
|
result#ntuples
|
||
|
|
| `Zero_or_one ->
|
||
|
|
if result#ntuples <= 1 then Ok () else
|
||
|
|
ksprintf reject "Received %d tuples, expected at most one."
|
||
|
|
result#ntuples
|
||
|
|
| `Zero_or_more -> Ok ())
|
||
|
|
| Pg.Empty_query -> fail "The query was empty."
|
||
|
|
| Pg.Bad_response ->
|
||
|
|
let msg = extract_result_error result in
|
||
|
|
Error (Caqti_error.response_rejected ~uri ~query msg)
|
||
|
|
| Pg.Fatal_error ->
|
||
|
|
let msg = extract_result_error result in
|
||
|
|
Error (Caqti_error.request_failed ~uri ~query msg)
|
||
|
|
| Pg.Nonfatal_error -> Ok () (* TODO: Log *)
|
||
|
|
| Pg.Copy_out | Pg.Copy_in | Pg.Copy_both ->
|
||
|
|
reject "Received unexpected copy response."
|
||
|
|
| Pg.Single_tuple ->
|
||
|
|
if not single_row_mode then
|
||
|
|
reject "Received unexpected single tuple response." else
|
||
|
|
if result#ntuples <> 1 then
|
||
|
|
reject "Expected a single row in single-row mode." else
|
||
|
|
Ok ())
|
||
|
|
|
||
|
|
let check_command_result ~uri ~query result =
|
||
|
|
check_query_result
|
||
|
|
~uri ~query ~row_mult:Row_mult.zero ~single_row_mode:false result
|
||
|
|
end
|
||
|
|
|
||
|
|
(* Driver Interface *)
|
||
|
|
|
||
|
|
module type CONNECTION = Caqti_connection_sig.S
|
||
|
|
with type 'a fiber := 'a Fiber.t
|
||
|
|
and type ('a, 'err) stream := ('a, 'err) Stream.t
|
||
|
|
|
||
|
|
module Make_connection_base
|
||
|
|
(Connection_arg : sig
|
||
|
|
val stdenv : stdenv
|
||
|
|
val dialect : Dialect.t
|
||
|
|
val subst : Query.subst
|
||
|
|
val uri : Uri.t
|
||
|
|
val db : Pg.connection
|
||
|
|
val use_single_row_mode : bool
|
||
|
|
val dynamic_capacity : int
|
||
|
|
end) =
|
||
|
|
struct
|
||
|
|
open Connection_arg
|
||
|
|
|
||
|
|
let dialect = dialect
|
||
|
|
|
||
|
|
module Copy_encoder = Make_encoder (struct
|
||
|
|
|
||
|
|
let encode_string s =
|
||
|
|
let buf = Buffer.create (String.length s) in
|
||
|
|
for i = 0 to String.length s - 1 do
|
||
|
|
(match s.[i] with
|
||
|
|
| '\\' -> Buffer.add_string buf "\\\\"
|
||
|
|
| '\n' -> Buffer.add_string buf "\\n"
|
||
|
|
| '\r' -> Buffer.add_string buf "\\r"
|
||
|
|
| '\t' -> Buffer.add_string buf "\\t"
|
||
|
|
| c -> Buffer.add_char buf c)
|
||
|
|
done;
|
||
|
|
Buffer.contents buf
|
||
|
|
|
||
|
|
let encode_octets s = encode_string (db#escape_bytea s)
|
||
|
|
end)
|
||
|
|
|
||
|
|
let in_use = ref false
|
||
|
|
let in_transaction = ref false
|
||
|
|
let pcache : Pcache.t = Pcache.create ~dynamic_capacity dialect
|
||
|
|
|
||
|
|
let wrap_pg ~query f =
|
||
|
|
try Ok (f ()) with
|
||
|
|
| Postgresql.Error err ->
|
||
|
|
let msg = extract_communication_error db err in
|
||
|
|
Error (Caqti_error.request_failed ~uri ~query msg)
|
||
|
|
|
||
|
|
let reset () =
|
||
|
|
Log.warn (fun p ->
|
||
|
|
p "Lost connection to <%a>, reconnecting." Caqti_error.pp_uri uri)
|
||
|
|
>>= fun () ->
|
||
|
|
in_transaction := false;
|
||
|
|
(match db#reset_start with
|
||
|
|
| exception Pg.Error _ -> Fiber.return false
|
||
|
|
| true ->
|
||
|
|
Pcache.clear_and_discard pcache;
|
||
|
|
Pg_io.communicate ~stdenv db (fun () -> db#reset_poll) >|=
|
||
|
|
(function
|
||
|
|
| Error _ -> false
|
||
|
|
| Ok () -> (try db#status = Pg.Ok with Pg.Error _ -> false))
|
||
|
|
| false ->
|
||
|
|
Fiber.return false)
|
||
|
|
|
||
|
|
let rec retry_on_connection_error ?(n = 1) f =
|
||
|
|
if !in_transaction then f () else
|
||
|
|
(f () : (_, [> Caqti_error.call]) result Fiber.t) >>=
|
||
|
|
(function
|
||
|
|
| Ok _ as r -> Fiber.return r
|
||
|
|
| Error (`Request_failed
|
||
|
|
{Caqti_error.msg = Connection_error_msg
|
||
|
|
{error = Postgresql.Connection_failure _; _}; _})
|
||
|
|
as r when n > 0 ->
|
||
|
|
let* reset_ok = reset () in
|
||
|
|
if reset_ok then
|
||
|
|
retry_on_connection_error ~n:(n - 1) f
|
||
|
|
else
|
||
|
|
Fiber.return r
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
|
||
|
|
let send_simple_query query =
|
||
|
|
retry_on_connection_error begin fun () ->
|
||
|
|
Fiber.return @@ wrap_pg ~query begin fun () ->
|
||
|
|
db#send_query query;
|
||
|
|
db#consume_input
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
let send_direct_query ~single_row_mode request_info params =
|
||
|
|
let {query; param_types; binary_params; _} = request_info in
|
||
|
|
retry_on_connection_error begin fun () ->
|
||
|
|
Fiber.return @@ wrap_pg ~query begin fun () ->
|
||
|
|
db#send_query ~params ~param_types ~binary_params query;
|
||
|
|
if single_row_mode then db#set_single_row_mode;
|
||
|
|
db#consume_input
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
let send_prepared_query ~single_row_mode request_info params =
|
||
|
|
let {query_name; query; binary_params; _} = request_info in
|
||
|
|
assert (query_name <> "");
|
||
|
|
retry_on_connection_error begin fun () ->
|
||
|
|
Fiber.return @@ wrap_pg ~query begin fun () ->
|
||
|
|
db#send_query_prepared ~params ~binary_params query_name;
|
||
|
|
if single_row_mode then db#set_single_row_mode;
|
||
|
|
db#consume_input
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
let fetch_one_result ~query () =
|
||
|
|
Pg_io.get_one_result ~stdenv ~uri ~query db
|
||
|
|
|
||
|
|
let fetch_final_result ~query () =
|
||
|
|
Pg_io.get_final_result ~stdenv ~uri ~query db
|
||
|
|
|
||
|
|
let fetch_single_row ~query () =
|
||
|
|
Pg_io.get_one_result ~stdenv ~uri ~query db >>=? fun result ->
|
||
|
|
(match result#status with
|
||
|
|
| Pg.Single_tuple ->
|
||
|
|
assert (result#ntuples = 1);
|
||
|
|
Fiber.return (Ok (Some result))
|
||
|
|
| Pg.Tuples_ok ->
|
||
|
|
assert (result#ntuples = 0);
|
||
|
|
Pg_io.get_next_result ~stdenv ~uri ~query db >|>=?
|
||
|
|
(function
|
||
|
|
| None -> Ok None
|
||
|
|
| Some _ ->
|
||
|
|
let msg =
|
||
|
|
Caqti_error.Msg "Extra result after final single-row result." in
|
||
|
|
Error (Caqti_error.response_rejected ~uri ~query msg))
|
||
|
|
| _ ->
|
||
|
|
Fiber.return @@ Result.map (fun () -> None) @@
|
||
|
|
Pg_io.check_query_result
|
||
|
|
~uri ~query ~row_mult:Row_mult.zero_or_more ~single_row_mode:true
|
||
|
|
result)
|
||
|
|
|
||
|
|
let prepare {query_name; query; param_types; _} =
|
||
|
|
assert (query_name <> "");
|
||
|
|
retry_on_connection_error begin fun () ->
|
||
|
|
let*? () =
|
||
|
|
Fiber.return @@ wrap_pg ~query @@ fun () ->
|
||
|
|
db#send_prepare ~param_types query_name query;
|
||
|
|
db#consume_input
|
||
|
|
in
|
||
|
|
let+*? result = Pg_io.get_final_result ~stdenv ~uri ~query db in
|
||
|
|
Pg_io.check_command_result ~uri ~query result
|
||
|
|
end
|
||
|
|
|
||
|
|
let free_prepared request_info =
|
||
|
|
let query = sprintf "DEALLOCATE %s" request_info.query_name in
|
||
|
|
let*? () = send_simple_query query in
|
||
|
|
let+*? result = fetch_final_result ~query () in
|
||
|
|
Pg_io.check_query_result
|
||
|
|
~uri ~query ~row_mult:Row_mult.zero ~single_row_mode:false
|
||
|
|
result
|
||
|
|
|
||
|
|
module Response = struct
|
||
|
|
|
||
|
|
type source =
|
||
|
|
| Complete of Pg.result
|
||
|
|
| Single_row
|
||
|
|
|
||
|
|
type ('b, 'm) t = {
|
||
|
|
row_type: 'b Row_type.t;
|
||
|
|
source: source;
|
||
|
|
query: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
let returned_count = function
|
||
|
|
| {source = Complete result; _} ->
|
||
|
|
Fiber.return (Ok result#ntuples)
|
||
|
|
| {source = Single_row; _} ->
|
||
|
|
Fiber.return (Error `Unsupported)
|
||
|
|
|
||
|
|
let affected_count = function
|
||
|
|
| {source = Complete result; _} ->
|
||
|
|
Fiber.return (Ok (int_of_string result#cmd_tuples))
|
||
|
|
| {source = Single_row; _} ->
|
||
|
|
Fiber.return (Error `Unsupported)
|
||
|
|
|
||
|
|
let exec _ = Fiber.return (Ok ())
|
||
|
|
|
||
|
|
let find = function
|
||
|
|
| {row_type; source = Complete result; _} ->
|
||
|
|
Fiber.return (decode_row ~uri row_type (result, 0))
|
||
|
|
| {source = Single_row; _} ->
|
||
|
|
assert false
|
||
|
|
|
||
|
|
let find_opt = function
|
||
|
|
| {row_type; source = Complete result; _} ->
|
||
|
|
Fiber.return begin
|
||
|
|
if result#ntuples = 0 then Ok None else
|
||
|
|
(match decode_row ~uri row_type (result, 0) with
|
||
|
|
| Ok y -> Ok (Some y)
|
||
|
|
| Error _ as r -> r)
|
||
|
|
end
|
||
|
|
| {source = Single_row; _} ->
|
||
|
|
assert false
|
||
|
|
|
||
|
|
let fold f {row_type; query; source} =
|
||
|
|
let decode = decode_row ~uri row_type in
|
||
|
|
(match source with
|
||
|
|
| Complete result ->
|
||
|
|
let n = result#ntuples in
|
||
|
|
let rec loop i acc =
|
||
|
|
if i = n then Ok acc else
|
||
|
|
(match decode (result, i) with
|
||
|
|
| Ok y -> loop (i + 1) (f y acc)
|
||
|
|
| Error _ as r -> r)
|
||
|
|
in
|
||
|
|
fun acc -> Fiber.return (loop 0 acc)
|
||
|
|
| Single_row ->
|
||
|
|
let rec loop acc =
|
||
|
|
fetch_single_row ~query () >>=? function
|
||
|
|
| None -> Fiber.return (Ok acc)
|
||
|
|
| Some result ->
|
||
|
|
(match decode (result, 0) with
|
||
|
|
| Ok y -> loop (f y acc)
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
in
|
||
|
|
loop)
|
||
|
|
|
||
|
|
let fold_s f {row_type; query; source} =
|
||
|
|
let decode = decode_row ~uri row_type in
|
||
|
|
(match source with
|
||
|
|
| Complete result ->
|
||
|
|
let n = result#ntuples in
|
||
|
|
let rec loop i acc =
|
||
|
|
if i = n then Fiber.return (Ok acc) else
|
||
|
|
(match decode (result, i) with
|
||
|
|
| Ok y -> f y acc >>=? loop (i + 1)
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
in
|
||
|
|
loop 0
|
||
|
|
| Single_row ->
|
||
|
|
let rec loop acc =
|
||
|
|
fetch_single_row ~query () >>=? function
|
||
|
|
| None -> Fiber.return (Ok acc)
|
||
|
|
| Some result ->
|
||
|
|
(match decode (result, 0) with
|
||
|
|
| Ok y -> f y acc >>=? loop
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
in
|
||
|
|
loop)
|
||
|
|
|
||
|
|
let iter_s f {row_type; query; source} =
|
||
|
|
let decode = decode_row ~uri row_type in
|
||
|
|
(match source with
|
||
|
|
| Complete result ->
|
||
|
|
let n = result#ntuples in
|
||
|
|
let rec loop i =
|
||
|
|
if i = n then Fiber.return (Ok ()) else
|
||
|
|
(match decode (result, i) with
|
||
|
|
| Ok y -> f y >>=? fun () -> loop (i + 1)
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
in
|
||
|
|
loop 0
|
||
|
|
| Single_row ->
|
||
|
|
let rec loop () =
|
||
|
|
fetch_single_row ~query () >>=? function
|
||
|
|
| None -> Fiber.return (Ok ())
|
||
|
|
| Some result ->
|
||
|
|
(match decode (result, 0) with
|
||
|
|
| Ok y -> f y >>=? fun () -> loop ()
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
in
|
||
|
|
loop ())
|
||
|
|
|
||
|
|
let to_stream {row_type; query; source} =
|
||
|
|
let decode = decode_row ~uri row_type in
|
||
|
|
(match source with
|
||
|
|
| Complete result ->
|
||
|
|
let n = result#ntuples in
|
||
|
|
let rec seq i () =
|
||
|
|
if i = n then Fiber.return Stream.Nil else
|
||
|
|
(match decode (result, i) with
|
||
|
|
| Ok y -> Fiber.return (Stream.Cons (y, seq (i + 1)))
|
||
|
|
| Error err -> Fiber.return (Stream.Error err))
|
||
|
|
in
|
||
|
|
seq 0
|
||
|
|
| Single_row ->
|
||
|
|
let rec seq () =
|
||
|
|
fetch_single_row ~query () >|= function
|
||
|
|
| Ok None -> Stream.Nil
|
||
|
|
| Ok (Some result) ->
|
||
|
|
(match decode (result, 0) with
|
||
|
|
| Ok y -> Stream.Cons (y, seq)
|
||
|
|
| Error err -> Stream.Error err)
|
||
|
|
| Error err -> Stream.Error err
|
||
|
|
in
|
||
|
|
seq)
|
||
|
|
end
|
||
|
|
|
||
|
|
let type_oid_cache = Hashtbl.create 19
|
||
|
|
|
||
|
|
let pp_request_with_param ppf =
|
||
|
|
Request.make_pp_with_param ~subst ~dialect () ppf
|
||
|
|
|
||
|
|
let fresh_static_name = Request_utils.fresh_name_generator "caqs"
|
||
|
|
let fresh_dynamic_name = Request_utils.fresh_name_generator "caqd"
|
||
|
|
|
||
|
|
let build_request_info request =
|
||
|
|
let templ = Request.query request dialect in
|
||
|
|
let query_name =
|
||
|
|
(match Request.prepare_policy request with
|
||
|
|
| Direct -> ""
|
||
|
|
| Static -> fresh_static_name ()
|
||
|
|
| Dynamic -> fresh_dynamic_name ())
|
||
|
|
in
|
||
|
|
let query = Pg_ext.query_string ~subst db templ in
|
||
|
|
let param_type = Request.param_type request in
|
||
|
|
let param_length = Row_type.length param_type in
|
||
|
|
let param_types = Array.make param_length 0 in
|
||
|
|
let binary_params = Array.make param_length false in
|
||
|
|
init_param_types ~type_oid_cache param_types binary_params param_type
|
||
|
|
|> Result.map @@ fun () ->
|
||
|
|
{query_name; query; param_length; param_types; binary_params}
|
||
|
|
|
||
|
|
let build_params request request_info param =
|
||
|
|
let param_type = Request.param_type request in
|
||
|
|
let params = Array.make request_info.param_length Pg.null in
|
||
|
|
Param_encoder.encode ~uri params param_type param
|
||
|
|
|> Result.map (fun () -> params)
|
||
|
|
|
||
|
|
let send_request ~single_row_mode request param =
|
||
|
|
(match Request.prepare_policy request with
|
||
|
|
| Direct ->
|
||
|
|
let/? request_info = build_request_info request in
|
||
|
|
let/? params = build_params request request_info param in
|
||
|
|
let+? () = send_direct_query ~single_row_mode request_info params in
|
||
|
|
request_info.query
|
||
|
|
| Dynamic | Static ->
|
||
|
|
let*? request_info =
|
||
|
|
(match Pcache.find_and_promote pcache request with
|
||
|
|
| Some request_info ->
|
||
|
|
Fiber.return (Ok request_info)
|
||
|
|
| None ->
|
||
|
|
let/? request_info = build_request_info request in
|
||
|
|
let+? () = prepare request_info in
|
||
|
|
Pcache.add pcache request request_info;
|
||
|
|
request_info)
|
||
|
|
in
|
||
|
|
let/? params = build_params request request_info param in
|
||
|
|
let+? () = send_prepared_query ~single_row_mode request_info params in
|
||
|
|
request_info.query)
|
||
|
|
|
||
|
|
let call_without_oids ~f request param =
|
||
|
|
Log.debug ~src:Logging.request_log_src (fun f ->
|
||
|
|
f "Sending %a" pp_request_with_param (request, param)) >>= fun () ->
|
||
|
|
|
||
|
|
let single_row_mode =
|
||
|
|
use_single_row_mode
|
||
|
|
&& Row_mult.can_be_many (Request.row_mult request)
|
||
|
|
in
|
||
|
|
|
||
|
|
(* Prepare, if requested, and send the query. *)
|
||
|
|
let*? query = send_request ~single_row_mode request param in
|
||
|
|
|
||
|
|
(* Fetch and process the result. *)
|
||
|
|
let row_type = Request.row_type request in
|
||
|
|
if single_row_mode then
|
||
|
|
f Response.{row_type; query; source = Single_row}
|
||
|
|
else begin
|
||
|
|
let row_mult = Request.row_mult request in
|
||
|
|
let*? result = fetch_final_result ~query () in
|
||
|
|
(match Pg_io.check_query_result
|
||
|
|
~uri ~query ~row_mult ~single_row_mode result with
|
||
|
|
| Ok () -> f Response.{row_type; query; source = Complete result}
|
||
|
|
| Error _ as r -> Fiber.return r)
|
||
|
|
end
|
||
|
|
|
||
|
|
let rec fetch_type_oids : type a. a Row_type.t -> _ = function
|
||
|
|
| Field (Enum name as field_type)
|
||
|
|
when not (Hashtbl.mem type_oid_cache name) ->
|
||
|
|
call_without_oids ~f:Response.find_opt Q.type_oid name >>=
|
||
|
|
(function
|
||
|
|
| Ok (Some oid) ->
|
||
|
|
Fiber.return (Ok (Hashtbl.add type_oid_cache name oid))
|
||
|
|
| Ok None ->
|
||
|
|
Log.warn (fun p ->
|
||
|
|
p "Failed to query OID for enum %s." name) >|= fun () ->
|
||
|
|
Error (Caqti_error.encode_missing ~uri ~field_type ())
|
||
|
|
| Error (`Encode_rejected _ | `Decode_rejected _ |
|
||
|
|
`Response_failed _ as err) ->
|
||
|
|
Log.err (fun p ->
|
||
|
|
p "Failed to fetch obtain OID for enum %s due to: %a"
|
||
|
|
name Caqti_error.pp err) >|= fun () ->
|
||
|
|
Error (Caqti_error.encode_missing ~uri ~field_type ())
|
||
|
|
| Error #Caqti_error.call as r ->
|
||
|
|
Fiber.return r)
|
||
|
|
| Field _ -> Fiber.return (Ok ())
|
||
|
|
| Option t -> fetch_type_oids t
|
||
|
|
| Product (_, prod) ->
|
||
|
|
let rec loop : type i. (i, a) Row_type.product -> _ = function
|
||
|
|
| Proj_end -> Fiber.return (Ok ())
|
||
|
|
| Proj (t, _, prod) -> fetch_type_oids t >>=? fun () -> loop prod
|
||
|
|
in
|
||
|
|
loop prod
|
||
|
|
| Annot (_, t0) -> fetch_type_oids t0
|
||
|
|
|
||
|
|
let using_db f =
|
||
|
|
if !in_use then
|
||
|
|
failwith "Invalid concurrent usage of PostgreSQL connection detected.";
|
||
|
|
in_use := true;
|
||
|
|
Fiber.cleanup
|
||
|
|
(fun () -> f () >|= fun res -> in_use := false; res)
|
||
|
|
(fun () -> reset () >|= fun _ -> in_use := false)
|
||
|
|
|
||
|
|
let deallocate request = using_db @@ fun () ->
|
||
|
|
(match Request.prepare_policy request with
|
||
|
|
| Direct -> failwith "deallocate called on direct request"
|
||
|
|
| Dynamic | Static ->
|
||
|
|
(match Pcache.deallocate pcache request with
|
||
|
|
| None -> Fiber.return (Ok ())
|
||
|
|
| Some (request_info, commit_remove) ->
|
||
|
|
free_prepared request_info >|= Result.map commit_remove))
|
||
|
|
|
||
|
|
let deallocate_some () =
|
||
|
|
let rec loop = function
|
||
|
|
| [] -> Fiber.return (Ok ())
|
||
|
|
| request_info :: orphans ->
|
||
|
|
let*? () = free_prepared request_info in
|
||
|
|
loop orphans
|
||
|
|
in
|
||
|
|
let orphans, commit = Pcache.trim pcache in
|
||
|
|
loop orphans >|=? commit
|
||
|
|
|
||
|
|
let call ~f req param = using_db @@ fun () ->
|
||
|
|
deallocate_some () >>=? fun () ->
|
||
|
|
fetch_type_oids (Request.param_type req) >>=? fun () ->
|
||
|
|
call_without_oids ~f req param
|
||
|
|
|
||
|
|
let disconnect () = using_db @@ fun () ->
|
||
|
|
try db#finish; Fiber.return () with Pg.Error err ->
|
||
|
|
Log.warn (fun p ->
|
||
|
|
p "While disconnecting from <%a>: %s"
|
||
|
|
Caqti_error.pp_uri uri (Pg.string_of_error err))
|
||
|
|
|
||
|
|
let validate () = using_db @@ fun () ->
|
||
|
|
if (try db#consume_input; db#status = Pg.Ok with Pg.Error _ -> false) then
|
||
|
|
Fiber.return true
|
||
|
|
else
|
||
|
|
reset ()
|
||
|
|
|
||
|
|
let check f = f (try db#status = Pg.Ok with Pg.Error _ -> false)
|
||
|
|
|
||
|
|
let exec q p = call ~f:Response.exec q p
|
||
|
|
let start () = exec Q.start () >|=? fun () -> in_transaction := true
|
||
|
|
let commit () = in_transaction := false; exec Q.commit ()
|
||
|
|
let rollback () = in_transaction := false; exec Q.rollback ()
|
||
|
|
|
||
|
|
let set_statement_timeout t =
|
||
|
|
let t_arg =
|
||
|
|
(match t with
|
||
|
|
| None -> 0
|
||
|
|
| Some t -> max 1 (int_of_float (t *. 1000.0 +. 500.0)))
|
||
|
|
in
|
||
|
|
call ~f:Response.exec (Q.set_statement_timeout t_arg) ()
|
||
|
|
|
||
|
|
let populate ~table ~columns row_type data =
|
||
|
|
let query =
|
||
|
|
sprintf "COPY %s (%s) FROM STDIN" table (String.concat "," columns)
|
||
|
|
in
|
||
|
|
let param_length = Row_type.length row_type in
|
||
|
|
let fail msg =
|
||
|
|
Fiber.return
|
||
|
|
(Error (Caqti_error.request_failed ~uri ~query (Caqti_error.Msg msg)))
|
||
|
|
in
|
||
|
|
let pg_error err =
|
||
|
|
let msg = extract_communication_error db err in
|
||
|
|
Fiber.return (Error (Caqti_error.request_failed ~uri ~query msg))
|
||
|
|
in
|
||
|
|
let put_copy_data data =
|
||
|
|
let rec loop fd =
|
||
|
|
match db#put_copy_data data with
|
||
|
|
| Pg.Put_copy_error ->
|
||
|
|
fail "Unable to put copy data"
|
||
|
|
| Pg.Put_copy_queued ->
|
||
|
|
Fiber.return (Ok ())
|
||
|
|
| Pg.Put_copy_not_queued ->
|
||
|
|
Unix.poll ~stdenv ~write:true fd >>= fun _ -> loop fd
|
||
|
|
in
|
||
|
|
(match db#socket with
|
||
|
|
| exception Pg.Error msg -> pg_error msg
|
||
|
|
| socket -> Unix.wrap_fd loop (Obj.magic socket))
|
||
|
|
in
|
||
|
|
let copy_row row =
|
||
|
|
let params = Array.make param_length "\\N" in
|
||
|
|
(match Copy_encoder.encode ~uri params row_type row with
|
||
|
|
| Ok () ->
|
||
|
|
Fiber.return (Ok (String.concat "\t" (Array.to_list params)))
|
||
|
|
| Error _ as r ->
|
||
|
|
Fiber.return r)
|
||
|
|
>>=? fun param_string -> put_copy_data (param_string ^ "\n")
|
||
|
|
in
|
||
|
|
begin
|
||
|
|
(* Send the copy command to start the transfer.
|
||
|
|
* Skip checking that there is only a single result: while in copy mode
|
||
|
|
* we can repeatedly get the latest result and it will always be
|
||
|
|
* Copy_in, so checking for a single result would trigger an error.
|
||
|
|
*)
|
||
|
|
send_simple_query query >>=? fun () ->
|
||
|
|
fetch_one_result ~query ()
|
||
|
|
>>=? fun result ->
|
||
|
|
(* We expect the Copy_in response only - turn other success responses
|
||
|
|
* into errors, and delegate error handling.
|
||
|
|
*)
|
||
|
|
(match result#status with
|
||
|
|
| Pg.Copy_in -> Fiber.return (Ok ())
|
||
|
|
| Pg.Command_ok -> fail "Received Command_ok when expecting Copy_in"
|
||
|
|
| _ -> Fiber.return (Pg_io.check_command_result ~uri ~query result))
|
||
|
|
>>=? fun () -> System.Stream.iter_s ~f:copy_row data
|
||
|
|
>>=? fun () ->
|
||
|
|
(* End the copy *)
|
||
|
|
let rec copy_end_loop fd =
|
||
|
|
match db#put_copy_end () with
|
||
|
|
| Pg.Put_copy_error ->
|
||
|
|
fail "Unable to finalize copy"
|
||
|
|
| Pg.Put_copy_not_queued ->
|
||
|
|
Unix.poll ~stdenv ~write:true fd >>= fun _ ->
|
||
|
|
copy_end_loop fd
|
||
|
|
| Pg.Put_copy_queued ->
|
||
|
|
Fiber.return (Ok ())
|
||
|
|
in
|
||
|
|
(match db#socket with
|
||
|
|
| exception Pg.Error msg -> pg_error msg
|
||
|
|
| socket -> Unix.wrap_fd copy_end_loop (Obj.magic socket))
|
||
|
|
>>=? fun () ->
|
||
|
|
(* After ending the copy, there will be a new result for the initial
|
||
|
|
* query.
|
||
|
|
*)
|
||
|
|
fetch_final_result ~query () >|>=? Pg_io.check_command_result ~uri ~query
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
let connect ~sw:_ ~stdenv ~subst ~config uri =
|
||
|
|
Fiber.return (Pg_ext.parse_uri uri)
|
||
|
|
>>=? fun (conninfo, notice_processing, use_single_row_mode) ->
|
||
|
|
(match new Pg.connection ~conninfo () with
|
||
|
|
| exception Pg.Error err ->
|
||
|
|
let msg = extract_connect_error err in
|
||
|
|
Fiber.return (Error (Caqti_error.connect_failed ~uri msg))
|
||
|
|
| db ->
|
||
|
|
Pg_io.communicate ~stdenv db (fun () -> db#connect_poll) >>=
|
||
|
|
(function
|
||
|
|
| Error err ->
|
||
|
|
let msg = extract_communication_error db err in
|
||
|
|
Fiber.return (Error (Caqti_error.connect_failed ~uri msg))
|
||
|
|
| Ok () ->
|
||
|
|
(match db#status <> Pg.Ok with
|
||
|
|
| exception Pg.Error err ->
|
||
|
|
let msg = extract_communication_error db err in
|
||
|
|
Fiber.return (Error (Caqti_error.connect_failed ~uri msg))
|
||
|
|
| true ->
|
||
|
|
let msg = Caqti_error.Msg db#error_message in
|
||
|
|
Fiber.return (Error (Caqti_error.connect_failed ~uri msg))
|
||
|
|
| false ->
|
||
|
|
db#set_notice_processing notice_processing;
|
||
|
|
let server_version =
|
||
|
|
let v0, v1, v2 = db#server_version in
|
||
|
|
Version.of_string_unsafe
|
||
|
|
(if v0 < 10 then
|
||
|
|
Printf.sprintf "%d.%d.%d" v0 v1 v2
|
||
|
|
else
|
||
|
|
Printf.sprintf "%d.%d" v0 (v1 * 100 + v2))
|
||
|
|
in
|
||
|
|
let module B = Make_connection_base
|
||
|
|
(struct
|
||
|
|
let dialect =
|
||
|
|
Dialect.create_pgsql
|
||
|
|
~server_version ~client_library:`postgresql ()
|
||
|
|
let subst = subst dialect
|
||
|
|
let stdenv = stdenv
|
||
|
|
let uri = uri
|
||
|
|
let db = db
|
||
|
|
let use_single_row_mode = use_single_row_mode
|
||
|
|
let dynamic_capacity =
|
||
|
|
Caqti_connect_config.(get dynamic_prepare_capacity) config
|
||
|
|
end)
|
||
|
|
in
|
||
|
|
let module Connection = struct
|
||
|
|
let driver_info = driver_info
|
||
|
|
let driver_connection = None
|
||
|
|
include B
|
||
|
|
include Connection_utils.Make_convenience (System) (B)
|
||
|
|
end in
|
||
|
|
Connection.exec Q.set_timezone_to_utc () >|=
|
||
|
|
(function
|
||
|
|
| Ok () -> Ok (module Connection : CONNECTION)
|
||
|
|
| Error err -> Error (`Post_connect err)))))
|
||
|
|
end
|
||
|
|
|
||
|
|
let () =
|
||
|
|
let open Caqti_platform_unix.Driver_loader in
|
||
|
|
register "postgres" (module Connect_functor);
|
||
|
|
register "postgresql" (module Connect_functor)
|