(* A basic example of Pgx_async usage *)
open Core_kernel
open Async_kernel
open Async_unix

module Employee = struct
  let create db =
    Pgx_async.simple_query
      db
      {|
      CREATE TEMPORARY TABLE Employee (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL UNIQUE);
    |}
    |> Deferred.ignore_m
  ;;

  (* This function lets us insert multiple users relatively efficiently *)
  let insert_many db names =
    let params = List.map names ~f:(fun name -> Pgx_async.Value.[ of_string name ]) in
    Pgx_async.execute_many
      db
      ~params
      ~query:
        {|
      INSERT INTO Employee (name)
      VALUES ($1)
      RETURNING id
    |}
    >>| List.map ~f:(function
            | [ [ id ] ] -> Pgx.Value.to_int_exn id
            | _ -> assert false)
  ;;

  let insert ~name db = insert_many db [ name ] >>| List.hd_exn
end

module Facility = struct
  let create db =
    Pgx_async.simple_query
      db
      {|
      CREATE TEMPORARY TABLE Facility (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL UNIQUE,
        director_id INT REFERENCES Employee(id) ON DELETE SET NULL);

      CREATE INDEX facility_director_id ON Facility (director_id);
    |}
    |> Deferred.ignore_m
  ;;

  let insert ~name ?director_id db =
    let params = Pgx_async.Value.[ of_string name; opt of_int director_id ] in
    Pgx_async.execute
      db
      ~params
      {|
      INSERT INTO Facility (name, director_id)
      VALUES ($1, $2)
      RETURNING id
    |}
    >>| function
    | [ [ id ] ] -> Pgx.Value.to_int_exn id
    | _ -> assert false
  ;;

  let all_name_and_director_name db =
    Pgx_async.execute
      db
      {|
      SELECT f.name, e.name
      FROM Facility f
      LEFT JOIN Employee e ON e.id = f.director_id
    |}
    >>| List.map ~f:(function
            | [ name; director_name ] ->
              Pgx.Value.(to_string_exn name, to_string director_name)
            | _ -> assert false)
  ;;

  let reassign_director db ~director_id ~from_facility_id ~to_facility_id =
    (* Note: with_transaction doesn't currently have any special handling
       for concurrent queries *)
    Pgx_async.with_transaction db
    @@ fun db ->
    let params = Pgx.Value.[ of_int director_id; of_int from_facility_id ] in
    Pgx_async.execute
      db
      ~params
      {|
      UPDATE Facility SET director_id = NULL WHERE id = $2 AND director_id = $1
    |}
    >>= fun _ ->
    let params = Pgx.Value.[ of_int director_id; of_int to_facility_id ] in
    Pgx_async.execute
      db
      ~params
      {|
      UPDATE Facility SET director_id = $1 WHERE id = $2
    |}
    |> Deferred.ignore_m
  ;;
end

let setup db = Employee.create db >>= fun () -> Facility.create db

let main () =
  Pgx_async.with_conn
  @@ fun db ->
  setup db
  >>= fun () ->
  Employee.insert ~name:"Steve" db
  >>= fun steve_id ->
  (* Parallel queries are not an error, but will execute in serial *)
  [ Facility.insert ~name:"Headquarters" ~director_id:steve_id db
  ; Facility.insert ~name:"New Office" db
  ]
  |> Deferred.all
  >>= function
  | [ headquarters_id; new_office_id ] ->
    Facility.all_name_and_director_name db
    >>| List.iter ~f:(fun (name, director_name) ->
            let director_name = Option.value director_name ~default:"(none)" in
            printf "The director of %s is %s\n" name director_name)
    >>= fun () ->
    print_endline "Re-assigning Steve to the New Office";
    Facility.reassign_director
      db
      ~director_id:steve_id
      ~from_facility_id:headquarters_id
      ~to_facility_id:new_office_id
    >>= fun () ->
    Facility.all_name_and_director_name db
    >>| List.iter ~f:(fun (name, director_name) ->
            let director_name = Option.value director_name ~default:"(none)" in
            printf "The director of %s is %s\n" name director_name)
  | _ -> assert false
;;

let () = Thread_safe.block_on_async_exn main
