sidorares / sidorares/node-mysql2
MariaDB: pipeline COM_STMT_PREPARE and COM_STMT_EXECUTE with statement id -1
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 4.4k
- Forks
- 680
- Avg merge
- 9h 7m
- Merged PRs (30d)
- 59
Description
Summary
The first execute() of a new SQL text on a connection costs two round trips: COM_STMT_PREPARE, wait for the statement id, then COM_STMT_EXECUTE. Later executions hit the statement cache and cost one. MariaDB's protocol allows sending both in one flight: per the MariaDB COM_STMT_EXECUTE documentation, the statement id value -1 (0xFFFFFFFF) "can be used to indicate to use the last statement prepared on current connection if no COM_STMT_PREPARE has failed since". MariaDB Connector/J uses this. MySQL has no equivalent.
Priority: low. One round trip saved per distinct statement per connection, for MariaDB users only, against a non-trivial change to the command pipeline. Filed so the design notes and the probe are not lost.
Verified locally (Docker)
A probe built on the driver's own Prepare/Execute state machines sent both packets back to back and read both responses:
| Server | Result |
|---|---|
| MariaDB 12.3.2 | SELECT ? + ? AS sum, ? AS label prepared and executed in one flight returns { sum: 42, label: 'pipelined' } |
| MariaDB, after a failed prepare | COM_STMT_EXECUTE with -1 is rejected (Unknown prepared statement handler (4294967295)), so it can never run an older statement by mistake |
| MySQL 8.3.0 and 9.7.2 | rejected outright: Unknown prepared statement handler (4294967295) given to mysql_stmt_precheck |
What it saves
Exactly one round trip on the first execution of each distinct statement per connection. Nothing changes for cached statements, for query(), or for CPU. It matters for many distinct prepared statements per connection, short-lived connections, or high-latency links.
Why it is more than a small change
- Parameter type hints. The execute packet has to be built before the prepare response exists, so the parameter definitions the driver now uses (
integerHint, adopting the integer types the server reports) are unavailable for the pipelined first execution. Its parameters would be encoded by JavaScript type only, differently from every later execution of the same statement. Needs a rule, for example pipelining only when no parameter could benefit from a hint, or accepting the first-execution difference explicitly. - Command queue and sequence ids. The queue runs one command at a time and the sequence-id bookkeeping assumes one request per response stream. A combined command must send two requests (each starting at sequence 0), then drive the prepare state machine and the execute state machine in turn, resetting the expected sequence id at the boundary (both responses start at 1). The probe did this in about 80 lines by reusing
Prepare.prototype.*andExecute.prototype.*state functions. - Errors. A failed prepare yields two error packets (prepare's and the execute's
Unknown prepared statement handler); both must be consumed and only the first reported. A prepare that succeeds followed by an execute error must still cache the statement. - Gating. Only when
connection._isMariaDB(already set from the handshake version string) and the statement is not cached; everything else keeps the current path.
Tests to add
- MariaDB: first
execute()of a new statement issues both packets before any response (fake server or packet capture), results identical to the sequential path. - Failed prepare: single error surfaced, connection remains usable, sequence ids intact.
- Parameter encoding of the first execution documented and asserted.
- MySQL: path never taken.
Probe
'use strict';
// Sends COM_STMT_PREPARE and COM_STMT_EXECUTE (id -1) back to back through
// the driver's own command state machines. Run from the repository root:
// MYSQL_PORT=3310 node probe.js
const mysql = require('./index.js');
const Command = require('./lib/commands/command.js');
const Prepare = require('./lib/commands/prepare.js');
const Execute = require('./lib/commands/execute.js');
const Packets = require('./lib/packets/index.js');
const ConnectionConfig = require('./lib/connection_config.js');
class PipelinedPrepareExecute extends Command {
constructor(sql, values, callback) {
super();
this.sql = sql;
this.prepare = new Prepare({ sql }, (err, statement) => {
this.prepareError = err;
this.statement = statement;
});
this.exec = new Execute({ sql, values }, (err, rows) =>
callback(this.prepareError, err, rows)
);
}
start(_packet, connection) {
const clientFlags =
connection.config.clientFlags & (connection.serverCapabilityFlags || 0);
connection.writePacket(
new Packets.PrepareStatement(this.sql, connection.config.charsetNumber).toPacket()
);
connection._resetSequenceId();
this.exec._connection = connection;
this.exec.options = ConnectionConfig.queryOptions(connection.config, { sql: this.sql });
connection.writePacket(
new Packets.Execute(0xffffffff, this.exec.parameters, connection.config.charsetNumber,
connection.config.timezone, undefined, clientFlags, connection._isMariaDB, []).toPacket()
);
connection.sequenceId = 1; // both responses start at sequence 1
this.step = Prepare.prototype.prepareHeader;
this.prepare.key = 'probe';
return this.prepareState;
}
prepareState(packet, connection) {
if (packet.isError()) {
this.prepareError = packet.asError(connection.clientEncoding);
return this.switchToExecute(connection);
}
const next = this.step.call(this.prepare, packet, connection);
if (next === null) {
return this.switchToExecute(connection);
}
this.step = next;
return this.prepareState;
}
switchToExecute(connection) {
connection.sequenceId = 1;
this.exec.statement = this.statement;
this.step = Execute.prototype.resultsetHeader;
return this.executeState;
}
executeState(packet, connection) {
if (packet.isError()) {
this.exec.onResult(packet.asError(connection.clientEncoding));
return null;
}
const next = this.step.call(this.exec, packet, connection);
if (next === null) {
return null;
}
this.step = next;
return this.executeState;
}
}
const conn = mysql.createConnection({
host: '127.0.0.1', port: Number(process.env.MYSQL_PORT || 3306), user: 'root', database: 'test',
});
conn.addCommand(
new PipelinedPrepareExecute('SELECT ? + ? AS sum, ? AS label', [40, 2, 'pipelined'],
(prepareError, execError, rows) => {
console.log({ prepareError: prepareError?.code, execError: execError?.sqlMessage, rows });
conn.end();
})
);
The driver's "packets out of order" warnings during the probe are expected: it is not aware of the second request.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with lib/commands/prepare.js, lib/commands/execute.js, lib/commands/command.js, and lib/connection_config.js, then run the provided probe from the repository root to understand the current state machines and packet sequencing. Done means a MariaDB-only first execute pipelines both packets, preserves error and sequence handling, leaves MySQL and cached statements unchanged, and adds the listed tests for failures and parameter encoding.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, mariadb, nodejs
- Domain
- backend, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100