WL#9270: X Protocol Prepared Statement execution
Motivation
Applications that repeatedly execute the same kind of statements with changed
values, currently have to go through same parse-execute
cycle for every
execution.
In the case of the Document Store a large part of the queries are simple
_id
based lookups which can be accelerated by preparing the Crud once
and executing it often.
Goals
Move SQL prepare functionality to X Protocol. Give user a possibility to prepared and execute DevAPI CRUD (X Protocol CRUD).
Optional Goals
Performance boost should be visible when the client-application
is going
to switch from SQL/CRUD execution to prepared statement execution.
Notes
Pipelining
Prepared Statements allow to split off the parse
phase from the execute
into two steps and allow to repeatedly execute a statement with different
values without reparsing the statement all the time.
In the classic protocol this is solved by:
C->S: COM_STMT_PREPARE(sql)
C<-S: (stmt_id, arg_count)
C->S: COM_STMT_EXECUTE(stmt_id, args, flags, ...)
C<-S: (resultset)
As the EXECUTE has to wait for the response of the PREPARE to get access to the
server assigned statement-id
using prepared-statements results in two
round-trips.
If prepared-statements are used as a performance feature the user has decide upfront if round-trip time of PREPARE + EXECUTE is less than the time spent to send just pipeline the statements and let the server parse them again and again.
As the xprotocol supports pipelining statements already, the prepared statements MUST support pipelining too.
Design requirements
- DR1
- user MUST be able to prepare and execute a SQL statement
- DR2
- user MUST be able to prepare and execute a CRUD statement
- DR3
- user MUST be able to pipeline a prepare and execute statement
- DR4
- user MUST be able to execute a earlier prepared statement
- DR5
- user MUST be able to close a prepared statement
- DR6
-
resultset of
execute
MUST be the same as the resultset of a directly executed statement (Sql::StmtExecute, Crud::Find, ...) - DR7
- user MUST be able to fetch a part of the resultset generated by prepared-statement execution
- DR8
-
user MUST be able to cleanup the resourced allocated
prepare
Function requirements - Server
- FR_S1
-
SQL accepted via the classic protocol for
COM_STMT_PREPARE
MUST be accepted by the xprotocol implementation too - FR_S2
-
arguments specified in the
execute
MUST replace wild-cards specified theprepare
- FR_S3
-
execute
MUST accept arguments - FR_S4
-
execute
MUST return out-parameters of stored procedures - FR_S5
-
close
MUST close a prepared statement and release its resources - FR_S6
-
closing a prepared-statement that has
cursor
assigned MUST close thecursor
- FR_S7
-
cursor
MUST be able to open/execute a prepared statement - FR_S8
-
cursor
MUST be able to fetch all resultsets referenced by acursor
- FR_S9
-
cursor
MUST be able to fetch a part of resultset/resultsets referenced by acursor
- FR_S10
-
execute
after aexecute
with a cursor MUST implicitlyclose
the cursor - FR_S11
-
preparing a statement with a statement-id already assigned MUST
close
the old prepared statement - FR_S12
-
closing X session should close all prepared-statements and
cursors
- FR_S13
-
reseting X session should close all prepared-statements and
cursors
- FR_S14
-
user should be able to use prepared-statements and
cursors
with expectations (for example open expect block with NO_ERROR)
error-cases
- FR_SE1
- executing a statement with a unassigned prepare-statement-id MUST fail
- FR_SE2
- closing a prepare-statement-id that's not in use MUST fail
- FR_SE3
-
fetching from a
cursor
with unassigned cursor-id MUST fail - FR_SE4
-
closing a
cursor
with unassigned cursor-id MUST fail - FR_SE5
-
fetching from a
cursor
that reached end of data MUST fail - FR_SE6
- preparing non SQL and non CRUD statement MUST fail
Function requirements - Client
- FR_C1
-
libmysqlxclient MUST support
prepare
,execute
,close
prepare statement operations - FR_C2
-
libmysqlxclient MUST be able to
fetch
a subset of rows - FR_C3
- libmysqlxclient MUST be able to operate on X Protocol messages directly
- FR_C4
- libmysqlxclient MUST be have API at session level to operate on prepared statements and cursors
Protocol
This worklog introduces two types of objects, prepared-statements and cursors. Both are managed by X Protocol messages and referred by numerical IDs. IDs are chosen by the client. Client must ensure that assigned IDs are reused only when ID is not needed or deallocated explicitly before reuse. Cursors and prepared-statement object in classic protocol are the same object, still to make X Protocol more flexible it was separated.
Prepared-statements
Multiple executions of same SQL statement is one of the expensive operations that can be optimized in classic-protocol using prepared statements. Adding prepared-statements to X Protocol gives additional gain in case of CRUD operations. When a CRUD operation is execute, X Plugin generates SQL from protobuf messages and the SQL is parsed by the server, thus the query is created and parsed each time before execution.
Mysqlx.Prepare.Prepare
Instead of generating the query and parsing it on the server side multiple times, user can do it just once and cache the result. Later on the cached query can be executed just by referring to its ID. SQL or CRUD statement can be prepared on X Protocol using following message:
message Prepare {
required uint32 stmt_id = 1;
// Determine which of optional fields was set by the client
// (Workaround for missing "oneof" keyword in pb2.5)
enum OneOfMessageType {
FIND = 0;
INSERT = 1;
UPDATE = 2;
DELETE = 4;
STMT = 5;
}
message OneOfMessage {
required OneOfMessageType type = 1 /* OneOfMessageType */;
optional Mysqlx.Crud.Find find = 2;
optional Mysqlx.Crud.Insert insert = 3;
optional Mysqlx.Crud.Update update = 4;
optional Mysqlx.Crud.Delete delete = 5;
optional Mysqlx.Sql.StmtExecute stmt_execute = 6;
}
required OneOfMessage stmt = 2;
option (client_message_id) = PREPARE_PREPARE; // comment_out_if PROTOBUF_LITE
}
stmt_id
- ID which is going to represent the result of preparation, its value is chosen by client and its his responsibility to guarantee its uniqueness in current session. Using same value on already prepared statement deallocates old one.stmt
- Statement which should be prepared, currently client can prepare SQL, Crud.Insert, Crud.Update, Crud.Find, Crud.Delete.stmt.type
- Value defines which filed from OneOfMessage message (stmt field) is valid. Possible values are define byPrepare.OneOfMessageType
.
Flow with this message looks following:
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, stmt=...)
alt Success
client<-server: Mysqlx.Ok
else
client<-server: Mysqlx.Error(
Error codes generated by the server|
Error codes generated by X Plugin query generator)
end
When preparing SQL, the rules are the same as in classic protocol, question
mark is a placeholder, which is going to be replaced by argument supplied in
args
of Mysqlx.Prepare.Execute
message. Example of placeholders:
SELECT SLEEP(?);
X Plugin generates SQL from CRUD messages, this means that user can't use
question marks like with SQL execution (StmtExecute). X Protocol already
supports placeholder inside CRUD. To use those placeholders, user must specify
and expression of type Mysqlx.Expr.Expr.PLACEHOLDER
, with index into
the args
field of:
- Mysqlx.Crud.Find, args
- Mysqlx.Crud.Insert, args
- Mysqlx.Crud.Update, args
- Mysqlx.Crud.Delete, args
When preparing one of those messages, placeholders with indexes that reach
outside the args
field, must be translated question mark (SQL placeholder).
Those not resolved placeholder indexes must be translated in a way, that they
point to array elements supplied by user in args
field of
Mysqlx.Prepare.Execute
message.
For example, lets assume that user prepared following:
Mysqlx.Prepare.Prepare {
stmt_id:1
stmt {
type: FIND
find {
collection {name:"coll"}
projection { type: PLACEHOLDER postion: 3 }
projection { type: PLACEHOLDER postion: 1 }
projection { type: PLACEHOLDER postion: 1 }
projection { type: PLACEHOLDER postion: 2 }
projection { type: PLACEHOLDER postion: 0 }
projection { type: PLACEHOLDER postion: 3 }
args {string:"A"}
args {string:"B"}
}
}
}
Which is going to generate following SQL:
SELECT ?, "B", "B", ?, "A", ? FROM coll;
X Plugin must remember the indexes that user specified for those question marks:
SELECT ?, /* placeholder_index:3 */,
"B",
"B",
?, /* placeholder_index:2 */,
"A",
?, /* placeholder_index:3 */,
FROM coll;
Missing data will be filled using binded data from field args
in
Mysqlx.Prepare.Execute
message. X Plugin will assume that data from
Execute/args are appedned to the end of Find/args. This can be interpreted
other way, by referencing Execute/args directly. To do so each index must
be substracted by number of elements in Find.args
field:
SELECT ?, /* placeholder_index:1 */,
"B",
"B",
?, /* placeholder_index:0 */,
"A",
?, /* placeholder_index:1 */,
FROM coll;
When user will execute the prepared Find (SQL above), using following message:
message Execute {
stmt_id: 1;
args { string:"X"}
args { string:"Y"}
}
X Plugin must build following array of binded arguments from the prepared statement:
["Y", "X", "Y"]
SQL allows to use placeholders inside the LIMIT statement, still X Protocol CRUD messages use following definition which doesn't allow to do that:
message Limit {
required uint64 row_count = 1;
optional uint64 offset = 2;
}
To workaround the problem, another message must be added as limit_expr
field
to following message: Mysqlx.Crud.Find, Mysqlx.Crud.Delete, Mysqlx.Crud.Update:
message LimitExpr {
required Mysqlx.Expr.Expr row_count = 1;
optional Mysqlx.Expr.Expr offset = 2;
}
The use of limit
and limit_expr
has following constraints:
only of those fields can be set, in case when X Plugin received two field set it must return ER_X_BAD_MESSAGE.
the
Expr
type, can describe a lot of different values. In case oflimit_expr
, X Plugin must only allow a Scalar/UINT value and Expr/PLACEHOLDER.
Mysqlx.Prepare.Execute
Following message is responsible for execution of already prepared SQL or CRUD statement:
message Execute {
required uint32 stmt_id = 1;
repeated Mysqlx.Datatypes.Any args = 2;
optional bool compact_metadata = 3 [ default = false ];
option (client_message_id) = PREPARE_EXECUTE; // comment_out_if PROTOBUF_LITE
}
stmt_id
- ID representing statement prepared withMysqlx.Prepare.Prepare
.args
- Arguments to insert in place of placeholders defined at preparation. X Plugin must support only scalar arguments. Not all arguments must be used when executing, still all dependencies from Mysqlx.Expr.Expr.PLACEHOLDER must be satisfied.compact_metadata
- Defines if that only type information should be send inMysqlx.Resultset::ColumnMetadata
, skipping names.
Flow with this message looks following:
client->server: Mysqlx.Prepare.Execute(stmt_id=1, args=...)
alt Success
client<-server: ...Resultset...
client<-server: Mysqlx.Sql.StmtExecuteOk
else
alt
client<-server: ...Part of Resultset...
end
client<-server: Mysqlx.Error(
ER_X_BAD_STATEMENT_ID|
ER_X_PREPARED_EXECUTE_ARGUMENT_NOT_SUPPORTED|
ER_X_PREPARED_EXECUTE_ARGUMENT_CONSISTENCY|
Error codes generated by the server)
end
Error cases:
- return ER_X_BAD_STATEMENT_ID when
stmt_id
is not assigned to prepared-statement in context of current session. - return ER_X_PREPARED_EXECUTE_ARGUMENT_NOT_SUPPORTED when value in
args
field is not supported by X Plugin, only Scalar types must be supported. - return ER_X_PREPARED_EXECUTE_ARGUMENT_CONSISTENCY when placeholder index
points outside
args
field boundaries.
In case when user executes a prepared Mysqlx.Curd.Insert
, then
all generated document ID must be returned to the client in notices.
Executing a prepared-statement that is in use by a cursor, must close the cursor:
client->server: Cursor.Open(cursor_id=1, stmt_id=10)
client<-server: Ok
client->server: Prepared.Execute(stmt_id=10)
.. resultsets ..
client<-server: StmtExecuteOk
client->server: Cursor.Fetch(cursor_id=1)
client<-server: Error (ER_X_BAD_CURSOR_ID)
Mysqlx.Prepare.Deallocate
Following message is responsible for deleting prepared statement and it
marks the stmt_id
as free:
message Deallocate {
required uint32 stmt_id = 1;
option (client_message_id) = PREPARE_DEALLOCATE; // comment_out_if PROTOBUF_LITE
}
stmt_id
- ID representing statement prepared withMysqlx.Prepare.Prepare
.
Flow with this message look following:
client->server: Mysqlx.Prepare.Deallocate(stmt_id=1)
alt Success
client<-server: Mysqlx.Ok
else
client<-server: Mysqlx.Error(
ER_X_BAD_STATEMENT_ID|
Error codes generated by the server)
end
Error cases:
- return ER_X_BAD_STATEMENT_ID when
stmt_id
is not assigned to prepared-statement in context of current session.
Deallocating a prepared-statement that is in use by a cursor, must close then cursor:
client->server: Cursor.Open(cursor_id=1, stmt_id=10)
client<-server: Ok
client->server: Prepared.Deallocate(stmt_id=10)
client<-server: Ok
client->server: Cursor.Fetch(cursor_id=1)
client<-server: Error (ER_X_BAD_CURSOR_ID)
Cursors
Cursors were added to X Protocol, to give user possibility to fetch resultsets in small chunks using iterator like behavior.
Client application can manage cursors using:
- create/open cursor by sending
Mysqlx.Cursor.Open
- fetch data from cursor by sending
Mysqlx.Cursor.Fetch
- close cursor by sending
Mysqlx.Cursor.Close
Cursors divide the flow of resultsets on multiple parts, each part is
transfered after execution of open
, fetch
. Information that
the resultsets were broken on parts can be determined by looking at
the last message of the result, which can be either FetchSuspended
or FetchDone
. The first fetched resultset (when opening) must
contain meta-data.
Not all statements may be fetched using cursors, an example of this case
is a call to "dynamic SQL" (stored procedure call). open
instead of
returning meta-data, it is going to return full resultset/resultsets.
Further attempts to fetch such cursor are going to end with error pointing
that there are no more data inside the cursor.
Mysqlx.Cursor.Open
Following message is responsible for opening a cursor for a given statement.
The cursor executes the statement and instead of streaming the result to client
it suspends it until client asks for more data.
Currently cursor can execute only prepared statements, still the message is
open for future changes.
Client must provide an ID (cursor_id
) which the X Plugin is going to use
as cursor identification:
message Open {
required uint32 cursor_id = 1;
message OneOfMessage {
enum Type {
PREPARE_EXECUTE = 0;
}
required Type type = 1 /* OneOfMessageType */;
optional Mysqlx.Prepare.Execute prepare_execute = 2;
}
required OneOfMessage stmt = 4;
optional uint64 fetch_rows = 5;
option (client_message_id) = CURSOR_OPEN; // comment_out_if PROTOBUF_LITE
}
cursor_id
- ID which is going to represent the newly created cursor, its value is chosen by client and its his responsibility to guarantee its uniqueness in current session. If client specified already assigned ID then old cursor is going to be closed.stmt
- Statement which is going to be executed and its resultset is going to be streamed to client application using cursor. In case whenstmt_id
is already opened by other cursor, it is going to be closed and reopened in new cursor.fetch_rows
- defines how many rows of the resultset should be send to the client. For dynamic SQL this argument is going to be ignored and whole resultset/resultsets are going to be returned. In case when client didn't set it, its equivalent to setting it to zero (fetch nothing).
Flow with this message looks following:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
alt
client<-server: Resultsets
end
alt Success
client<-server: StmtExecuteOk
else
client<-server: Mysqlx.Error(
ER_X_BAD_STATEMENT_ID|
ER_X_PREPARED_EXECUTE_ARGUMENT_NOT_SUPPORTED|
ER_X_PREPARED_EXECUTE_ARGUMENT_CONSISTENCY|
Error codes generated by the server)
end
Error cases:
- return ER_X_BAD_STATEMENT_ID when stmt_id does not refer to an existing prepared statement.
- return ER_X_PREPARED_EXECUTE_ARGUMENT_NOT_SUPPORTED when value in
Cursor.stmt.prepare_execute.args
field is not supported by X Plugin, only Scalar types must be supported.
The flow for opening a cursor is quite generic, there are several different scenarios that can occur:
Case 1: Cursor was created and user didn't request any data to be fetched
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData...
client<-server: Mysqlx.Resultset.FetchSuspended
client<-server: Mysqlx.Sql.StmtExecuteOk
User can interpret FetchSuspended
messages as information that cursor is
open and valid.
Case 2: Cursor was created and user requested more data than the resultset has:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1}, fetch_row=10)
client<-server: Mysqlx.Resultset.ColumnMetaData...
client<-server: Mysqlx.Resultset.Row
client<-server: Mysqlx.Resultset.Row
client<-server: Mysqlx.Resultset.FetchDone
client<-server: Mysqlx.Sql.StmtExecuteOk
User can interpret FetchDone
messages as information that cursor is
open still there are no more data in it.
Case 3: Cursor was created and user requested less data than the resultset has:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1}, fetch_row=1)
client<-server: Mysqlx.Resultset.ColumnMetaData...
client<-server: Mysqlx.Resultset.Row
client<-server: Mysqlx.Resultset.FetchSuspended
client<-server: Mysqlx.Sql.StmtExecuteOk
Case 4: Cursor was created, still its doesn't contains any data. Stmt was executed right away and doesn't have any resultset:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.FetchDone
client<-server: Mysqlx.Sql.StmtExecuteOk
Case 5: Cursor was created, still fetching is not possible because stmt executed right away (probably because its was dynamic SQL):
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData...
client<-server: Mysqlx.Resultset.Row
client<-server: Mysqlx.Resultset.Row
client<-server: Mysqlx.Resultset.FetchDone
client<-server: Mysqlx.Sql.StmtExecuteOk
There are even more combinations possible, still all are in align with resultset flow.
Mysqlx.Cursor.Fetch
Following message is responsible for fetching a part or resultset
from
a cursor:
message Fetch {
required uint32 cursor_id = 1;
optional uint64 fetch_rows = 2;
option (client_message_id) = CURSOR_FETCH; // comment_out_if PROTOBUF_LITE
}
cursor_id
- ID which represents opened cursorfetch_rows
- number of rows which should be fetched from the resultset, in case when it was not set then rest of resultsets is going to be returned.
Flow with this message looks following:
client -> server: Fetch
alt Success
... none or partial Resultsets or full Resultsets ...
client <- server: StmtExecuteOk
else
client <- server: Error(ER_X_BAD_CURSOR_ID|..Error generated by server..)
end
User can check if only a part of resultset/resultsets was transfered by looking
at message before StmtExecuteOk. In case when there is no data to be transfered
in the cursor then the message is going to be Mysqlx.Resultset.FetchDone
or Mysqlx.Resultset.FetchDoneMoreResultsets
(old behavior), in other case
the message is going to be Mysqlx.Resultset.FetchSuspended
.
Flow for transferring resultsets, looks following:
loop has more resultsets or not at end of fetch
group resultset
loop has more columns
server --> client: ColumnMetaData
end
loop has more rows
server --> client: Row
end
end
alt has more resultsets
server --> client: FetchDoneMoreResultsets
end
end
loop has more OUT-paramsets or not at end of fetch
server --> client: FetchDoneMoreOutParams
group resultset
loop has more columns
server --> client: ColumnMetaData
end
loop has more rows
server --> client: Row
end
end
end
alt at end of all resultsets
server --> client: FetchDone
else cursor is opened
server --> client: FetchSuspended
end
After fetching whole resultset (reception of FetchDone), a cursor should be marked
that it has not data. Thus next Mysqlx.Cursor.Fetch
must fail with an error.
Error cases:
- return ER_X_BAD_CURSOR_ID when client used cursor-id that was not opened.
- return ER_X_CURSOR_REACHED_EOF when all data were already fetched from cursor represented by cursor-id (server send FetchDone to mark the EOF).
Mysqlx.Cursor.Close
Following message is responsible for closing the cursor, which deallocates all data associated with the cursor:
message Close {
required uint32 cursor_id = 1;
option (client_message_id) = CURSOR_CLOSE; // comment_out_if PROTOBUF_LITE
}
cursor_id
- ID which represents opened cursor
Flow with this message look following:
client -> server: Close
alt Success
client <- server: Ok
else
client <- server: Error(ER_X_BAD_CURSOR_ID|..Error generated by server..)
end
Error cases:
- return ER_X_BAD_CURSOR_ID when client used cursor-id that was not opened.
Stored procedure output parameters
X Protocol already defines a way of transferring stored procedures OUT and INTOUT parameters in following description:
loop has more resultsets
group resultset
loop has more columns
server --> client: ColumnMetaData
end
loop has more rows
server --> client: Row
end
end
alt has more resultsets
server --> client: FetchDoneMoreResultsets
end
end
loop has more OUT-paramsets
server --> client: FetchDoneMoreOutParams
group resultset
loop has more columns
server --> client: ColumnMetaData
end
loop has more rows
server --> client: Row
end
end
end
alt at end of all resultsets
server --> client: FetchDone
else cursor is opened
server --> client: FetchSuspended
end
Where the output parameters are transfered as a last resultset with single row
defining the values. The 'out-param' resultset begins after reception of
FetchDoneMoreOutParams
followed by ColumnMetaData
which defines types for
the row.
Assuming that user defined following procedure:
CREATE PROCEDURE my_inc(INOUT inout_number INT)
BEGIN
SET inout_number=inout_number + 1;
END$$
After preparing following statement:
CALL my_inc(?)
and executing it with argument set to "1", it will generate following resultset:
client->server: Mysqlx.Prepare.Execute(stmt_id=1, args=1)
server->client: Mysqlx.Resultset.FetchDoneMoreOutParams
server->client: Mysqlx.Resultset.ColumnMetaData(type=UINT)
server->client: Mysqlx.Resultset.Row(field:2)
server->client: Mysqlx.Resultset.FetchDone
Same behavior must be implemented in aspect of:
- Mysqlx.Prepare.Execute
- Mysqlx.Sql.StmtExecute
- Mysqlx.Cursor.Fetch
Pipelining
Current protocol design doesn't allow to open a cursor or execute a prepared
statement from Mysqlx.Prepare.Prepare
message. Still user can achieve the
same effect by pipelining two message, for example:
Mysqlx.Prepare.Prepare
withMysqlx.Prepare.Execute
Mysqlx.Prepare.Prepare
withMysqlx.Cursor.Open
This keeps the new protocol messages clean, by applying the "single responsibility" principle and paying small price of around additional 7 bytes (X header + X Message type + stmt_id) of network transfer.
Additionally user pipeline more X Protocol messages to block execution of
Prepare.Execute
message when "prepare fails", for example:
client->server: Mysqlx.Expect.Open(no_error=true)
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, ...)
client->server: Mysqlx.Prepare.Execute(stmt_id=1, ...)
client->server: Mysqlx.Expect.Close()
alt successful scenraio
server->client: Mysqlx.Ok
server->client: ... ResultSet ...
server->client: StmtExecuteOk
server->client: Mysqlx.Ok
server->client: Mysqlx.Ok
else error scenraio
server->client: Mysqlx.Ok
server->client: Mysqlx.Error(*prepare failed*)
server->client: Mysqlx.Error(no-executed)
server->client: Mysqlx.Error(no-executed)
end
Reseting
Cursors and prepared-statements are part of the session, in case when client is
going to reset the session (by using Mysqlx.Session.Reset
) those objects
must be released.
Same applies to release of the session in case of connection closes, session is responsible for releasing cursors and prepared-statements.
Instrumentation
Status variables
- mysqlx_prep_prepare - number of prepared-statement messages received
Property | Value |
---|---|
Variable Name | mysqlx_prep_prepare |
Type | LONGLONG |
Scope | GLOBAL/SESSION |
Default | 0 |
- mysqlx_prep_execute - number of prepared-statement-execute messages received
Property | Value |
---|---|
Variable Name | mysqlx_prep_execute |
Type | LONGLONG |
Scope | GLOBAL/SESSION |
Default | 0 |
* mysqlx_prep_deallocate - number of prepared-statement-deallocate messages received
Property | Value |
---|---|
Variable Name | mysqlx_prep_deallocate |
Type | LONGLONG |
Scope | GLOBAL/SESSION |
Default | 0 |
* mysqlx_cursor_open - number of cursor-open messages received
Property | Value |
---|---|
Variable Name | mysqlx_cursor_open |
Type | LONGLONG |
Scope | GLOBAL/SESSION |
Default | 0 |
* mysqlx_cursor_close - number of cursor-close messages received
Property | Value |
---|---|
Variable Name | mysqlx_cursor_close |
Type | LONGLONG |
Scope | GLOBAL/SESSION |
Default | 0 |
* mysqlx_cursor_fetch - number of cursor-fetch messages received
Property | Value |
---|---|
Variable Name | mysqlx_cursor_fetch |
Type | LONGLONG |
Scope | GLOBAL/SESSION |
Default | 0 |
Performance schema
Prepared statement handling must be observable through already existing infrastructure: https://dev.mysql.com/doc/refman/8.0/en/prepared-statements-instances-table.html
This basically means that all prepared statements must have an entry in "preapared_statements_instances" table and must be updated on every successful "Prepare", "PrepareExecute", "PrepareDeallocate".
X Plugin session holds an association between prepared statement client side id
(stmt_id
) and server side id. The problem is that performance schema
presents server ids. To be able to localize stmt_id
in PS, user can
create a query that uses mysql_get_prepared_statment_id
UDF for translating
the identifiers:
SELECT * FROM performance_schema.prepared_statments_instances
WHERE stmt_id=mysql_get_prepared_statment_id(1 /* client side id */);
Error and Warnings
Property | Value |
---|---|
Name | ER_X_BAD_STATEMENT_ID |
Error code | 5110 |
Text | Statement with ID=%i was not prepared. |
Example | Statement with ID=12 was not prepared. |
Property | Value |
---|---|
Name | ER_X_BAD_CURSOR_ID |
Error code | 5111 |
Text | Cursor with ID=%i was not opened. |
Example | Cursor with ID=22 was not opened. |
Property | Value |
---|---|
Name | ER_X_PREPARED_EXECUTE_ARGUMENT_NOT_SUPPORTED |
Error code | TODO |
Text | Argument at index '%i' and of type '%s' is not supported |
for binding to prepared statement |
Property | Value |
---|---|
Name | ER_X_PREPARED_EXECUTE_ARGUMENT_CONSISTENCY |
Error code | TODO |
Text | There is no argument for statement placeholder at position: %i |
Property | Value |
---|---|
Name | ER_X_CURSOR_REACHED_EOF |
Error code | TODO |
Text | No more data in cursor (cursor id:'%i') |
User defined function
X Protocol introduces its own IDs for prepared-statements, to make it work with current performance-schema infrastructure, client must be able to map the ID used by X Plugin with ID shown in performance schema, using following UDF:
- mysqlx_get_prepared_statment_id(client_side_prepared_stmt_id) - returns server side prep-stmt-id, allocated by current session or NULL when conversion is not possible.
To make server side ID generation work correctly with prepared statements the plugin must provide following function to embed it into generated SQL queries:
- mysqlx_generate_document_id([offset INT, [increment INT,
[generate_key BOOLEAN]]]) -
generate unique document
ID
that can be used when user did provide a document without anID
. In case when offset or increment were not provided then function is going to use "1" as default value. Last argument can be used to point if the function should generate the id or not, it is useful in case when on SQL level the decision is going to be made about inserting theID
.
Mysqlx.Crud.Insert
When user put a placeholder in place of the whole document, X Plugin can't determine if document contains ID. This means X Plugin doesn't know if the ID should be inserted or not.
The decision about inserting the ID was moved to SQL level which requires that all those operations must translate the X Protocol message:
client->server: Mysqlx.Prepare.Prepare(stmt_id:1 insert{projection{placeholder:0}})
in following way:
INSERT INTO coll(doc) VALUES((SELECT JSON_INSERT(TMP1.doc, '$._id', CONVERT(
mysqlx_generate_document_id(@@auto_increment_offset,
@@auto_increment_increment, JSON_CONTAINS_PATH(TMP1.doc, 'one',
'$._id')) USING utf8mb4 FROM (SELECT ? as doc) as TMP1)))
optionally it can expand to:
INSERT INTO coll(doc) VALUES(JSON_INSERT(?, '$._id', CONVERT(
mysqlx_generate_document_id(@@auto_increment_offset, @@auto_increment_increment,
JSON_CONTAINS_PATH(?, 'one', '$._id')) USING utf8mb4)))
where X Plugin must pass same bound argument multiple times to server.
Examples
Execute prepared statement
Statement ID can be allocated to one stmt, old one is deallocated
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, stmt=%VALID%)
client<-server: Mysqlx.Ok
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, stmt=%VALID%)
client<-server: Mysqlx.Ok
Statement ID can be reused after deleting old prep-stmt
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, stmt=%VALID%)
client<-server: Mysqlx.Ok
client->server: Mysqlx.Prepare.Deallocate(stmt_id=1)
client<-server: Mysqlx.Ok
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, stmt=%VALID%)
client<-server: Mysqlx.Ok
Preparing and executing simple SQL
client->server: Mysqlx.Prepare.Prepare(stmt_id=1, stmt=StmtExecute(stmt="SELECT NOW();"))
client<-server: Mysqlx.Ok
client->server: Mysqlx.Prepare.Execute(stmt_id=1)
client<-server: ...Resultsets...
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Prepare.Deallocate(stmt_id=1)
client<-server: Mysqlx.Ok
Opening same ID twice, implicitly closes old one
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=2})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
Cursor ID can be used after closing old one
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Cursor.Close(cursor_id=1)
client<-server: Mysqlx.Ok
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=2})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
Fetch '0' rows from cursor
Assuming that resultset
has two columns:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Cursor.Fetch(cursor_id=1, fetch_rows=0)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
Fetch all rows from cursor
Assuming that resultset
has two columns:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Cursor.Fetch(cursor_id=1)
client<-server: ...Resultset rows...
client<-server: Mysqlx.Resultset.FetchDone()
client<-server: Mysqlx.Sql.StmtExecuteOk
Fetch same number of rows as there are in resultset
Assuming that resultset
has two columns and two rows:
client->server: Mysqlx.Cursor.Open(cursor_id=1, stmt={stmt_id=1})
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.ColumnMetaData(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Cursor.Fetch(cursor_id=1, fetch_rows=2)
client<-server: Mysqlx.Resultset.Row(...)
client<-server: Mysqlx.Resultset.Row(...)
client<-server: Mysqlx.Resultset.FetchSuspended()
client<-server: Mysqlx.Sql.StmtExecuteOk
client->server: Mysqlx.Cursor.Fetch(cursor_id=1, fetch_rows=1)
client<-server: Mysqlx.Resultset.FetchDone()
client<-server: Mysqlx.Sql.StmtExecuteOk
Srv_service requirements
To enable out-params on Srv_service implementation needs to set following flag: CLIENT_PS_MULTI_RESULTS.
Opening a cursor for every query is not possible. In case when opening a cursor is not possible then the server streams the whole resultset to the user. X Protocols st_command_service_cbs must fail the streaming in such case.
On the server side relation between cursor and prepared statement is one-to-one, still X Protocol allows multiple-to-one. The limitation must be handled on X Plugin side when the client tries to open a second cursor for the prepared statement.
st_command_service_cbs
gives the hint about position inside the cursor using following flag: SERVER_STATUS_CURSOR_EXISTS, returned throughcall_handle_ok
. It must be used to determine if cursor is at EOR (last record was fetched) or not.Server converts the Mysqlx.Prepare.Execute arguments to types expected by placeholders. No additional work needs to be done.
UDF requirements
- UDF API doesn't pass to plugin/component current THD, thus X Plugin to
identify
xpl::Session
must fetch THD using "current_thd" macro.