postgres.bend relies on unsafe/foreign
raw source on the hub · import bend-kit-postgres@0.1.0.1/postgres.bend as Postgres
Postgres client: protocol 3.0 over TCP or TLS, SCRAM-SHA-256, prepared queries, and a pool. Source: https://github.com/paymog/bend-kit/tree/main/postgres
6 imports
import Base import bend-kit-bytes@0.3.1.0/bytes.bend as Bytes import bend-kit-wire@0.4.2.0/wire.bend as Wire import bend-kit-dns@0.5.0.0/dns.bend as Dns import ./codec.bend as Codec import ./scram.bend as Scram
Types
type Config source · line 12 · raw
Data
host is a name or a numeric address; TLS checks the certificate against it. With tls set, a server that refuses SSLRequest fails with NoTls rather than falling back to TCP. ms bounds each connect and read (0: none). user, pass, and db are byte strings (one Char per octet).
Config@host:String -> @port:U32 -> @tls:Bool -> @user:String -> @pass:String -> @db:String -> @ms:U32 -> Config
type Err source · line 20 · raw
Data
A server error: the SQLSTATE code (for example "42P01") and the message.
Err@code:String -> @message:String -> Err
type Failure source · line 25 · raw
Type
A failed call closes its connection. Rejected carries the server's error during startup; Auth is an authentication method this client does not speak, or a SCRAM check that failed.
IoFail@code:U32 -> @why:String -> Failure
ClosedFailure
MalformedFailure
NoTlsFailure
Auth@why:String -> Failure
Rejected@err:Err -> Failure
type Reply source · line 36 · raw
Type
A query's result. Rows holds the columns, the rows in order (None is NULL, values in text format), and the command tag ("SELECT 2", "INSERT 0 1"). Failed is an error the server reported for this query; the connection stays in step and can run the next one.
Rows@fields:List<&1, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Field> -> @rows:List<&1, List<&1, Maybe<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>>> -> @tag:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Reply
Failed@err:Err -> Reply
type Conn source · line 42 · raw
Type
key names the pool bucket: TLS, user, host, port, and db (never the password). status is the last ReadyForQuery byte: 73 'I' idle, 84 'T' in a transaction, 69 'E' failed one.
Conn@tls:Bool -> @sock:Socket -> @rd:0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Reader -> @key:String -> @ms:U32 -> @status:U32 -> Conn
type Rd source · line 132 · raw
Type
RNext holds the reader after its last decode attempt; RFail a socket to close.
RNext@tls:Bool -> @sock:Socket -> @key:String -> @ms:U32 -> @status:U32 -> @r:Pair(0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Reader, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Next) -> Rd
RFail@tls:Bool -> @sock:Socket -> @why:Failure -> Rd
type Acc source · line 462 · raw
Type
Acc@fields:List<&1, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Field> -> @rows:List<&1, List<&1, Maybe<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>>> -> @tag:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> @err:Maybe<&1, Err> -> Acc
type Step source · line 465 · raw
Type
Go@acc:Acc -> Step
Stop@acc:Acc -> @status:U32 -> Step
OddStep
type Pool source · line 582 · raw
Type
Idle connections per key, up to cap per key.
Pool@cap:U32 -> @idle:Map<&1, List<&1, Conn>> -> Pool
Definitions
def config source · line 16 · raw
@+host:String -> @+port:U32 -> @+user:String -> @+pass:String -> @+db:String -> Config
TLS on, 10 s deadlines.
def text source · line 46 · raw
@+s:String -> Maybe<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>
A parameter in text format.
def bytes source · line 49 · raw
@+s:String -> 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes
def err.put source · line 54 · raw
@+k:U32 -> @+s:String -> @e:Err -> Err
def err.go source · line 58 · raw
@fs:List<&1, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Notice> -> @e:Err -> Err
def err.of source · line 66 · raw
@fs:List<&1, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Notice> -> Err
The C (SQLSTATE) and M (message) fields of an ErrorResponse.
def io.close source · line 71 · raw
@tls:Bool -> @s:Socket -> IO(Unit)
def io.send source · line 78 · raw
@tls:Bool -> @s:Socket -> @+len:U32 -> @buf:Array<U32> -> IO(Pair(Socket, Result<&1, &1, Pair(U32, String), Unit>))
def io.write source · line 85 · raw
@tls:Bool -> @s:Socket -> @data:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> IO(Pair(Socket, Result<&1, &1, Pair(U32, String), Unit>))
def io.recv source · line 89 · raw
@tls:Bool -> @s:Socket -> @+ms:U32 -> IO(Pair(Socket, Result<&1, &1, Pair(U32, String), Pair(U32, Array<U32>)>))
def io.close.after source · line 96 · raw
@tls:Bool -> @m:Pair(Socket, Result<&1, &1, Pair(U32, String), Unit>) -> IO(Unit)
def close source · line 101 · raw
@c:Conn -> IO(Unit)
Sends Terminate, then closes the socket.
def abort source · line 107 · raw
@-A:Type -> @c:Conn -> @why:Failure -> IO(Result<&1, &1, Failure, A>)
def reject source · line 112 · raw
@-A:Type -> @c:Conn -> @fs:List<&1, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Notice> -> IO(Result<&1, &1, Failure, A>)
def send.after source · line 115 · raw
@+tls:Bool -> @rd:0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Reader -> @+key:String -> @+ms:U32 -> @+status:U32 -> @m:Pair(Socket, Result<&1, &1, Pair(U32, String), Unit>) -> IO(Result<&1, &1, Failure, Conn>)
def send source · line 125 · raw
@c:Conn -> @data:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> IO(Result<&1, &1, Failure, Conn>)
def got.len source · line 136 · raw
@zero:Bool -> @+tls:Bool -> @+key:String -> @+ms:U32 -> @+status:U32 -> @rd:0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Reader -> @s:Socket -> @+len:U32 -> @words:Array<U32> -> Rd
def got source · line 143 · raw
@+tls:Bool -> @+key:String -> @+ms:U32 -> @+status:U32 -> @rd:0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Reader -> @m:Pair(Socket, Result<&1, &1, Pair(U32, String), Pair(U32, Array<U32>)>) -> Rd
def read.fail source · line 151 · raw
@tls:Bool -> @sock:Socket -> @why:Failure -> IO(Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)>)
def read.loop source · line 157 · raw
@fuel:Nat -> @st:Rd -> IO(Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)>)
One recv per step. NoticeResponse and ParameterStatus may arrive at any time; they are skipped.
def read source · line 187 · raw
@c:Conn -> IO(Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)>)
The next message. Bytes already read are decoded before the socket is asked for more. 2^24 reads of up to 64 KiB pass Postgres's 1 GiB field limit.
def reply source · line 192 · raw
@r:Result<&1, &1, Failure, Conn> -> IO(Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)>)
The next message after a send.
def exchange source · line 199 · raw
@c:Conn -> @m:0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Front -> IO(Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)>)
def set.status source · line 204 · raw
@c:Conn -> @+status:U32 -> Conn
def ready.loop source · line 211 · raw
@fuel:Nat -> @r:Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)> -> IO(Result<&1, &1, Failure, Conn>)
BackendKeyData, then ReadyForQuery.
def ready source · line 236 · raw
@c:Conn -> IO(Result<&1, &1, Failure, Conn>)
def auth.done source · line 242 · raw
@r:Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)> -> IO(Result<&1, &1, Failure, Conn>)
AuthenticationOk, after a password or the SCRAM exchange.
def sasl.verified source · line 255 · raw
@c:Conn -> @v:Result<&1, &1, Pair(U32, String), Unit> -> IO(Result<&1, &1, Failure, Conn>)
def sasl.final source · line 265 · raw
@fin:0x93d94e676e81df7c0c1a0e8f66ffa209/scram.Final -> @r:Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)> -> IO(Result<&1, &1, Failure, Conn>)
AuthenticationSASLFinal carries the server signature, which proves the server knew the password.
def sasl.respond source · line 280 · raw
@c:Conn -> @f:Result<&1, &1, Pair(U32, String), Pair(0x93d94e676e81df7c0c1a0e8f66ffa209/scram.Final, String)> -> IO(Result<&1, &1, Failure, Conn>)
def sasl.cont source · line 289 · raw
@st:0x93d94e676e81df7c0c1a0e8f66ffa209/scram.First -> @+pass:String -> @r:Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)> -> IO(Result<&1, &1, Failure, Conn>)
def sasl.first source · line 304 · raw
@c:Conn -> @+pass:String -> @f:Result<&1, &1, Pair(U32, String), Pair(0x93d94e676e81df7c0c1a0e8f66ffa209/scram.First, String)> -> IO(Result<&1, &1, Failure, Conn>)
def sasl source · line 313 · raw
@ok:Bool -> @c:Conn -> @+user:String -> @+pass:String -> IO(Result<&1, &1, Failure, Conn>)
def has.mech source · line 322 · raw
@xs:List<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes> -> Bool
def auth source · line 330 · raw
@+user:String -> @+pass:String -> @r:Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)> -> IO(Result<&1, &1, Failure, Conn>)
The first message after StartupMessage picks the authentication method.
def startup source · line 351 · raw
@c:Conn -> @+user:String -> @+pass:String -> @+db:String -> IO(Result<&1, &1, Failure, Conn>)
def tls.done source · line 356 · raw
@+key:String -> @+ms:U32 -> @+user:String -> @+pass:String -> @+db:String -> @m:Pair(Socket, Result<&1, &1, Pair(U32, String), Unit>) -> IO(Result<&1, &1, Failure, Conn>)
def plain.fail source · line 366 · raw
@s:Socket -> @why:Failure -> IO(Result<&1, &1, Failure, Conn>)
def ssl.byte source · line 372 · raw
@s:Socket -> @+key:String -> @+host:String -> @+user:String -> @+pass:String -> @+db:String -> @+ms:U32 -> @r:Pair(0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes, Maybe<&2, U32>) -> IO(Result<&1, &1, Failure, Conn>)
'S' starts TLS; 'N' refuses it. Anything else (an old server's error) is not an answer.
def ssl.answer source · line 389 · raw
@+key:String -> @+host:String -> @+user:String -> @+pass:String -> @+db:String -> @+ms:U32 -> @a:Pair(Socket, Result<&1, &1, Pair(U32, String), Pair(U32, Array<U32>)>) -> IO(Result<&1, &1, Failure, Conn>)
The answer is one byte, read alone so no byte sent before the TLS handshake is taken as data.
def ssl.sent source · line 397 · raw
@+key:String -> @+host:String -> @+user:String -> @+pass:String -> @+db:String -> @+ms:U32 -> @m:Pair(Socket, Result<&1, &1, Pair(U32, String), Unit>) -> IO(Result<&1, &1, Failure, Conn>)
def ssl.ask source · line 407 · raw
@s:Socket -> @+key:String -> @+host:String -> @+user:String -> @+pass:String -> @+db:String -> @+ms:U32 -> IO(Result<&1, &1, Failure, Conn>)
def dialed source · line 412 · raw
@r:Result<&1, &1, Pair(U32, String), Socket> -> @tls:Bool -> @+key:String -> @+host:String -> @+user:String -> @+pass:String -> @+db:String -> @+ms:U32 -> IO(Result<&1, &1, Failure, Conn>)
def dial.after source · line 423 · raw
@r:Result<&1, &1, Pair(U32, String), Socket> -> @+ip:String -> @+port:U32 -> @+ms:U32 -> IO(Result<&1, &1, Pair(U32, String), Socket>)
def dial.more source · line 430 · raw
@prev:IO(Result<&1, &1, Pair(U32, String), Socket>) -> @+ip:String -> @+port:U32 -> @+ms:U32 -> IO(Result<&1, &1, Pair(U32, String), Socket>)
def dial source · line 436 · raw
@ips:List<&2, String> -> @+port:U32 -> @+ms:U32 -> @acc:IO(Result<&1, &1, Pair(U32, String), Socket>) -> IO(Result<&1, &1, Pair(U32, String), Socket>)
Each address in turn, until one connects.
def key source · line 443 · raw
@cfg:Config -> String
def connect.to source · line 448 · raw
@+k:String -> @cfg:Config -> IO(Result<&1, &1, Failure, Conn>)
def connect source · line 457 · raw
@+cfg:Config -> IO(Result<&1, &1, Failure, Conn>)
TCP, SSLRequest and TLS when cfg.tls (checking the certificate and host name), StartupMessage, cleartext or SCRAM-SHA-256 authentication, then ReadyForQuery. MD5 is refused as Auth.
def acc.fields source · line 470 · raw
@a:Acc -> @fs:List<&1, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Field> -> Acc
def acc.row source · line 475 · raw
@a:Acc -> @cols:List<&1, Maybe<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>> -> Acc
Rows are kept newest first until ReadyForQuery.
def acc.tag source · line 479 · raw
@a:Acc -> @t:0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes -> Acc
def err.first source · line 483 · raw
@old:Maybe<&1, Err> -> @e:Err -> Maybe<&1, Err>
def acc.err source · line 490 · raw
@a:Acc -> @e:Err -> Acc
def absorb source · line 494 · raw
@a:Acc -> @m:0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back -> Step
def absorbed source · line 521 · raw
@a:Acc -> @r:Result<&1, &1, Failure, Pair(Conn, 0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Back)> -> Result<&1, &1, Failure, Pair(Conn, Step)>
def finish source · line 528 · raw
@c:Conn -> @+status:U32 -> @a:Acc -> IO(Result<&1, &1, Failure, Pair(Conn, Reply)>)
def collect source · line 538 · raw
@fuel:Nat -> @r:Result<&1, &1, Failure, Pair(Conn, Step)> -> IO(Result<&1, &1, Failure, Pair(Conn, Reply)>)
After an ErrorResponse the server skips to Sync, so reading on to ReadyForQuery keeps the connection in step. A message that has no place in the reply closes it.
def query.bytes source · line 562 · raw
@+sql:String -> @params:List<&1, Maybe<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>> -> 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes
The whole extended-query exchange for one statement, in one write.
def query source · line 573 · raw
@c:Conn -> @+sql:String -> @params:List<&1, Maybe<&1, 0xb7603dcfa1d7f60d01e06c928738cc73/bytes.Bytes>> -> IO(Result<&1, &1, Failure, Pair(Conn, Reply)>)
Runs sql (a byte string, parameters as $1, $2, ...) as an unnamed prepared statement: Parse, Bind, Describe, Execute, Sync. Parameters and results are in text format; None is NULL. A server error is a Failed reply on a live connection; a Failure has closed it.
def pool.new.with source · line 585 · raw
@+cap:U32 -> Pool
def pool.new source · line 588 · raw
Pool
def pool.take source · line 591 · raw
@+cap:U32 -> @+k:String -> @r:Pair(Map<&1, List<&1, Conn>>, Maybe<&1, List<&1, Conn>>) -> Pair(Pool, Maybe<&1, Conn>)
def pool.opened source · line 603 · raw
@p:Pool -> @r:Result<&1, &1, Failure, Conn> -> IO(Pair(Pool, Result<&1, &1, Failure, Conn>))
def pool.got source · line 606 · raw
@cfg:Config -> @r:Pair(Pool, Maybe<&1, Conn>) -> IO(Pair(Pool, Result<&1, &1, Failure, Conn>))
def pool.get source · line 618 · raw
@p:Pool -> @+cfg:Config -> IO(Pair(Pool, Result<&1, &1, Failure, Conn>))
The idle connection given back last, or a new one. ponytail: an idle connection the server has since closed fails on its first query; ping before reuse if that bites.
def conns.cut source · line 624 · raw
@xs:List<&1, Conn> -> @n:Nat -> IO(List<&1, Conn>)
The first n connections; the rest are closed.
def pool.keep source · line 639 · raw
@+cap:U32 -> @+k:String -> @c:Conn -> @r:Pair(Map<&1, List<&1, Conn>>, Maybe<&1, List<&1, Conn>>) -> IO(Pool)
def pool.clean source · line 645 · raw
@clean:Bool -> @p:Pool -> @c:Conn -> IO(Pool)
def pool.left source · line 656 · raw
@p:Pool -> @+tls:Bool -> @sock:Socket -> @+key:String -> @+ms:U32 -> @+status:U32 -> @r:Pair(0x93d94e676e81df7c0c1a0e8f66ffa209/codec.Reader, U32) -> IO(Pool)
def pool.put source · line 662 · raw
@p:Pool -> @c:Conn -> IO(Pool)
Gives c back for reuse. A connection with unread bytes is out of step, and one inside a transaction would leak it to the next user, so either is closed instead.
def pool.close.go source · line 666 · raw
@xs:List<&1, List<&1, Conn>> -> IO(Unit)
def pool.close source · line 675 · raw
@p:Pool -> IO(Unit)