(* Copyright (C) 2017--2025 Petter A. Urkedal * * 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 * and , 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)