Rowtype_sqliterowtype over SQLite, through the sqlite3 binding: the one place the two meet, and the one package of rowtype that binds C, since SQLite is a library, not a server, and has no protocol to speak in OCaml.
Statements are SQLite's as written. A placeholder is $1, $2, ... as in every backend, or SQLite's own ?1 or ?, each bound by its number from the shape's values, so $1 twice asks about one value twice. One with a name, :name, is refused, since a shape's values have none, and so are placeholders that do not number the values from one, each at least once, before anything runs. One statement is run at a time: a text holding several is exec_raw's, and a space, a comment or a ; after a statement is no second one.
SQLite keeps each value as an integer, a float, text or a blob, whatever its column was declared as, and each scalar is kept as one of them:
int, an int64 and a bool as an integer, a bool 0 or 1;float as a float, and read from an integer too within 253 either side of zero, where a float holds every integer exactly; a NaN is kept as NULL, as SQLite keeps one;text, a json and an enum's label as text, and bytes as a blob;Rowtype.instant as text in UTC, 2026-10-20 09:24:46.621000, the form SQLite's date and time functions read and write, to the microsecond, so a column of them sorts as time does and compares with datetime('now'); read with the zone the text gives -- Z, +02:00 -- and in UTC where it gives none, as SQLite reads one, to the microsecond;Rowtype.timestamp as the same text, a reading on no clock, and one that gives a zone is refused;Rowtype.date as 2026-10-20;Rowtype.interval as ISO 8601 text, P14M3DT-0.000005S: months, days and seconds, each signed on its own;Rowtype.uuid as text, 7c9e6679-7425-40de-944b-e07fc1f90ae7, and read from a blob of sixteen bytes too;Rowtype.array as a JSON array, as text, which json_each reads in the SQL -- insert into t select value from json_each($1) -- and json_group_array writes; a bytes element is lowercase hex in it, which unhex reads.A value of another kind than its scalar's is refused, naming the kind and never the value. A unique, primary-key or foreign-key violation is `Conflict, named by the columns SQLite names, users.email, and a foreign key's by none, since SQLite names none. SQLITE_BUSY -- a lock another connection still held when the wait ran out, preparing a statement or running it -- is `Not_serializable: nothing of the statement was done, and running it again may succeed.
A statement SQLite cannot prepare fails in SQLite's words, which quote only its text: no such table: t. One that fails as it runs is told by SQLite's code alone, since a function quotes its argument in its message -- a JSON path, an FTS5 query -- and a trigger's RAISE says what its author wrote; a NOT NULL or CHECK constraint keeps SQLite's words, which name the column or the constraint and no value.
Each connection enforces foreign keys, which SQLite leaves off unless asked. Each statement is a debug line on rowtype-sqlite with how long it took: its first line of text, never its parameters. SQLite's calls block, so a statement runs on a system thread, as Eio_unix.run_in_systhread runs one, and its rows are read in chunks of 64, so the domain's other fibers run while it waits on the disk or a lock. A connection is one fiber's at a time. A fiber cancelled during a statement waits for the chunk being read, then the statement is ended and the cancellation passes; a raise from fold's function passes too, and the connection is as usable after either as before.
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 := connval run :
conn ->
('p, 'r) Rowtype.statement ->
'p ->
('r, [> Rowtype.error ]) Stdlib.resultrun 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.resultfold 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.resultrun_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.resultFor 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.resultcopy_in db ~table ~columns row rows writes the rows with one prepared insert, each bound as run binds a statement's parameters, all of them or none, and answers how many were written. The sequence is read as the rows are written, a chunk at a time, so a load of any size is never held whole.
schema is the attached database the table is in, main unless given. columns are in the row's order, every column in table order where empty, and quoted as given; a number of them that is not the row's arity is refused before anything is written. A row that cannot be bound, a constraint and a raise from the sequence each fail the whole load, with nothing written; the raise passes. Inside a transaction, the load is the transaction's, and goes with it.
val connect :
sw:Eio.Switch.t ->
mono_clock:_ Eio.Time.Mono.t ->
?timeout:Mtime.Span.t ->
?observe:observer ->
string ->
(conn, [> Rowtype.error ]) Stdlib.resultconnect ~sw ~mono_clock file opens the database in file, making it where there is none, as SQLite names one: ":memory:" is one in memory, this connection's alone. timeout is how long a statement waits for a lock another connection holds before it is `Not_serializable, 5 s unless given, to the millisecond above it. observe is around each statement it runs, a transaction's begin and commit among them, and nothing unless given. A file that cannot be opened is `Unsent. The connection is closed when sw finishes, unless close closed it first.
val close : conn -> unitClose it for good; its next statement is `Connection `Closed. A no-op on one already closed.
module Pool : sig ... endConnections made up front to one file and lent one at a time.
module Transaction : sig ... endOne transaction on one connection, and its own failures told apart from the work's.
val verify :
conn ->
Rowtype.any list ->
(unit, [> `Disagreements of string list | Rowtype.error ]) Stdlib.resultEvery statement prepared without being run, and what SQLite says of each compared with what the statement declares: whether it prepares at all, how many parameters and, for a statement that reads rows, how many columns, and each column read straight from a table by its declared type -- an integer column as one an int, an int64 or a bool reads, a real one as a float's, a text one as text's, an instant's or any other kept as text, and a blob one as bytes or a uuid. A column made by an expression, or declared with no type, holds any kind of value, and is checked where it is read. Its placeholders are held to run's rule -- numbered from one, each value taken at least once, none past the values declared, none named -- so a statement verify passes is not refused for them when it runs. `Disagreements is every problem, in words for a person, each naming its statement's first line and where in it.
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 ] -> stringIn words for a person, which never hold a 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.