This document describes the external interface that the coordinator presents to the protocol layers (pgwire and HTTP). In general, the concepts exposed by the coordinator API are close to native pgwire concepts (prepare, bind, execute, and so on), but there is not a 1-to-1 correspondence, and so this document is not simply a restatement of the Postgres protocol documentation.
During system startup, all users of the coordinator are instantiated with an adapter client, in some documentation also referred to as a coordinator client. This is a reference-counted handle to the coordinator, with the capability to send messages to the coordinator instructing it to perform various tasks.
Approximately the only useful actions that an adapter client allows one to take (via, behind the scenes, sending messages to the coordinator) are creating and [starting]start session sessions. A session is an abstraction of a network connection that is authenticated (that is, always associated with a valid user) and further associated with various metadata such as the values of various session variables and the set of prepared statements and executing cursors.
The state associated with a session is partly stored in the
Session
struct, and partly in a map maintained by the
coordinator and keyed on the session's connection ID.
A statement is the most general term for what is sometimes called a
query or a command. In SQL source text, statements are typically
delimited by semicolons. That said, the coordinator does not directly
accept SQL source text, but rather deals with the
Statement
AST. Statements may have parameters. These
are unspecified values that appear in places where an actual value is
expected.
Typically, before a statement may be executed, it must be prepared; preparation does some planning and type-checking and binds the statement to a name.
The next step after preparation is binding the statement to a portal, also called a cursor. It is at this stage that the values for all parameters must be specified. The lifecycles and names of portals and the prepared statements that are bound to them are not strongly connected: one prepared statement may be bound to many different portals, and may be deallocated or reassigned while the portals still exist, without affecting them.
As a convenience, preparation and binding may be combined in one step,
using an interface function called declare
. This prepares a
statement (whose parameter values must all be speciified up front) and
binds it to a portal in one step, without naming the prepared
statement.
Once a prepared statement has been bound to a portal, it may be executed. Execution may cause the catalog to be mutated in various ways, and may return a result set. Describe the full set of effects and return values of statements is outside the scope of this document; the interested reader should consult the general Materialize SQL documentation.
Note that these concepts are not only exposed by the controller
interface, but also at a higher level, to the end-user, in SQL. For
example, the user may bind the statement SELECT * FROM t
to the
portal c
by executing the SQL DECLARE c CURSOR for SELECT * FROM
t
, and may later execute this portal using the SQL FETCH FORWARD ALL
FROM c
. Alternatively, the user may make that statement into a
prepared statement named ps
by executing PREPARE ps AS SELECT *
FROM t
, and later execute it with EXECUTE ps
. It's important to
note that these statements are distinct from the ones they reference;
for example, the statement DECLARE c CURSOR for SELECT * FROM t
must
itself be prepared, bound to a portal (distinct from the portal c
),
and executed.
Once a session has been established, the owner of the adapter client gains access to the much richer interface of the session client. Most functions of this interface are implemented according to the same pattern: they send a command to the coordinator and wait for a response.
We avoid the need to design intricate locking protocols by simply requiring that the coordinator have exclusive access to the session object during the entire execution of most commands. Therefore, as part of the common communication pattern for most commands mentioned above, the entire session object is sent to the coordinator, and then sent back. The session client then simply crashes if any of these functions are called while it does not have possession of the session object.
In the upcoming sections, we will describe the various interface functions in more detail.
prepare
The SessionClient::prepare
function corresponds to the
Command::Prepare
coordinator command. Its purpose is to bind a
statement (possibly with parameters) to a name. It is called with a
description of a parsed parameterized SQL statement, a name to bind it
to, and a description of the types of its parameters (for those that
are known). The coordinator plans the statement in order to determine
the types of all parameters and the output relation (if any), and
associates this information with the given name in its per-session
state.
Using the empty string for the name may be used to specify the default prepared statement, matching postgres protocol semantics.
When successful, this function returns no value.
set_portal
The Session::set_portal
function does not correspond to
any coordinator command and, in fact, does not require communication
with the coordinator at all -- note that it is a function on the
Session
object itself, rather than on either of the coordinator
clients. Its most important parameters are a portal name, a statement
AST and its description (discovered during preparation), and a list of
values for the parameters of the statement. The session assigns the
given statement to a portal of the given name.
When successful, this function returns no value.
declare
The SessionClient::declare
function corresponds to the
Command::Declare
coordinator command. It behaves essentially like a
combination of SessionClient::prepare
and Session::set_portal
, preparing a statement and
binding it to a portal for execution, except that it does not cause
the statement to be bound to a prepared statement name (only to a
portal name).
When successful, this function returns no value.
execute
The SessionClient::execute
function corresponds to the
Command::Execute
coordinator command. Its purpose is to begin
execution of a portal to which a statement and set of parameters has
previously been bound. It is called with the name of the portal and a
wire called cancel_future
on which the coordinator can listen for
requests to cancel the execution.
When successful, this function returns an ExecuteResponse
, whose
meaning varies depending on the kind of statement being executed. In
some cases it means that the statement's execution has finished; in
others, it only means that it has begun, and the client must either
wait for results, or take further actions to drive it to
completion. The specific protocols for various types of statements are
described in more detail below.
SELECT
queriesThe coordinator responds with an instance of
ExecuteResponse::SendingRows
, which contains a future
that will ultimately resolve to a batch of rows, an error, or a
cancelation signal. If the query is successful, all rows it returns
will be available as the output of the future as soon as it resolves
(i.e., this is not a streaming API).
In general, queries may take arbitrarily long to finish, including
possibly never terminating. If the client is no longer interested in
the results of a query, for example because the end user disconnected
or requested a cancellation, then the client is expected to
communicate that fact to the coordinator by causing the
cancel_future
wire discussed above to activate.
The coordinator responds to a bare SUBSCRIBE
statement (that is, one
that is not part of a COPY ... TO STDOUT
statement) with the an instance of
ExecuteResponse::Subscribing
. This response contains
a channel on which batches of rows are received. The client may
continue drawing from this channel until either it closes gracefully
(indicating that the subscribe has finished, as can happen for
constant relations), or an error or cancellation is indicated.
As is the case for SELECT
queries, if the client is no longer
interested in further responses, it should indicate that fact by
actuating the corresponding cancel_future
.
COPY ... TO STDOUT
The coordinator responds to such a statement with an instance of
ExecuteResponse::CopyTo
, which simply wraps an instance of
the ExecuteResponse
for the inner subscription or
SELECT
query.
COPY ... FROM STDIN
The coordinator responds with an instance of
ExecuteResponse::CopyFrom
containing some metadata about the
expected input format. The execution of the statement has by no means
finished at this point; this ExecuteResponse
is merely an
instruction to the client to collect some data from the end user,
decode it into rows, and pass them to the coordinator in the
SessionClient::insert_rows
method (which corresponds to a
Command::CopyRows
coordinator command). After the
coordinator receives this second command, the statement execution is
considered finished.
FETCH
The coordinator responds with an instance of
ExecuteResponse::Fetch
, which instructs the session to
execute a cursor with a given name (i.e., the cursor specified
textually in the FETCH
statement being executed). Thus, a total of
two calls to execute
should happen as part of this flow (an "outer"
one to execute
the FETCH
statement, and an "inner" one execute
the cursor referenced in the FETCH
statement). The entire statement
execution is considered to have finished once this "inner" execute
finishes.
Unless described differently elsewhere, the typical statement
terminates in the coordinator, and no further action by the client is
necessary. To give one arbitrary example among many, if a CREATE
SOURCE
statement is successful, the coordinator responds with an
instace of ExecuteResponse::CreatedSource
, which
carries no additional information and requires no response.