class CQL::Postgres::Session

Overview

One protocol-v3 conversation with a server over a single stream.

Every public operation takes the session's lock, so at most one request is in flight and fibers can share a session safely (they queue). The session counts the replies the server still owes (one ReadyForQuery per Sync or simple query it was sent). After a ServerError, or an exception raised by the caller's own block, nothing is owed and the session stays usable; any failure that leaves replies unread closes it, since the stream could no longer be matched up with requests.

#on_notice and #on_notification handlers run while the session's lock is held, so they must not use the same session.

Defined in:

postgres/session.cr
postgres/session/copying.cr
postgres/session/pipelining.cr
postgres/session/queries.cr
postgres/session/streaming.cr

Constant Summary

CANCEL_GRACE = 5.seconds
Log = ::Log.for("cql.postgres")
PING_TIMEOUT = 5.seconds
PIPELINE_BATCH_BYTES = 32 * 1024

A batch is written completely before any reply is read, so it has to fit in the socket buffers: if the server's replies filled our receive buffer while we were still writing, neither side could make progress.

Constructors

Instance Method Summary

Constructor Detail

def self.connect(config : Config) : Session #

Connects, asking for the newest protocol the config allows. A server that rejects that version outright (before PostgreSQL 9.3.21, or some poolers) gets one more try with 3.0 when the config allows it.


[View source]
def self.new(config : Config, io : IO, outcome : Handshake::Outcome, socket : IO = io) #

[View source]

Instance Method Detail

def binary_params_sent : Int64 #

