Module Rowtype_postgres

rowtype over Postgres, through postgres-eio: the one place the two meet.

Statements are Postgres's as written, $1 for a shape's first value. An Rowtype.instant is a timestamptz, which keeps the microsecond too, sent as ISO 8601 in UTC and read back whatever the session's time zone, which the driver reads in the styles it sets on every connection (DateStyle=ISO, IntervalStyle=postgres). Years 1 to 9999: within a day of either end, a session far from UTC prints a year that a connection reading text cannot read. A value its scalar cannot hold -- infinity, a year outside them, an int8 past an int's 63 bits -- is refused as what it is, saying what reads it instead. A COMMIT of a transaction a failed statement aborted is rolled back without an error, which Transaction.within tells from a commit. A result is read in binary wherever the driver reads its type, and in text on a connection that keeps no statements, which cannot be bound for binary. A unique, exclusion or foreign-key violation is `Conflict, with the constraint the server named, whether a statement or a deferred check at COMMIT found it.

Each statement is a debug line on rowtype-postgres with how long it took: its first line of text, never its parameters. A statement runs on the calling fiber, as an Eio effect, and a connection is one fiber's at a time. A raise from fold's function passes, and closes the connection, whose answer was read only in part: its next statement is `Connection `Closed until revive. A fiber cancelled with a statement in flight closes its connection and asks the server to cancel the statement, waiting a few seconds at most for the server to take the request, so nothing runs on for nobody.

module Conninfo = Postgres_eio.Conninfo

What a connection is made to: read from a connection string, either form, by Conninfo.of_string, and moved to another database by Conninfo.with_database. Its connect_timeout bounds each address tried, which Conninfo.with_connect_timeout sets.

type conn

An open connection.

type observer = {
  1. around : 'a. string -> (unit -> 'a) -> 'a;
}

What watches every statement a connection runs: around sql run is handed the statement's text and runs it, answering what run answered -- a span begun and ended around it, a count. The text has placeholders where the values are, and the values are never handed over, since a code, a digest or a token is one. Polymorphic, so one observer watches statements of every row type.

include Rowtype.S with type conn := conn
val run : conn -> ('p, 'r) Rowtype.statement -> 'p -> ('r, [> Rowtype.error ]) Stdlib.result

run db statement args: the statement with its parameters, answering what it says it answers. A row with more columns or fewer than its shape reads is a `Db error, as a backend's check of the statement refuses it.

val fold : conn -> ('p, 'r list) Rowtype.statement -> 'p -> init:'acc -> ('acc -> 'r -> 'acc) -> ('acc, [> Rowtype.error ]) Stdlib.result

fold db statement args ~init f: what run answers, folded as List.fold_left would fold it, without the list being made -- each row of a Rowtype.list statement is decoded and handed to f as it arrives, so a result of any size is read in the memory of the rows a backend reads at a time, never of the whole result. A row that does not decode is the answer, and f sees none after it.

