mysql onmysql2 driver, using itsmysqlmysql2 (mysql2/promise)`table`.`column`?mysql.createPool), sized from pool.max (default 10)INSERT ... ON DUPLICATE KEY UPDATEAUTO_INCREMENT columns + LAST_INSERT_ID()MATCH(...) AGAINST(...) over FULLTEXT indexes| Property | Value |
|---|---|
| Dialect name | mysql |
| Extends | nothing — implements Dialect directly |
| Driver | mysql2 |
| Cloud SDK | none |
| Default port | 3306 |
| Local runtime | prorm db up --dialect mysql (mysql:8.0.39); docker-compose.test.yml |
createSchema, dropSchema, showAllSchemas) intentionally throw and point you atCREATE DATABASE / DROP DATABASE / SHOW DATABASES equivalents;listSchemas() runs SHOW DATABASES.$ npm install ts-prorm-orm mysql2$ pnpm add ts-prorm-orm mysql2$ yarn add ts-prorm-orm mysql2$ bun add ts-prorm-orm mysql2import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'mysql',
host: 'localhost',
database: 'app',
username: 'app',
password: process.env.DB_PASSWORD,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'mysql',
url: 'mysql://app:secret@localhost:3306/app',
});host, port, database, username and password before the dialect is built; any of those passed alongside it wins.import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
name: { type: DataTypes.STRING(100) },
});
await db.sync(); // CREATE TABLE — see Generated SQL belowimport { Op } from 'ts-prorm-orm';
await User.create({ email: 'ada@example.com', name: 'Ada' });
const page = await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});query(), captured with the driver stubbed out. The model layer inlines the values.db.sync() for the model in the Quick start:CREATE TABLEawait User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });upsert() also reads the row by primary key before and after the write, to return the instance.await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});[!NOTE] No JSON operator in the WHERE builder DataTypes.JSON()maps toJSON, but this dialect's WHERE builder refusesOp.containson it: Cannot render WHERE condition at meta.contains' is not implemented by its WHERE builder. The statement was not sent, because running it without the condition would match rows it should not.
await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
// A nested transaction is a SAVEPOINT; its failure undoes only its own work.
await db
.transaction(async (sp) => {
await User.destroy({ where: { id: 1 }, transaction: sp });
throw new Error('keep the row');
}, { transaction: t })
.catch(() => {});
});SKIP LOCKED)lock with skipLocked inside a transaction:await db.transaction(async (t) => {
await User.findAll({ where: { name: 'Ada' }, lock: { level: 'UPDATE', skipLocked: true }, transaction: t });
});STRING → VARCHAR(255), auto-increment INTEGER → INT, BOOLEAN → TINYINT(1), DATE → DATETIME, UUID → CHAR(36), JSON → JSON. The type comparison lines the major engines up side by side.dialect: 'mysql' to the Prorm constructor along with standard connectionlocalhost and the default port is 3306.import { Prorm } from 'ts-prorm-orm';
const db = new Prorm({
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'app',
password: 's3cret',
database: 'shop',
});
await db.connect(); // creates the pool and verifies one connection
// ... use the database ...
await db.close(); // pool.end()import { createMySQLDialect } from 'prorm/dialects/mysql';
const dialect = createMySQLDialect({
host: '127.0.0.1',
port: 3306,
username: 'app',
password: 's3cret',
database: 'shop',
charset: 'utf8mb4', // default
timezone: 'Z', // default — store/read as UTC
pool: { max: 20 }, // maps to mysql2 connectionLimit
});
await dialect.connect();
const version = await dialect.getDatabaseVersion(); // SELECT VERSION()| Option | Default | Notes |
|---|---|---|
host | localhost | |
port | 3306 | |
username / password / database | — | Passed to mysql2 as user / password / database. |
charset | utf8mb4 | Full Unicode incl. emoji. |
timezone | Z | UTC. |
ssl | — | Passed straight through to mysql2. |
pool | { max: 10 } | pool.max sets the mysql2 connectionLimit. |
retry | see below | Connection/query retry policy. |
connectTimeout / idleTimeout | — | Forwarded to mysql2 when set. |
connect() and query() retry on transient failures (ECONNREFUSED,ETIMEDOUT, Too many connections, Lock wait timeout, deadlocks, etc.). Byretry.backoff for exponential backoff.? placeholders, and the builder methods emit them.values array — never string-interpolate userconst { sql, values } = dialect.buildInsertQuery('users', {
email: 'ada@example.com',
name: 'Ada',
});
// sql: INSERT INTO `users` (`email`, `name`) VALUES (?, ?)
// values: ['ada@example.com', 'Ada']
await dialect.query(sql, { replacements: values });Literal, which is inlined verbatim?:// `Literal` lives in src/core/expressions and is not exported from the package root.
import { Literal } from 'ts-prorm-orm/core/expressions';
dialect.buildInsertQuery('events', { name: 'signup', at: new Literal('NOW()') });
// INSERT INTO `events` (`name`, `at`) VALUES (?, NOW())getDataTypeSql() maps the ORM's DataTypes to MySQL column types:| ORM type | MySQL column |
|---|---|
STRING(n) | VARCHAR(n) (default 255) |
CHAR(n) | CHAR(n) |
TEXT | TEXT / MEDIUMTEXT / LONGTEXT (by length) |
INTEGER | INT (or TINYINT / SMALLINT / MEDIUMINT / BIGINT by length), UNSIGNED supported |
BIGINT | BIGINT |
FLOAT / DOUBLE / DECIMAL(p,s) | FLOAT / DOUBLE / DECIMAL(p,s) |
BOOLEAN | TINYINT(1) |
DATE | DATETIME (DATETIME(p) with precision) |
DATEONLY / TIME | DATE / TIME |
BLOB | BLOB (TINYBLOB / MEDIUMBLOB / LONGBLOB) |
ENUM / SET | ENUM(...) / SET(...) |
JSON / JSONB | JSON (MySQL has no separate JSONB) |
UUID | CHAR(36) |
BIT / YEAR | BIT(n) / YEAR |
GEOMETRY, POINT, POLYGON, ... | native spatial types, optional SRID |
BOOLEAN is stored as TINYINT(1); booleans are emitted as the TRUE / FALSE1 / 0) so comparisons against JSON booleans extracted withJSON_EXTRACT() behave correctly.AUTO_INCREMENT is the MySQL identity mechanism. Mark a columnautoIncrement: true (typically with primaryKey: true); after an insert,LAST_INSERT_ID() returns the generated value.await dialect.createTable(
'users',
{
id: { type: 'BIGINT', primaryKey: true, autoIncrement: true },
email: { type: 'VARCHAR(255)', allowNull: false, unique: true },
name: { type: 'VARCHAR(120)' },
bio: { type: 'TEXT' },
created_at: { type: 'DATETIME', defaultValue: 'CURRENT_TIMESTAMP' },
},
{
engine: 'InnoDB',
charset: 'utf8mb4',
initialAutoIncrement: 1000, // AUTO_INCREMENT=1000
},
);
const res = await dialect.query(
'INSERT INTO `users` (`email`, `name`) VALUES (?, ?)',
{ replacements: ['ada@example.com', 'Ada'] },
);
// res.rowCount === affectedRows
const [{ id }] = (
await dialect.query('SELECT LAST_INSERT_ID() AS id')
).rows;engine, charset, collate, comment,initialAutoIncrement, and rowFormat. Column definitions supportCHARACTER SET / COLLATE, INVISIBLE columns (MySQL 8.0+), and comments.references (and an association's onDelete / onUpdate) isFOREIGN KEY (…) REFERENCES … ON DELETE …, inCREATE TABLE and ADD COLUMN, because InnoDB ignores a column-levelREFERENCES. Before 4.9.2 the column-level form was emitted and no foreignINSERT ... ON DUPLICATE KEY UPDATE. It fires whenever a rowPRIMARY KEY or UNIQUE constraint — you cannot scope it toON CONFLICT (cols) does.AS new_vals) rather than the deprecatedVALUES(col) function form removed in newer MySQL:const { sql, values } = dialect.buildUpsertQuery(
'users',
{ email: 'ada@example.com', name: 'Ada Lovelace', login_count: 1 },
{ updateOnDuplicate: ['name', 'login_count'] },
);
// INSERT INTO `users` (`email`, `name`, `login_count`)
// VALUES (?, ?, ?) AS new_vals
// ON DUPLICATE KEY UPDATE `name` = new_vals.`name`,
// `login_count` = new_vals.`login_count`
await dialect.query(sql, { replacements: values });updateOnDuplicate is omitted, every inserted column is updated on conflict.buildInsertQuery(..., { upsert: true }) produces the same clause inline.RETURNING is not supported. MySQL has no RETURNINGclause, so passingreturningtobuildUpsertQuerythrows rather than emitting invalid SQL.
Re-select the row afterward (e.g. viaLAST_INSERT_ID()or the known unique
key).
FULLTEXT index, then query it with MATCH(...) AGAINST(...). Full-textCHAR / VARCHAR / TEXTawait dialect.createFulltextIndex('articles', 'ft_body', ['title', 'body']);
// Build a MATCH expression directly:
const expr = dialect.buildMatchAgainst(['title', 'body'], 'search engines', {
mode: 'boolean',
});
// MATCH(`title`, `body`) AGAINST('search engines' IN BOOLEAN MODE)
const { rows } = await dialect.query(
`SELECT id, title FROM \`articles\` WHERE ${expr} LIMIT 20`,
);buildMatchAgainst supports mode: 'natural' (default, IN NATURAL LANGUAGE
MODE) and mode: 'boolean' (IN BOOLEAN MODE), plus query-expansion modifiers.WHERE objects the $match operator produces a parameterized MATCH(...)
AGAINST(? ...) predicate:const { sql, values } = dialect.buildWhereClause({
body: { $match: { columns: ['title', 'body'], searchTerm: 'ada', mode: 'boolean' } },
});
// sql: MATCH(`title`, `body`) AGAINST(? IN BOOLEAN MODE)
// values: ['ada']query() returns { rows, rowCount, fields }. SELECT / SHOW / DESCRIBE /EXPLAIN populate rows; other statements report affectedRows as rowCount.queryStream() returns a Node Readable backed bybulkInsert() batches many rows into one multi-rowINSERT:await dialect.bulkInsert('users', [
{ email: 'a@example.com', name: 'A' },
{ email: 'b@example.com', name: 'B' },
]);
// INSERT INTO `users` (`email`, `name`) VALUES (?, ?), (?, ?)JSON type is well supported via helpers such as buildJsonExtractJSON_UNQUOTE(JSON_EXTRACT(...))), buildJsonContains, buildJsonSet,buildJsonTable (8.0+), and the JSON_ARRAYAGG / JSON_OBJECTAGG aggregates.OFFSET without LIMIT is emitted as LIMIT 18446744073709551615 OFFSET n,LIMIT alongside OFFSET.MySQLTransaction wraps a dedicated pooled connection and supportscommit() / rollback() plus nested SAVEPOINT operationscreateSavepoint, rollbackToSavepoint, releaseSavepoint).const tx = await db.transaction();
try {
const sp = await tx.createSavepoint();
// ... work on tx.connection ...
await tx.releaseSavepoint(sp);
await tx.commit();
} catch (err) {
await tx.rollback();
throw err;
}RANGE, LIST, HASH, and KEY partitioning declared inCREATE TABLE. createPartitionedTable emits RANGE COLUMNS / LIST COLUMNSaddPartition /dropPartition manage partitions on an existing table. dropPartition will lookINFORMATION_SCHEMA.PARTITIONS if you don't passparentTable.CREATE DATABASE / SHOW DATABASES.RETURNING. Re-fetch after insert/upsert (LAST_INSERT_ID()).DETACH PARTITION. detachPartition throws; use DROP PARTITION.createUserMapping / dropUserMapping andON DUPLICATE KEY UPDATE reacts to any unique-keyJSONB maps to JSON. MySQL has a single native JSON type.capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | mysql |
|---|---|
| Transactions atomic | yes |
| Savepoints | yes |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable no; constraint deferral no |
| RETURNING | not emitted; returning is accepted and ignored |
| Upsert | INSERT … ON DUPLICATE KEY UPDATE |
| JSON column | JSON (JSONB → JSON) |
| Vector column | VECTOR(3) |
| Array column | VARCHAR(255) |
| WHERE builder | 28 operator keys plus the $json: prefix; $and/$or/$not; $exists: no; $in subquery: no; expression operands: no |
| SQL objects | family mysql: triggers yes; procedures yes; functions multiStatement; materialized views emulated (procedure); sequences no; enums inline; RLS no |
| Streaming | queryStream() present; LISTEN absent; createExtension() present |
FindOptions becomes SQL