297 lines
11 KiB
OCaml
297 lines
11 KiB
OCaml
|
|
(* Copyright (C) 2017--2023 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.
|
||
|
|
*)
|
||
|
|
|
||
|
|
(** Error descriptors. *)
|
||
|
|
|
||
|
|
|
||
|
|
(** {2 Error Causes}
|
||
|
|
|
||
|
|
The {!type:cause} type is an incomplete enumeration of consolidated causes
|
||
|
|
of errors between different database systems. The selection includes the
|
||
|
|
causes which are believed to be useful to handle and excludes causes which
|
||
|
|
are specific to the implementation of a certain database system.
|
||
|
|
|
||
|
|
The causes are classified into subtypes to help matching them collectively.
|
||
|
|
Each subtype has a fall-back case which is used if the database system does
|
||
|
|
not clearly report one of the specific cases. An condition which is
|
||
|
|
reported as the fall-back case may in a future version be reported as a
|
||
|
|
specific case, possibly adding a new case to the subtype. Therefore, for
|
||
|
|
backwards compatibility you should match the full subtype rather than the
|
||
|
|
fall-back, e.g.
|
||
|
|
{[
|
||
|
|
(match Caqti_error.cause error with
|
||
|
|
| `Unique_violation -> handle_unique_violation ()
|
||
|
|
| #integrity_constraint_violation -> handle_other_constraint_violation ()
|
||
|
|
| _ -> handle_other_error ())
|
||
|
|
]}
|
||
|
|
This ensures that your code stil compiles when a new case is added to
|
||
|
|
{!integrity_constraint_violation}, and that the error condition receiving
|
||
|
|
mapped to the new case is still handled by the subtype pattern when you link
|
||
|
|
to the new version of Caqti.
|
||
|
|
|
||
|
|
Currently we do not have access to the extended error codes from SQLite3,
|
||
|
|
meaning that all integrity constraint violation conditions will be reported
|
||
|
|
as [`Integrity_constraint_violation__don't_match].
|
||
|
|
|
||
|
|
Since the consolitation of each error condition requires some investitation
|
||
|
|
and testing, the selection made is very conservative. If you need to handle
|
||
|
|
an error which is currently unlisted, please open an issue or create a pull
|
||
|
|
request. A pull request should, if possible, include a extension of
|
||
|
|
[test_error_cause.ml] to demonstrate how the error is triggered by the
|
||
|
|
database systems. *)
|
||
|
|
|
||
|
|
type integrity_constraint_violation = [
|
||
|
|
| `Restrict_violation
|
||
|
|
(** This is meant to indicate that a deletion or update would cause a
|
||
|
|
foreign key violation, although this may be reported as a
|
||
|
|
[`Foreign_key_violation]. *)
|
||
|
|
| `Not_null_violation
|
||
|
|
(** An insertion or update attempts to assign a [NULL] value to column
|
||
|
|
having a [NOT NULL] constraint. *)
|
||
|
|
| `Foreign_key_violation
|
||
|
|
(** An modification would cause a column to reference a non-existing key.
|
||
|
|
This cause may also be reported for cases which should have been
|
||
|
|
covered by [`Restrict_violation]. *)
|
||
|
|
| `Unique_violation
|
||
|
|
(** An insertion or update would duplicate a key as declared by a [UNIQUE]
|
||
|
|
or [PRIMARY KEY] constraint. *)
|
||
|
|
| `Check_violation
|
||
|
|
(** A requested change would violate a [CHECK] constraint. *)
|
||
|
|
| `Exclusion_violation
|
||
|
|
(** A requested insertion or update would cause an overlap of rows
|
||
|
|
according to an [EXCLUDE] constraint. *)
|
||
|
|
| `Integrity_constraint_violation__don't_match
|
||
|
|
(** An yet unclassified cause; match the full subtype instead. *)
|
||
|
|
]
|
||
|
|
(** A subtype of {!type:cause} informing about violation of SQL constraints. *)
|
||
|
|
|
||
|
|
type insufficient_resources = [
|
||
|
|
| `Disk_full
|
||
|
|
(** The server is out of disk space. *)
|
||
|
|
| `Out_of_memory
|
||
|
|
(** The server is out of memory *)
|
||
|
|
| `Too_many_connections
|
||
|
|
(** The server does not accept establishing more connections. *)
|
||
|
|
| `Configuration_limit_exceeded
|
||
|
|
(** Some unspecific server limit is exceeded. *)
|
||
|
|
| `Insufficient_resources__don't_match
|
||
|
|
(** An yet unclassified cause; match the full subtype instead. *)
|
||
|
|
]
|
||
|
|
(** A subtype of {!type:cause} informing about insufficient resources on the
|
||
|
|
server side. *)
|
||
|
|
|
||
|
|
type cause = [
|
||
|
|
| integrity_constraint_violation
|
||
|
|
| insufficient_resources
|
||
|
|
| `Unspecified__don't_match
|
||
|
|
]
|
||
|
|
(** The selection of causes of errors which have been mapped. *)
|
||
|
|
|
||
|
|
val show_cause : [< cause] -> string
|
||
|
|
|
||
|
|
|
||
|
|
(** {2 Messages} *)
|
||
|
|
|
||
|
|
type msg = ..
|
||
|
|
(** In this type, drivers can stash information about any errors in their own
|
||
|
|
format, which can later be used for pretty-printing and or future
|
||
|
|
operations. Drivers must call {!define_msg} on each constructor added to
|
||
|
|
this type. *)
|
||
|
|
|
||
|
|
val define_msg :
|
||
|
|
pp: (Format.formatter -> msg -> unit) ->
|
||
|
|
?cause: (msg -> cause) ->
|
||
|
|
extension_constructor -> unit
|
||
|
|
(** Mandatory registration of pretty-printer for a driver-supplied error
|
||
|
|
descriptor. *)
|
||
|
|
|
||
|
|
val pp_msg : Format.formatter -> msg -> unit
|
||
|
|
(** [pp_msg ppf msg] formats [msg] on [ppf]. *)
|
||
|
|
|
||
|
|
type msg += Msg : string -> msg
|
||
|
|
(** The shape of locally generated messages and messages from drivers without
|
||
|
|
dedicated error type. *)
|
||
|
|
|
||
|
|
(**/**)
|
||
|
|
val pp_uri : Format.formatter -> Uri.t -> unit
|
||
|
|
(** Pretty printer of URIs which omits the password, used by drivers when
|
||
|
|
logging. *)
|
||
|
|
(**/**)
|
||
|
|
|
||
|
|
(** {2 Messages with Metadata}
|
||
|
|
|
||
|
|
{b Note.} Please consider the fields internal for now, they may still be
|
||
|
|
revised or hidden. *)
|
||
|
|
|
||
|
|
type load_error = private {
|
||
|
|
uri: Uri.t;
|
||
|
|
msg: msg;
|
||
|
|
}
|
||
|
|
type connection_error = private {
|
||
|
|
uri: Uri.t;
|
||
|
|
msg: msg;
|
||
|
|
}
|
||
|
|
type query_error = private {
|
||
|
|
uri: Uri.t;
|
||
|
|
query: string;
|
||
|
|
msg: msg;
|
||
|
|
}
|
||
|
|
type coding_error = private {
|
||
|
|
uri: Uri.t;
|
||
|
|
typ: Caqti_type.any;
|
||
|
|
msg: msg;
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
(** {2 Documented Constructors} *)
|
||
|
|
|
||
|
|
|
||
|
|
(** {3 Errors during Driver Loading} *)
|
||
|
|
|
||
|
|
val load_rejected : uri: Uri.t -> msg -> [> `Load_rejected of load_error]
|
||
|
|
(** [load_rejected ~uri msg] indicates that a driver could not be identified
|
||
|
|
from [uri]. *)
|
||
|
|
|
||
|
|
val load_failed : uri: Uri.t -> msg -> [> `Load_failed of load_error]
|
||
|
|
(** [load_failed ~uri msg] indicates that a driver for [uri] could not be
|
||
|
|
loaded. *)
|
||
|
|
|
||
|
|
|
||
|
|
(** {3 Errors during Connect} *)
|
||
|
|
|
||
|
|
val connect_rejected : uri: Uri.t -> msg ->
|
||
|
|
[> `Connect_rejected of connection_error]
|
||
|
|
(** [connect_rejected ~uri msg] indicates that the driver rejected the URI. *)
|
||
|
|
|
||
|
|
val connect_failed : uri: Uri.t -> msg ->
|
||
|
|
[> `Connect_failed of connection_error]
|
||
|
|
(** [connect_failed ~uri msg] indicates that the driver failed to establish a
|
||
|
|
connection to the database. *)
|
||
|
|
|
||
|
|
|
||
|
|
(** {3 Errors during Call} *)
|
||
|
|
|
||
|
|
val encode_missing : uri: Uri.t -> field_type: 'a Caqti_type.Field.t -> unit ->
|
||
|
|
[> `Encode_rejected of coding_error]
|
||
|
|
(** [encode_missing ~uri ~field_type ()] indicates that the driver does not
|
||
|
|
support [field_type] and no fallback encoding is available for the type. *)
|
||
|
|
|
||
|
|
val encode_rejected : uri: Uri.t -> typ: 'a Caqti_type.t -> msg ->
|
||
|
|
[> `Encode_rejected of coding_error]
|
||
|
|
(** [encode_rejected ~uri ~typ msg] indicates that encoding a value to [typ]
|
||
|
|
failed, e.g. due to being out of range. *)
|
||
|
|
|
||
|
|
val encode_failed : uri: Uri.t -> typ: 'a Caqti_type.t -> msg ->
|
||
|
|
[> `Encode_failed of coding_error]
|
||
|
|
(** [encode_failed ~uri ~typ msg] indicates that a parameter of type [typ] was
|
||
|
|
not accepted by the database client library. *)
|
||
|
|
|
||
|
|
val request_failed : uri: Uri.t -> query: string -> msg ->
|
||
|
|
[> `Request_failed of query_error]
|
||
|
|
(** [request_failed ~uri ~query msg] indicates that the request could not be
|
||
|
|
transmitted to the database, that the database was not ready to process the
|
||
|
|
request, or that something went wrong while processing the request. *)
|
||
|
|
|
||
|
|
|
||
|
|
(** {3 Errors during Result Retrieval} *)
|
||
|
|
|
||
|
|
val decode_missing : uri: Uri.t -> field_type: 'a Caqti_type.Field.t -> unit ->
|
||
|
|
[> `Decode_rejected of coding_error]
|
||
|
|
(** [decode_missing ~uri ~field_type ()] indicates that the driver does not
|
||
|
|
support [field_type] for decoding result rows. *)
|
||
|
|
|
||
|
|
val decode_rejected : uri: Uri.t -> typ: 'a Caqti_type.t -> msg ->
|
||
|
|
[> `Decode_rejected of coding_error]
|
||
|
|
(** [decode_rejected ~uri ~typ msg] indicates that the driver could not decode a
|
||
|
|
field of type [typ] from the returned row, e.g. due to an invalid value or
|
||
|
|
limited range of the target type. *)
|
||
|
|
|
||
|
|
val response_failed : uri: Uri.t -> query: string -> msg ->
|
||
|
|
[> `Response_failed of query_error]
|
||
|
|
(** [response_failed ~uri ~query msg] indicates that something when wrong while
|
||
|
|
fetching a delayed part of the response. *)
|
||
|
|
|
||
|
|
val response_rejected : uri: Uri.t -> query: string -> msg ->
|
||
|
|
[> `Response_rejected of query_error]
|
||
|
|
(** [response_rejected ~uri ~query msg] indicates that the response from the
|
||
|
|
database was rejected due to requirements posed by client code. *)
|
||
|
|
|
||
|
|
|
||
|
|
(** {2 Specific Error Types} *)
|
||
|
|
|
||
|
|
type call =
|
||
|
|
[ `Encode_rejected of coding_error
|
||
|
|
| `Encode_failed of coding_error
|
||
|
|
| `Request_failed of query_error
|
||
|
|
| `Response_rejected of query_error ]
|
||
|
|
|
||
|
|
type retrieve =
|
||
|
|
[ `Decode_rejected of coding_error
|
||
|
|
| `Request_failed of query_error
|
||
|
|
| `Response_failed of query_error
|
||
|
|
| `Response_rejected of query_error ]
|
||
|
|
(** Errors which may occur during retrival of result rows. This includes
|
||
|
|
[`Request_failed] since the request is fused with retrieval for the pgx
|
||
|
|
driver. *)
|
||
|
|
|
||
|
|
type call_or_retrieve = [call | retrieve]
|
||
|
|
|
||
|
|
type transact = [call | retrieve] (* TODO: Should be a subset. *)
|
||
|
|
|
||
|
|
type load =
|
||
|
|
[ `Load_rejected of load_error
|
||
|
|
| `Load_failed of load_error ]
|
||
|
|
|
||
|
|
type connect =
|
||
|
|
[ `Connect_rejected of connection_error
|
||
|
|
| `Connect_failed of connection_error
|
||
|
|
| `Post_connect of call_or_retrieve ]
|
||
|
|
|
||
|
|
type load_or_connect = [load | connect]
|
||
|
|
|
||
|
|
|
||
|
|
(** {2 Generic Error Type and Functions} *)
|
||
|
|
|
||
|
|
type t = [load | connect | call | retrieve]
|
||
|
|
(** The full union of errors used by Caqti. *)
|
||
|
|
|
||
|
|
val uri : [< t] -> Uri.t
|
||
|
|
(** [uri error] is the URI of the connection used where [error] occurred. *)
|
||
|
|
|
||
|
|
val pp : Format.formatter -> [< t] -> unit
|
||
|
|
(** [pp ppf error] prints an explanation of [error] on [ppf]. *)
|
||
|
|
|
||
|
|
val show : [< t] -> string
|
||
|
|
(** [show error] is an explanation of [error]. *)
|
||
|
|
|
||
|
|
val cause :
|
||
|
|
[< `Request_failed of query_error | `Response_failed of query_error] -> cause
|
||
|
|
(** A matchable representation of the cause of the error, if available. *)
|
||
|
|
|
||
|
|
type counit = |
|
||
|
|
(** An uninhabited type used by {!uncongested}. *)
|
||
|
|
|
||
|
|
val uncongested :
|
||
|
|
('a, [< t | `Congested of counit]) result ->
|
||
|
|
('a, [> t]) result
|
||
|
|
(** [uncongested r] eliminates an unused [`Congested] case from the error. *)
|
||
|
|
|
||
|
|
exception Exn of t
|
||
|
|
(** [Exn error] can be used when an exception is preferred over explicit error
|
||
|
|
handling. The core Caqti API never raises exceptions which originate from
|
||
|
|
runtime errors. *)
|