跳到主要内容

Typed Parameters

connection.execute() sends each bind parameter with a MySQL type. By default that type is inferred from the JavaScript value, which cannot express integer width, signedness, or whether a value is binary or text. TypedParameter lets you state the type yourself.

const mysql = require('mysql2');
const { TypedParameter: T } = mysql;

const connection = await mysql.createConnection(opts).promise();

await connection.execute('SELECT * FROM users LIMIT ?', [T.BIGINT(10)]);
await connection.execute('INSERT INTO ledger (amount) VALUES (?)', [
T.BIGINT('9007199254740993'),
]);

Why it matters

Numbers larger than Number.MAX_SAFE_INTEGER

Every JavaScript number is sent as DOUBLE. A cached prepared statement keeps the type of its first execution, so once a position has been bound to a number, an exact BIGINT string bound later is read back through that DOUBLE and rounded.

await connection.execute('INSERT INTO t (big) VALUES (?)', [1]);
await connection.execute('INSERT INTO t (big) VALUES (?)', [
'9007199254740993',
]);
// stored: 9007199254740992

await connection.execute('INSERT INTO t (big) VALUES (?)', [
T.BIGINT('9007199254740993'),
]);
// stored: 9007199254740993

Unsigned values

Without a type the driver cannot set the unsigned flag, so the top half of BIGINT UNSIGNED is unreachable.

await connection.execute('INSERT INTO t (big) VALUES (?)', [
T.BIGINT.unsigned('18446744073709551615'),
]);

Index lookups on string columns

Comparing an indexed string column to a number makes MySQL compare the column numerically, which cannot use the index — and the cached statement keeps that shape for every later execution. Pinning the position to one type keeps the lookup on the index whatever the caller passes.

await connection.execute('SELECT id FROM t WHERE code = ?', [
T.VARCHAR(userInput),
]);

Binary data

A string parameter is converted from the connection charset. Sending binary as a BLOB keeps the bytes intact, which matters for VARBINARY and MariaDB VECTOR columns.

await connection.execute('INSERT INTO t (raw) VALUES (?)', [T.BLOB(buffer)]);

Available types

Each factory takes the value and returns a parameter you pass in place of it.

GroupFactories
IntegerTINYINT SMALLINT MEDIUMINT INT INTEGER BIGINT YEAR, and the protocol names TINY SHORT INT24 LONG LONGLONG
DecimalFLOAT DOUBLE REAL DECIMAL NEWDECIMAL
TemporalDATE DATETIME TIMESTAMP TIME
TextVARCHAR CHAR STRING VAR_STRING TEXT MEDIUMTEXT LONGTEXT ENUM SET
BinaryBLOB TINY_BLOB MEDIUM_BLOB LONG_BLOB BINARY VARBINARY VECTOR
OtherJSON NULL

Integer factories also expose .unsigned:

T.INT.unsigned(4294967295);

BIT and GEOMETRY have no factory because neither server accepts them as a bind type. Send a bit mask as T.BIGINT(mask) and geometry as T.BLOB(wkb).

Types the server will not bind

Some declared types are not valid COM_STMT_EXECUTE bind types, so they travel as the nearest type both servers accept. The value is unchanged.

DeclaredSent as
MEDIUMINTLONG
YEARSHORT
ENUM SETSTRING
VECTORBLOB
JSONVAR_STRING on MariaDB, which has no JSON bind type

Values are checked before they are sent

Integer factories reject anything they cannot represent, at the call site rather than inside the driver.

T.TINYINT(300);
// RangeError: TINY parameter out of range: 300 is not within -128..127

T.BIGINT(9007199254740993);
// RangeError: LONGLONG parameter 9007199254740992 exceeds Number.MAX_SAFE_INTEGER
// and has already lost precision; pass a string or BigInt instead

T.INT('7abc');
// TypeError: LONG parameter must be an integer, got "7abc"

Pass large integers as a string or BigInt, both of which stay exact.

Typed NULL

T.BIGINT(null) sends SQL NULL while keeping the declared type, so a position does not change type between a null and a non-null execution. T.NULL() sends an untyped NULL, matching a plain null.

Integer types chosen for you

For positions where the server reports an integer type, the driver adopts it automatically — no TypedParameter needed. This is what makes the most common failure work:

await connection.execute('SELECT * FROM t LIMIT ?', [10]);

MySQL requires an integer for LIMIT and rejects a DOUBLE with ER_WRONG_ARGUMENTS. The server reports the placeholder as LONGLONG, so the driver sends LONGLONG.

The type is adopted only when the reported type is an integer type and the value is already an integer that fits it. In every other case the type is inferred from the JavaScript value exactly as before, which keeps SELECT ? round-tripping a number as a number and leaves the server's own string coercions in place.

Not every server resolves parameter types, and on those nothing is adopted:

ServerReports parameter types
MySQL 8.0 and upYes, including the unsigned flag
MySQL 5.7No — VAR_STRING with zero length for every position
MariaDBNo — MYSQL_TYPE_NULL for every position

Neither of those needs the adoption: both re-prepare a statement when a later execution changes a parameter type, which is what MySQL 8.0.22 stopped doing. Where nothing is reported, TypedParameter is the only way to state a type.