Reference for the vitess SQL dialect — Vitess and PlanetScale.Overview#
Vitess shards MySQL horizontally. Applications connect to
vtgate, a stateless proxy that speaks the MySQL protocol, parses every
statement, plans it against the VSchema (the description of how each table
is sharded), and routes it to the right shards.
PlanetScale is a hosted Vitess.| Property | Value |
|---|
| Dialect name | vitess |
| Extends | MySQLDialect (mysql) |
| Driver | mysql2 |
| Cloud SDK | none |
| Default port | 15306 |
| Local runtime | self-hosted; no image in this repo |
Because the wire is MySQL's and the driver is mysql2, VitessDialect
extends MySQLDialect and inherits pooling, escaping, ? placeholders,
buildWhereClause, buildSelectQuery and every CRUD builder unchanged.
What it adds is the vtgate surface — keyspace targeting, SHOW extensions,
VSchema DDL and managed online DDL — and what it refuses is what vtgate does
not accept.export class VitessDialect extends MySQLDialect {
readonly name = 'vitess';
}
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: 'vitess',
host: 'localhost',
database: 'commerce', // the keyspace
username: 'user',
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'vitess',
url: 'vitess://user@localhost:15306/commerce',
});
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.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(() => {});
});
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.Connection#
The dialect is reached through Prorm (the class is not exported from the package root):import { Prorm } from 'ts-prorm-orm';
// Local Vitess (vtgate MySQL listener).
const local = new Prorm({
dialect: 'vitess',
host: '127.0.0.1',
// port defaults to 15306 — vtgate's MySQL port in every Vitess example
username: 'user',
database: 'commerce', // the keyspace
});
// PlanetScale: port 3306, TLS required.
const ps = new Prorm({
dialect: 'vitess',
host: 'aws.connect.psdb.cloud',
port: 3306,
username: process.env.PS_USER,
password: process.env.PS_PASSWORD,
database: 'mydb',
dialectOptions: {
ssl: { rejectUnauthorized: true },
sharded: true, // see "AUTO_INCREMENT" below
},
});
await ps.connect();
| Option | Default | Meaning |
|---|
port | 15306 | vtgate's MySQL port. Port 3306 on a Vitess host is usually a tablet's own mysqld, which bypasses vtgate and its sharding. PlanetScale serves vtgate on 3306 — pass it explicitly. |
sharded | false | Declares the keyspace sharded, which turns on the AUTO_INCREMENT refusal. |
Driver: mysql2. (PlanetScale's HTTP driver @planetscale/database is not
used by this dialect.)What differs from MySQL#
VitessDialect extends MySQLDialect (mysql) and
overrides 40 of its methods (Object.getOwnPropertyNames(VitessDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.| Area | Overridden |
|---|
| DML builders | buildSetDdlStrategySql |
| DDL and schema | addAutoIncrement, addSequence, addTableVindex, alterMigration, buildAddAutoIncrementSql, buildAddSequenceSql, buildAddTableVindexSql, buildAddVSchemaTableSql, buildAlterMigrationSql, buildCreateVindexSql, buildDropAutoIncrementSql, buildDropSequenceSql, buildDropTableVindexSql, buildDropVSchemaTableSql, buildDropVindexSql, createEvent, createProcedure, createSchema, createStoredProcedure, createTable, createTrigger, createVindex, dropSchema |
| Introspection | assertBareWord, showMigrations |
| Other | buildShowGlobalGtidExecutedSql, buildShowKeyspacesSql, buildShowMigrationsSql, buildShowReplicationStatusSql, buildShowShardsSql, buildShowTabletsSql, buildShowThrottlerStatusSql, buildShowVSchemaKeyspacesSql, buildShowVSchemaTablesSql, buildShowVSchemaVindexesSql, buildUseTargetSql, buildVindexDefinition, procedureRefusal, quoteKeyspaceTable |
Keyspace targeting#
d.quoteKeyspaceTable('customer', 'corder');
// `customer`.`corder`
d.buildUseTargetSql({ keyspace: 'commerce', tabletType: 'replica' });
// USE `commerce@replica`
d.buildUseTargetSql({ keyspace: 'customer', shard: '-80' });
// USE `customer:-80`
d.buildUseTargetSql({ keyspace: 'customer', shard: '80-', tabletType: 'rdonly' });
// USE `customer:80-@rdonly`
The whole target is one quoted identifier that vtgate parses internally, so the
parts are validated, not escaped: a keyspace containing : or @ would
silently change the shard or tablet type, and is refused. Shards must be a key
range (-80, 80-, 40-80) or 0; tablet types are primary, replica,
rdonly.USE is session state. On this dialect's mysql2 pool it retargets only the
connection that ran it. For pool-wide reach into another keyspace, use
quoteKeyspaceTable(); for a replica session, hold a dedicated connection.SHOW extensions#
| Builder | SQL |
|---|
buildShowKeyspacesSql() | SHOW KEYSPACES |
buildShowShardsSql() | SHOW VITESS_SHARDS |
buildShowTabletsSql() | SHOW VITESS_TABLETS |
buildShowVSchemaTablesSql() | SHOW VSCHEMA TABLES |
buildShowVSchemaKeyspacesSql() | SHOW VSCHEMA KEYSPACES |
buildShowVSchemaVindexesSql(table?) | SHOW VSCHEMA VINDEXES [ON table] |
buildShowReplicationStatusSql(like?) | SHOW VITESS_REPLICATION_STATUS [LIKE '...'] |
buildShowGlobalGtidExecutedSql(ks?) | SHOW GLOBAL GTID_EXECUTED [FROM ks] |
buildShowThrottlerStatusSql() | SHOW VITESS_THROTTLER STATUS |
buildShowMigrationsSql({ keyspace?, like?, where? }) | SHOW VITESS_MIGRATIONS [FROM ks] [LIKE ... | WHERE ...] |
buildShowMigrationsSql takes a where-object and returns bound values:d.buildShowMigrationsSql({ where: { migration_status: 'running' } });
// { sql: 'SHOW VITESS_MIGRATIONS WHERE `migration_status` = ?', values: ['running'] }
await d.showMigrations({ where: { migration_status: 'running' } }); // rows
VSchema DDL — ALTER VSCHEMA#
Every documented form has a builder. Vindex types and parameter names are
validated as bare identifiers; parameter values are single-quoted literals
(which is also what allows dotted names like a lookup's table).d.buildCreateVindexSql('corder_keyspace_idx', 'consistent_lookup_unique', {
table: 'product.corder_keyspace_idx', from: 'corder_id', to: 'keyspace_id',
});
// ALTER VSCHEMA CREATE VINDEX `corder_keyspace_idx` USING consistent_lookup_unique
// WITH table = 'product.corder_keyspace_idx', from = 'corder_id', to = 'keyspace_id'
d.buildAddTableVindexSql('customer.corder', { name: 'hash', columns: ['customer_id'] });
// ALTER VSCHEMA ON `customer`.`corder` ADD VINDEX `hash` (`customer_id`)
d.buildAddSequenceSql('product.corder_seq');
// ALTER VSCHEMA ADD SEQUENCE `product`.`corder_seq`
d.buildAddAutoIncrementSql('customer.corder', 'corder_id', 'product.corder_seq');
// ALTER VSCHEMA ON `customer`.`corder` ADD AUTO_INCREMENT `corder_id` USING `product`.`corder_seq`
Also: buildDropVindexSql, buildAddVSchemaTableSql, buildDropVSchemaTableSql,
buildDropTableVindexSql, buildDropSequenceSql, buildDropAutoIncrementSql,
and the executors createVindex, addTableVindex, addSequence,
addAutoIncrement.Managed online DDL#
d.buildSetDdlStrategySql({ strategy: 'vitess', postponeCompletion: true, cutOverThreshold: '10s' });
// SET @@ddl_strategy = 'vitess --postpone-completion --cut-over-threshold=10s'
d.buildAlterMigrationSql('aa89f255_8d68_11eb_815f_f875a4d24e90', 'complete');
// ALTER VITESS_MIGRATION 'aa89f255_8d68_11eb_815f_f875a4d24e90' COMPLETE
d.buildAlterMigrationSql('all', 'cancel');
// ALTER VITESS_MIGRATION CANCEL ALL
Strategies: vitess, mysql, direct (gh-ost/pt-osc were removed in
Vitess 22 and are refused). Boolean flags: postponeCompletion,
postponeLaunch, allowConcurrent, declarative, singleton,
singletonContext, singletonTable, inOrderCompletion, preferInstantDdl,
analyzeTable. Duration flags (cutOverThreshold, forceCutOverAfter,
retainArtifacts) must be Go durations (10s, 1h30m).Migration actions: launch, complete, postpone complete, cancel,
retry, cleanup, force_cutover, throttle, unthrottle. retry and
cleanup have no ALL form and refuse 'all'.@@ddl_strategy is session state — run it on the connection that runs the
DDL. On PlanetScale production branches with safe migrations, schema changes go
through deploy requests and direct DDL is rejected.Refused, by name#
| Call | Why |
|---|
createStoredProcedure, createProcedure | vtgate does not accept CREATE PROCEDURE; Vitess says to create procedures on the underlying MySQL (impossible on PlanetScale). |
createTrigger | vtgate does not accept trigger DDL. |
createEvent | vtgate does not accept event-scheduler DDL. |
createSchema, dropSchema | CREATE/DROP DATABASE are disabled by default; keyspaces are created with vtctldclient CreateKeyspace or the PlanetScale dashboard/API. |
createTable with an autoIncrement column when sharded: true | Each shard's MySQL has its own counter, so ids collide. Use a sequence + buildAddAutoIncrementSql. |
The first three throw UnsupportedSchemaObjectError.Not refused, but limited by vtgate: CALL of an existing procedure (unsharded
keyspace or targeted shard only, IN parameters only, no result set);
foreign keys (need foreign_key_mode: managed in the VSchema, or FK support
enabled on PlanetScale); views (read-only, single keyspace); LOAD DATA
(unsharded only).Type mapping#
Identical to the MySQL dialect — vtgate forwards column types to
MySQL unchanged.Caveats / not-yet-verified#
Every statement above is asserted as a string in tests/dialects/vitess.test.ts
without a database. Execution against a live vtgate is not exercised.
The dialect cannot know whether a keyspace is sharded; sharded is the
caller's declaration.
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 | vitess |
|---|
| 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 |
Modified at 2026-10-04 00:40:16