How many bind parameters have gone out in binary on this session (the ones binary_params allowed and the server's types had a form for).


[View source]
def cancel : Nil #

Asks the server, over a second connection, to cancel what this session is running. Safe to call from any fiber.


[View source]
def cancel_grace : Time::Span #

The patience for cancelling. A timed-out request has this long, from the moment its timeout fires, for the cancel to take effect before its socket is closed under it. A cancel's own connect and wait for the postmaster's hang-up are each capped at it, and a request that was cancelled waits up to twice this for the cancel to finish before the session is handed on (and drops the session if it still has not).


[View source]
def cancel_grace=(cancel_grace : Time::Span) #

The patience for cancelling. A timed-out request has this long, from the moment its timeout fires, for the cancel to take effect before its socket is closed under it. A cancel's own connect and wait for the postmaster's hang-up are each capped at it, and a request that was cancelled waits up to twice this for the cancel to finish before the session is handed on (and drops the session if it still has not).


[View source]
def close : Nil #

Ends the session politely (Terminate) once any in-flight request is done. The Terminate write can still wait on a dead path with a full send buffer (the write timeout is lifted after startup); use #hard_close to cut that short.


[View source]
def closed? : Bool #

[View source]
def config : Config #

[View source]
def copy_in(sql : String, timeout : Time::Span | Nil = nil, & : CopyIn -> ) : Int64 #

Streams COPY ... FROM STDIN; returns the number of rows copied. If the block raises, the COPY is aborted and the exception re-raised.


[View source]
def copy_in_binary(sql : String, types : Array(UInt32), timeout : Time::Span | Nil = nil, & : BinaryCopy::Writer -> ) : Int64 #

COPY ... FROM STDIN (FORMAT binary) with the signature and trailer handled for you: types are the OIDs of the copied columns and the block writes rows. If the block raises, the COPY is aborted like #copy_in. A statement the server runs as text raises ArgumentError before any data moves.

engine = CQL::Postgres::AsyncEngine.connect(ENV["DATABASE_URL"])
types = [CQL::Postgres::Types::OID::INT8, CQL::Postgres::Types::OID::TEXT]
engine.with_connection do |connection|
  connection.session.copy_in_binary("COPY t (id, name) FROM STDIN (FORMAT binary)", types) do |rows|
    rows.row(1_i64, "a")
  end
end

[View source]
def copy_out(sql : String, timeout : Time::Span | Nil = nil, & : Bytes -> ) : Int64 #

Streams COPY ... TO STDOUT chunks to the block; returns rows copied. If the block raises, the remaining chunks are read and dropped before the exception is re-raised.


[View source]
def copy_out_binary(sql : String, types : Array(UInt32), timeout : Time::Span | Nil = nil, &on_row : Array(Types::Value) -> ) : Int64 #

COPY ... TO STDOUT (FORMAT binary): yields each row decoded by the column types (OIDs; a type without a decoder arrives as raw bytes). Stopping early, timeouts and cancel behave as in #copy_out; a statement the server runs as text raises ArgumentError once its output has been read.


[View source]
def encode(value) : Types::Param #

A value as a bind parameter for this session, in binary if its config allows.


[View source]
def forget_prepared_statements : Nil #

Forgets every cached statement after the server dropped them (DISCARD ALL, DEALLOCATE ALL).


[View source]
def hard_close : Nil #

Drops the stream immediately.


[View source]
def in_transaction? : Bool #

[View source]
def inspect(io : IO) : Nil #

Identity and state only: the cancel key, staged protocol bytes and SQL stay out of logs.


[View source]
def last_used : Time::Instant #

[View source]
def on_notice : Proc(Hash(Char, String), Nil) | Nil #

[View source]
def on_notice=(on_notice : Proc(Hash(Char, String), Nil) | Nil) #

[View source]
def on_notification : Proc(Notification, Nil) | Nil #

[View source]
def on_notification=(on_notification : Proc(Notification, Nil) | Nil) #

[View source]
def ping(timeout : Time::Span = PING_TIMEOUT) : Bool #

One cheap round trip (an empty query). If the server does not answer within timeout the socket is closed (a silently dropped path would otherwise block this forever) and the answer is false.


[View source]
def pipeline(statements : Array(Tuple(String, Array(Types::Param))), timeout : Time::Span | Nil = nil) : Array(Result | ServerError) #

Sends several statements before reading any answer, so they cost one network round trip (two for statements not prepared yet). Large batches go out in chunks that fit the socket buffers. Each statement has its own sync point: one failing leaves the others unaffected and shows up as a ServerError in its slot. A timeout bounds the whole pipeline: when it expires the running statement is canceled, the rest are abandoned, the session is dropped and QueryTimeout is raised. A server FATAL raises the error and drops the session; results already read are lost.


[View source]
def process_id : Int32 #

[View source]
def protocol_version : ProtocolVersion #

[View source]
def query(sql : String, params : Array(Types::Param) = [] of Types::Param, cache : Bool = true, timeout : Time::Span | Nil = nil) : Result #

Runs one statement with bound parameters through the extended protocol, preparing it once per connection when cache is on.


[View source]
def requests : Int64 #

Requests run on this session so far (a pipeline counts once).


[View source]
def secret_key : Bytes #

[View source]
def server_parameters : Hash(String, String) #

[View source]
def simple_query(sql : String, timeout : Time::Span | Nil = nil) : Array(Result) #

Runs text with the simple protocol, which allows several statements. Every column comes back as text.


[View source]
def stream(sql : String, params : Array(Types::Param) = [] of Types::Param, fetch_size : Int32 = 1000, cache : Bool = true, timeout : Time::Span | Nil = nil, & : RowStream -> T) : T forall T #

Runs one statement and yields its rows fetch_size at a time (Execute with a row limit; the portal stays open between fetches). The session is locked for the whole block, so use another connection inside it. When the block ends, however early, the portal is closed and the session left in sync, ready for the pool; the stream cannot be read afterwards. timeout bounds the whole stream, consumer time included: once it passes, the next fetch raises QueryTimeout and the session is dropped unless the server confirmed a cancel. Session#cancel ends a stream that still has rows to fetch: it cancels the fetch in progress, or, if the portal is suspended and the server has nothing to cancel, makes the next fetch raise QueryCanceled (rows already buffered, up to the fetch size, are still delivered first). A cancel still in flight when the request ends is waited for (every request does this, not only streams), so it cannot hit the session's next statement; a cancel that lands on the stream's own cleanup is ignored. A cancel begun while no request holds the session is not covered. COPY cannot be streamed.


[View source]
def transaction_status : Char #

[View source]