Overview#
Reference documentation for the singlestore dialect.| Property | Value |
|---|
| Dialect name | singlestore |
| Extends | MySQLDialect (mysql) |
| Driver | mysql2 |
| Cloud SDK | none |
| Default port | 3306 |
| Local runtime | self-hosted; no image in this repo |
SingleStore (formerly MemSQL) is a distributed, MySQL-wire-compatible database. Because it speaks the MySQL protocol, SingleStoreDialect extends MySQLDialect and reuses the exact same mysql2 driver, connection pooling, query execution, escaping, and the WHERE / ORDER / INSERT / UPDATE / DELETE / SELECT builders. Only the grammar that genuinely differs from stock MySQL is overridden.reports name === 'singlestore' but keeps library === 'mysql2' (verified in tests: dialect.name is 'singlestore', dialect.library is 'mysql2');
defaults the SQL port to 3306 — the constructor calls super({ port: 3306, ...config }), so the MySQL wire port is used unless you override it;
overrides only: CREATE TABLE grammar (storage kind, SHARD KEY, SORT KEY, clustered columnstore key, reference tables), the upsert form, the GEOGRAPHY type mapping, and the foreign-key handling.
SingleStoreDialectOptions is declared as interface SingleStoreDialectOptions extends MySQLDialectOptions {} — the connection options are identical to MySQL's, since the wire protocol and driver are shared.
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: 'singlestore',
host: 'svc-abc.svc.singlestore.com',
database: 'app',
username: 'admin',
password: process.env.DB_PASSWORD,
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'singlestore',
url: 'singlestore://admin:secret@svc-abc.svc.singlestore.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.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#
await db.transaction(async (t) => {
await User.create({ email: 'grace@example.com', name: 'Grace' }, { transaction: t });
await User.update({ name: 'Grace Hopper' }, { where: { email: 'grace@example.com' }, transaction: t });
});
[!WARNING] No savepoints
Nested transactions need savepoints, and singlestore has none. Run the work in a single transaction, or restructure it so nothing nests.
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#
import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'singlestore',
host: 'your-singlestore-host',
port: 3306, // default; SingleStore uses the MySQL wire protocol
database: 'app',
username: 'admin',
password: 'secret',
});
await prorm.connect();
'singlestore' is a registered dialect string in Prorm.createDialect; it is lazy-loaded (require('./dialects/singlestore').SingleStoreDialect) so the mysql2 driver is only required when the dialect is actually used. If port is omitted, the dialect constructor supplies 3306.
3. SingleStore-specific features#
These are configured through SingleStoreTableOptions, which is intersected with the shared TableOptions and passed to createTable(...) / buildCreateTableSQL(...):interface SingleStoreTableOptions {
rowstore?: boolean; // CREATE ROWSTORE TABLE ...
reference?: boolean; // CREATE REFERENCE TABLE ...
shardKey?: string[]; // SHARD KEY (...) ([] => SHARD KEY ())
sortKey?: string[]; // SORT KEY (...)
columnstoreKey?: { fields: string[] }; // KEY (...) USING CLUSTERED COLUMNSTORE
}
buildCreateTableSQL(tableName, columns, options) is a synchronous, no-I/O builder (so the grammar can be inspected/tested without a live connection); createTable(...) delegates to it and then creates any secondary indexes.SHARD KEY and SORT KEY (columnstore)#
SingleStore is distributed and shards data across leaf nodes by a shard key; columnstore tables also store rows ordered by a sort key.dialect.buildCreateTableSQL(
'events',
{
id: { type: DataTypes.BIGINT, allowNull: false },
user_id: { type: DataTypes.BIGINT, allowNull: false },
created_at: { type: DataTypes.DATE },
},
{ shardKey: ['user_id'], sortKey: ['created_at'] },
);
Emits (columnstore is the default — no ROWSTORE/REFERENCE keyword):(Tests assert the output contains CREATE TABLE `events`, SHARD KEY (`user_id`), and SORT KEY (`created_at`). DATE maps to DATETIME — see the type table.)Keyless SHARD KEY (random sharding)#
An empty shardKey array emits the keyless SHARD KEY () form, which requests random sharding (also how a plain distributed table with no explicit shard key is declared). Note shardKey is emitted whenever it is not undefined, so [] still produces a clause:dialect.buildCreateTableSQL(
'metrics',
{ value: { type: DataTypes.DOUBLE } },
{ shardKey: [] },
);
ROWSTORE tables#
SingleStore tables are columnstore by default. { rowstore: true } requests an in-memory rowstore table:dialect.buildCreateTableSQL(
'sessions',
{ id: { type: DataTypes.BIGINT, primaryKey: true } },
{ rowstore: true, shardKey: ['id'] },
);
REFERENCE (replicated) tables#
{ reference: true } emits CREATE REFERENCE TABLE. Reference tables are fully replicated to every node rather than sharded — typically small dimension tables joined against large sharded fact tables (so a shard key is normally omitted):dialect.buildCreateTableSQL(
'countries',
{
code: { type: DataTypes.STRING, primaryKey: true },
name: { type: DataTypes.STRING },
},
{ reference: true },
);
USING CLUSTERED COLUMNSTORE key#
{ columnstoreKey: { fields: [...] } } emits the explicit clustered-columnstore key form. It is only emitted when fields is non-empty:dialect.buildCreateTableSQL(
'logs',
{
ts: { type: DataTypes.DATE },
message: { type: DataTypes.TEXT },
},
{ columnstoreKey: { fields: ['ts'] } },
);
Clause ordering#
Within the parenthesised body, buildCreateTableSQL appends parts in this order: column definitions → table constraints (PK/UNIQUE/CHECK) → SHARD KEY (...) → SORT KEY (...) → KEY (...) USING CLUSTERED COLUMNSTORE. Table-level options (DEFAULT CHARSET=, COLLATE, COMMENT=, and IF NOT EXISTS) are inherited-style and handled after the body.
4. Caveats#
Foreign keys are not supported — the dialect refuses to emit FK SQL#
SingleStore does not support foreign key constraints (a real server rejects FOREIGN KEY ... REFERENCES ...). Rather than generate statements that would fail, the dialect handles FKs as follows:CREATE TABLE, table-level FOREIGN KEY constraint — silently omitted. The private buildTableConstraintSQL returns null for FOREIGN KEY (PRIMARY KEY / UNIQUE / CHECK are emitted normally), so the table is still created, just without the FK clause. A test passes a FOREIGN KEY constraint and asserts the output contains neither FOREIGN KEY nor REFERENCES.
CREATE TABLE, column-level references — also omitted. buildColumnSQL never emits a REFERENCES clause, so a column definition carrying references produces no REFERENCES in the DDL (verified by test).
addColumn / changeColumn — strip any column-level references (via stripReferences) before delegating to the MySQL base, whose column builder would otherwise emit a REFERENCES ... clause SingleStore rejects.
addForeignKey(...) — throws: SingleStore does not support foreign key constraints. Enforce referential integrity in application code instead.
createConstraint(...) with type: 'FOREIGN KEY' — throws the same error. Other constraint types delegate to the MySQL base.
Enforce referential integrity in application code instead.Unique constraints on columnstore tables must include the shard key#
This is a SingleStore data-distribution rule (a unique index on a columnstore table must contain the shard key columns). As documented in the source header, the ORM does not attempt to police this — it will emit the UNIQUE (...) you ask for; a real server enforces the rule. Plan your UNIQUE constraints/shardKey accordingly.No RETURNING clause#
Like MySQL, SingleStore has no RETURNING clause. buildUpsertQuery(..., { returning: true }) fails fast with an error matching /RETURNING/; re-fetch the row after the upsert instead.
5. Inherited MySQL behavior#
CRUD and standard DDL are inherited from MySQLDialect and behave identically, using backtick identifier quoting and ? placeholders. Verified in the CRUD tests:dialect.buildInsertQuery('users', { id: 1, name: 'Ada' });
// sql: INSERT INTO `users` (`id`, `name`) VALUES (?, ?)
// values: [1, 'Ada']
Literal values are inlined without a placeholder:dialect.buildInsertQuery('t', { id: 1, created_at: new Literal('NOW()') });
// sql: INSERT INTO `t` (`id`, `created_at`) VALUES (?, NOW())
// values: [1]
UPDATE / DELETE / INCREMENTdialect.buildUpdateQuery('users', { name: 'Grace' }, { id: 1 }, { limit: 1 });
// UPDATE `users` SET `name` = ? WHERE `id` = ? LIMIT 1 values: ['Grace', 1]
dialect.buildDeleteQuery('users', { id: 7 });
// DELETE FROM `users` WHERE `id` = ? values: [7]
dialect.buildIncrementQuery('posts', 'views', { id: 3 }, { by: 2 });
// UPDATE `posts` SET `views` = `views` + ? ... values: [2, 3]
SELECT — inherited WHERE (AND / Op.or), ORDER BY, GROUP BY / HAVING, and LIMIT/OFFSET. MySQL's offset-only semantics are preserved: buildLimitOffset(undefined, 10) yields LIMIT 18446744073709551615 OFFSET 10.Standard DDL — IF NOT EXISTS, DEFAULT CHARSET=, COLLATE, and table COMMENT= all work through buildCreateTableSQL:PRIMARY KEY / UNIQUE / CHECK table constraints emit normally alongside a shard key:This is the one CRUD override. The MySQL base emits the MySQL 8.0.19 row-alias form (... AS new_vals ... = new_vals.col), which SingleStore does not support. SingleStoreDialect overrides both buildInsertQuery (upsert branch) and buildUpsertQuery to emit the classic VALUES(col) reference instead:dialect.buildInsertQuery(
'users',
{ id: 1, name: 'Ada', email: 'ada@x.io' },
{ upsert: true },
);
// ... ON DUPLICATE KEY UPDATE `id` = VALUES(`id`), `name` = VALUES(`name`), `email` = VALUES(`email`)
// (never contains "AS new_vals" or "new_vals.")
updateOnDuplicate restricts the SET list; otherwise all inserted columns are updated:dialect.buildUpsertQuery('kv', { k: 'a', v: 10 });
// INSERT INTO `kv` (`k`, `v`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `k` = VALUES(`k`), `v` = VALUES(`v`)
// values: ['a', 10]
6. Type mapping#
Scalar types are mapped exactly as in MySQL (getDataTypeSql calls super.getDataTypeSql) — with one override: the geospatial GEOGRAPHY type. SingleStore has a native GEOGRAPHY type, whereas stock MySQL has none and the base dialect degrades it to GEOMETRY. The dialect intercepts { key: 'GEOGRAPHY' } and returns the native type so geography columns round-trip correctly.ORM type (DataTypes.* / { key }) | SingleStore SQL | Notes |
|---|
STRING | VARCHAR(255) | inherited from MySQL |
BIGINT | BIGINT | inherited |
INTEGER | INT | inherited |
DOUBLE | DOUBLE | inherited |
BOOLEAN | TINYINT(1) | inherited |
JSON | JSON | inherited |
DATE | DATETIME | inherited |
UUID | CHAR(36) | inherited |
TEXT | TEXT | inherited |
DECIMAL (precision 10, scale 2) | DECIMAL(10,2) | inherited |
GEOMETRY | GEOMETRY | inherited (unchanged) |
GEOGRAPHY | GEOGRAPHY | SingleStore override (MySQL base would emit GEOMETRY) |
All rows except GEOGRAPHY are asserted directly in the type-mapping tests; the GEOGRAPHY override and the GEOMETRY contrast are also asserted.The source header also mentions GEOGRAPHYPOINT as a SingleStore native type, but only the GEOGRAPHY key is intercepted in getDataTypeSql; there is no dedicated GEOGRAPHYPOINT mapping in the code.
7. Not yet verified against a live server#
Everything above about SQL string generation is verified by the unit tests, which exercise only the synchronous builders (buildCreateTableSQL, buildInsertQuery, buildUpsertQuery, buildSelectQuery, buildUpdateQuery, buildDeleteQuery, buildIncrementQuery, getDataTypeSql, buildLimitOffset) plus the FK-rejection error paths. No test opens a connection (connect() is never called).The following are not exercised against a real SingleStore instance and should be validated in a live environment before relying on them:Connectivity and the mysql2 driver against a real cluster — pool creation, authentication, retries, and query execution are inherited from the MySQL dialect and untested here against SingleStore specifically.
Server acceptance of the generated DDL — that a live SingleStore server accepts SHARD KEY, SORT KEY, KEY (...) USING CLUSTERED COLUMNSTORE, CREATE ROWSTORE TABLE, and CREATE REFERENCE TABLE exactly as emitted.
Data-distribution rules — that a UNIQUE constraint including the shard key is accepted, and that one omitting it is rejected by the server (the ORM does not police this).
Runtime behavior of VALUES(col) upserts, GEOGRAPHY round-tripping, and the FK omission — that omitting foreign keys behaves acceptably for your integrity needs, and that the columnstore/rowstore/reference semantics match expectations at query time.
Introspection helpers inherited from MySQL (showTables, describeTable, getCreateTable, showIndexes, showConstraints, INFORMATION_SCHEMA-based partition helpers, etc.) — these assume MySQL system-catalog shapes and have not been checked against SingleStore.
What differs from MySQL#
SingleStoreDialect extends MySQLDialect (mysql) and
overrides 23 of its methods (Object.getOwnPropertyNames(SingleStoreDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.| Area | Overridden |
|---|
| DML builders | buildInsertQuery, buildUpsertQuery, formatDefaultValue |
| Types | legacyDataTypeSql, mapNativeType, modernDataTypeSql |
| DDL and schema | addColumn, addForeignKey, buildColumnSQL, buildCreateTableSQL, buildTableConstraintSQL, changeColumn, createConstraint, createSavepointSQL, createTable, createTrigger, dropTrigger |
| Introspection | hasTrigger, listTriggers |
| Other | legacyNeedsModern, releaseSavepointSQL, rollbackToSavepointSQL, stripReferences |
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 | singlestore |
|---|
| Transactions atomic | yes |
| Savepoints | no — a nested transaction() throws |
| 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, F32) |
| 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 |
Testing#
tests/dialects/singlestore.test.ts exercises the dialect with a mocked driver, so nothing touches a server.Querying — how a FindOptions becomes SQL Modified at 2026-10-04 00:40:16