Overview#
PolarDB is Alibaba Cloud's
cloud-native database family. It is a family, not one engine, and that is the
first thing to settle before using this dialect.| Property | Value |
|---|
| Dialect name | polardb |
| Extends | MySQLDialect (mysql) |
| Driver | mysql2 |
| Cloud SDK | none |
| Default port | 3306 |
| Local runtime | none — hosted service only |
Quick start#
Install#
$ npm install ts-prorm-orm mysql2
$ pnpm add ts-prorm-orm mysql2
$ yarn add ts-prorm-orm mysql2
$ bun add ts-prorm-orm mysql2
mysql2 is the same driver the mysql dialect uses.Connect#
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'polardb',
host: 'pc-xxxx.mysql.polardb.rds.aliyuncs.com',
database: 'app',
username: 'app',
password: process.env.DB_PASSWORD,
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'polardb',
url: 'polardb://app:secret@pc-xxxx.mysql.polardb.rds.aliyuncs.com:3306/app',
});
A URL is folded into host, port, database, username and password before the dialect is built; any of those passed alongside it wins.Define a model#
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 below
Query#
import { 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,
});
Generated SQL#
Each block is the statement the model call above it hands the dialect's query(), captured with the driver stubbed out. The model layer inlines the values.Create table#
db.sync() for the model in the Quick start:CREATE TABLE
Insert, update and delete#
await User.create({ email: 'ada@example.com', name: 'Ada' });
await User.update({ name: 'Grace' }, { where: { email: 'ada@example.com' } });
await User.destroy({ where: { id: 1 } });
Upsert#
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,
});
JSON#
[!NOTE] No JSON operator in the WHERE builder
DataTypes.JSON() maps to JSON, but this dialect's WHERE builder refuses Op.contains on it: Cannot render WHERE condition at meta.containsforpolardb:theoperator′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.
Transaction with a savepoint#
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 rows (SKIP LOCKED)#
A worker that claims rows from a queue table should pass over rows another transaction holds rather than wait for them. lock with skipLocked inside a transaction:await db.transaction(async (t) => {
await User.findAll({ where: { name: 'Ada' }, lock: { level: 'UPDATE', skipLocked: true }, transaction: t });
});
Type mapping for the portable descriptors: 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.Which PolarDB this is — and the limitation#
Alibaba Cloud ships four PolarDB products:| Product | Wire protocol | Driver | Covered here? |
|---|
| PolarDB for MySQL | MySQL | mysql2 | Yes |
| PolarDB for PostgreSQL | PostgreSQL | pg | No |
| PolarDB for PostgreSQL (Oracle-compatible) | PostgreSQL | pg | No |
| PolarDB-X (distributed) | MySQL | mysql2 | No |
They do not share a wire protocol, a grammar, or a driver. This library resolves
one dialect name to one engine, so 'polardb' has to pick one.It picks PolarDB for MySQL — the flagship edition, the one with by far the
most documentation and the one people usually mean by "PolarDB". PolarDBDialect
extends MySQLDialect and reuses the mysql2 driver on the standard
port 3306.PolarDB for PostgreSQL is not covered by this name. Pointing this dialect
at it will fail to connect — it needs pg, not mysql2. Use the
postgres dialect against it; that is the closest thing
available today, and it will miss any PolarDB-PG-specific behaviour. PolarDB-X is not covered either. It is MySQL-wire compatible, so a
connection will succeed, but its sharding DDL (DBPARTITION BY,
TBPARTITION BY) and distributed sequences (AUTO_INCREMENT BY GROUP |
SIMPLE | TIME) have no representation here, and its restrictions differ from
PolarDB for MySQL's. Treating it as this dialect will work for simple CRUD and
mislead you on anything schema-shaped.
If either of those matters to you, they want their own dialect names
(polardb-pg, polardb-x) rather than a flag on this one.Connection#
import { Prorm } from 'ts-prorm-orm';
const db = new Prorm({
dialect: 'polardb',
host: 'pc-xxxx.mysql.polardb.rds.aliyuncs.com',
port: 3306, // the cluster endpoint's default; no override needed
username: 'app',
password: '...',
database: 'test',
});
Point host at the cluster endpoint to get PolarDB's read/write splitting
(reads distributed across read-only nodes, writes to the primary), or at the
primary endpoint to bypass it.MySQL version compatibility#
| PolarDB engine | Compatible with MySQL Community |
|---|
| 8.0.2 | 8.0.18 and earlier |
| 8.0.1 | 8.0.13 and earlier |
| 5.7 | 5.7.28 and earlier |
| 5.6 | 5.6.16 and earlier |
That ceiling of 8.0.18 matters below.What differs from MySQL#
PolarDBDialect extends MySQLDialect (mysql) and
overrides 9 of its methods (Object.getOwnPropertyNames(PolarDBDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.| Area | Overridden |
|---|
| Connection and execution | startTransaction |
| DML builders | buildInsertQuery, buildSelectQuery, buildUpsertQuery |
| DDL and schema | createPartitionedTable |
| Introspection | assertInnoDBPartitioning |
| Other | buildDuplicateKeyUpdateClause, buildParallelHint, withRouteHint |
Divergences from MySQL#
No SERIALIZABLE isolation#
PolarDB for MySQL documents support for READ UNCOMMITTED, READ COMMITTED
(the default) and REPEATABLE READ. SERIALIZABLE is explicitly not
supported — a documented divergence from stock InnoDB, which has all four.The base MySQL dialect passes whatever level it is given into
SET TRANSACTION ISOLATION LEVEL, leaving the caller believing a serializable
transaction was started. startTransaction() rejects it by name instead:PolarDB for MySQL does not support the SERIALIZABLE isolation level.
Supported levels are READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ.
MySQL's row-alias upsert form (INSERT ... VALUES (...) AS new_vals ON DUPLICATE
KEY UPDATE col = new_vals.col) landed in 8.0.19 — one release past
PolarDB's documented 8.0.18 ceiling. The base MySQLDialect emits it.PolarDBDialect overrides buildInsertQuery (with options.upsert) and
buildUpsertQuery to emit the classic VALUES(col) function form, which is
inside the compatibility window:db.dialect.buildUpsertQuery('kv', { k: 'a', v: 1 });
PolarDB, like MySQL, has no RETURNING clause; passing returning to
buildUpsertQuery throws.Partitioned tables are InnoDB only#
Every PolarDB table is InnoDB. On an ordinary table a MyISAM / MEMORY /
CSV engine clause is silently converted to InnoDB — nothing breaks, and the
engine option is passed through unchanged. On a partitioned table it is
rejected outright with ER_CHECK_NOT_IMPLEMENTED.createPartitionedTable() therefore refuses a non-InnoDB engine up front,
naming the rule rather than surfacing a server error code:PolarDB for MySQL supports only the InnoDB engine for partitioned tables (got
'MyISAM'). Non-InnoDB engines are converted to InnoDB on ordinary tables but
rejected on partitioned ones.
assertInnoDBPartitioning(engine?) is exposed separately so the check can be
made without a connection.PolarDB-specific features#
Read/write-splitting hints#
A cluster endpoint routes each statement between the primary and its read-only
nodes. Two comment hints override that routing per statement:/*FORCE_MASTER*/ — run on the primary. Use for a read that must observe a
write made outside the current session, where replication lag on a read-only
node would return stale data.
/*FORCE_SLAVE*/ — run on a read-only node. Use to keep a heavy analytical
read off the primary.
db.dialect.withRouteHint('SELECT * FROM t', 'master');
// "/*FORCE_MASTER*/SELECT * FROM t"
Or through a built query:db.dialect.buildSelectQuery({ tableName: 'users', route: 'master' });
The hint must lead the statement — PolarDB's proxy reads it before parsing —
and it may not contain a statement that changes session or environment
variables. Note that the mysql CLI strips comment hints unless run with -c;
the mysql2 driver this dialect uses does not.Within a single session, PolarDB guarantees session (monotonic read)
consistency without any hint: a read always reflects that session's own prior
writes regardless of replication lag. FORCE_MASTER is for reads that must see
another session's writes.Elastic parallel query#
/*+ PARALLEL(n) */ requests n parallel workers for a statement; parallel: 0
emits NO_PARALLEL() to pin it to a serial plan.db.dialect.buildSelectQuery({ tableName: 'users', parallel: 8 });
The two hint kinds sit in different places and cannot be merged — the routing
hint is a leading comment read by the proxy before parsing, while the parallel
hint is an optimizer hint that must follow the SELECT keyword. Asking for both
places each correctly:Inherited MySQL behavior#
Standard CRUD and most DDL are inherited from MySQLDialect and behave exactly
as documented for the MySQL dialect — backtick-quoted identifiers,
SELECT / INSERT / UPDATE / DELETE builders, JSON helpers, FOR UPDATE /
LOCK IN SHARE MODE, and the full type mapping.Two inherited behaviours carry PolarDB-specific costs worth knowing:FULLTEXT and SPATIAL indexes work, with Online-DDL penalties. Adding the
first FULLTEXT index to a table with no FTS_DOC_ID column rebuilds the whole
table; adding a SPATIAL index blocks concurrent DML for the duration.
Foreign keys work, except on partitioned tables. An INPLACE DDL that
changes a foreign key requires foreign_key_checks disabled; otherwise it
falls back to a full-table-locking COPY DDL.
And one operational note that no SQL expresses: a DDL on the primary must
synchronise its exclusive metadata lock to every read-only node before it
completes, so a long-running transaction on a read-only node can stall a DDL on
the primary until replica_lock_wait_timeout expires.Caveats / not-yet-verified#
SQL generation is verified; live execution is not. Every SQL snippet on
this page is the exact output of a builder asserted in
tests/dialects/polardb.test.ts, which runs without any database
connection. None of it has been executed against a live PolarDB cluster in
this repository.
The upsert form is inferred, not confirmed. No PolarDB document states
whether the 8.0.19 row-alias form is accepted or rejected; the classic
VALUES(col) form is emitted because it is inside the documented 8.0.18
compatibility ceiling and is known-good either way. This is a conservative
choice, not a claim that the row-alias form fails.
mysql2 is not named as a validated driver. Alibaba documents standard
MySQL clients and JDBC connectors, and states drop-in MySQL-driver
compatibility explicitly for PolarDB-X; no page names mysql2 specifically.
It is MySQL-wire compatible in practice.
Managed-service restrictions are not modelled. Whether PolarDB restricts
LOAD DATA LOCAL INFILE, user-defined functions, CHANGE MASTER, SET
GLOBAL, FLUSH/RESET PRIVILEGES or CREATE TABLESPACE is not documented
on the pages consulted. RDS-style restrictions are plausible by analogy but
were not confirmed, so nothing here rejects them. Expect the usual managed-
service limits and verify against your cluster.
Temporary tables are assumed to work normally. No documentation says
otherwise; the only explicit exclusion found is that instant column addition
does not apply to them.
Introspection helpers inherited from MySQL (showTables, describeTable,
getCreateTable, showIndexes, showConstraints, and the
INFORMATION_SCHEMA-based partition helpers) have not been checked against a
PolarDB cluster.
Capabilities#
Computed from the code — capabilitiesOf() (src/transaction-support.ts),
whereSupportOf() (src/query-builders/expression-render.ts),
sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the
builders — by the scratch test described in the
capability matrix.| Capability | polardb |
|---|
| 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 unknown: triggers no; procedures no; functions no; materialized views no; sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN absent; createExtension() present |
Querying — how a FindOptions becomes SQL Modified at 2026-10-04 00:40:16