val run_many : conn -> ('p, unit) Rowtype.statement -> 'p list -> (unit, [> Rowtype.error ]) Stdlib.result

run_many db statement values: an Rowtype.exec statement run once for each value, in one round trip and all or nothing -- if any run fails, none applies, and the answer is that failure. No values sends nothing. A statement that answers a row, a Rowtype.find of unit, is `Db before anything is sent, since a batch reads no rows.

val exec_raw : conn -> string -> (unit, [> Rowtype.error ]) Stdlib.result

For DDL and for a session's settings: no parameters, and rows are discarded. Several statements may be sent in one call. A transaction is never a begin and a commit sent here: a backend runs its own, and this library runs none, since a database may answer the COMMIT of a transaction a failed statement aborted by rolling it back without an error, which only the backend's own transaction tells from a commit.

val copy_in : conn -> ?schema:string -> table:string -> columns:string list -> 'a Rowtype.ty -> 'a Stdlib.Seq.t -> (int, [> Rowtype.error ]) Stdlib.result

copy_in db ~table ~columns row rows writes the rows with one COPY ... FROM STDIN, each bound as run binds a statement's parameters, and answers how many were written. The sequence is read as the rows are sent, so a load of any size is held a row at a time.

columns are in the row's order, every column in table order where empty, and quoted as given, so "Jobs" is not jobs; a number of them that is not the row's arity is refused before anything is sent. A row that cannot be bound, a constraint and a raise from the sequence each fail the whole COPY, with nothing written and the connection usable; the raise propagates. Cancellation closes the connection and cancels the COPY, as for any statement. For a few rows, an insert of unnested arrays is one statement too.

val connect : sw:Eio.Switch.t -> net:_ Eio.Net.t -> mono_clock:_ Eio.Time.Mono.t -> ?parameters:(string * string) list -> ?timeout:Mtime.Span.t -> ?statement_cache:int -> ?observe:observer -> Conninfo.t -> (conn, [> Rowtype.error ]) Stdlib.result

One connection, as Postgres_eio.connect makes it, every option passed through: a parameter setting one of the styles the driver reads text in another way is refused, as the driver refuses it. observe is around each statement it runs, a transaction's begin and commit among them, and nothing unless given.

val close : conn -> unit

Close it for good, telling the server; its next statement says so, and neither revive nor a Transaction.within opens it again. A no-op on one already closed.

val revive : conn -> (unit, [> Rowtype.error ]) Stdlib.result

Make again a connection a failure closed -- the server restarted, say -- in place, with its parameters; Ok at once for an open one. One close closed is an Error and stays closed, and so is one that cannot be made again, saying why -- an address that does not answer, a password the server now refuses.

val timeout : conn -> Mtime.Span.t option

The bound on every read and write, None for none.

val set_timeout : conn -> timeout:Mtime.Span.t option -> unit

Set timeout: lifted around work that runs longer, a migration, and put back after it.

module Pool : sig ... end

Connections made up front and lent one at a time, as Postgres_eio.Pool lends them.

module Listener : sig ... end

Notifications, on a connection of their own, as Postgres_eio.Listener hears them: a session that ran LISTEN keeps what it hears, so a listener is never a pooled connection.

module Transaction : sig ... end

One transaction on one connection, and its own failures told apart from the work's.

Statements against the database

val verify : conn -> Rowtype.any list -> (unit, [> `Disagreements of string list | Rowtype.error ]) Stdlib.result

Every statement described by the database without being run, and what it says of each compared with what the statement declares: how many parameters and, for a statement that reads rows, how many columns, and each one's type -- an int an integer or an oid, a float a float, a text text, a name, a uuid or JSON, and the rest their own, a domain as the type it is made from. An enum is read and written by a Rowtype.enum each of whose labels is one of the enum's, and refused where its labels are none or repeat one; a label only the database has is no disagreement, since a migration adds one ahead of the code that reads it, and a row holding it is refused where it is read. An enum is also written from a text, and read as one through a cast, ::text. A statement this check accepts is one run reads, in binary and in text alike. A parameter the database could not type is left to it. `Disagreements is every problem, in words for a person, each naming its statement's first line and where in it. Where the check could not ask -- a connection lost -- the answer is that failure, as a statement's is, and no statement is said to disagree.

Whether a column may be NULL is not checked: the database says so only of a column read as it is stored, and a query may promise more, so a check would refuse what is right. A NULL a shape does not allow fails where it is read, naming its column.

type error = [
  1. | Rowtype.error
  2. | `Disagreements of string list
  3. | Pool.error
  4. | Transaction.failure
]

Every failure this backend answers: a statement's, verify's, a pool's and a transaction's, which join in one result -- a transaction in a borrowed connection is Result.join (Pool.use pool (fun db -> Transaction.within db work)).

val error_to_string : [< error ] -> string

In words for a person, which never hold a parameter's value: every disagreement verify found, a pool's state and a transaction's own failure, and a statement's failure as Rowtype.error_to_string tells it.