| Property | Value |
|---|---|
| Dialect name | questdb |
| Extends | PostgresDialect (postgres) |
| Driver | pg |
| Cloud SDK | none |
| Default port | 8812 |
| Local runtime | prorm db up --dialect questdb (questdb/questdb:8.1.1) |
questdbquestdb, questdb-crudQuestDBDialect extends PostgresDialect and reuses the exact same pg driver:export class QuestDBDialect extends PostgresDialect {
readonly name = 'questdb';
constructor(config: PostgresDialectOptions = {}) {
// QuestDB's PostgreSQL-wire (pgwire) port defaults to 8812.
super({ port: 8812, ...config });
}
// ...
}dialect.name is 'questdb'.dialect.library is 'pg' (inherited from PostgresDialect).pgwire port), not Postgres's 5432. Any port you pass overrides it.escapeId, e.g. "trades").CREATE TABLE DDL, type mapping, LIMIT pagination, INSERT/UPSERT/UPDATE/DELETE, ADD COLUMN, SYMBOL indexes, RENAME TABLE, and the (unsupported) relational constraints.buildCreateTableSQL() is a pure, synchronous function that returns a SQL string, so it can be unit-tested without a live connection. createTable() simply runs it through the inherited query().$ npm install ts-prorm-orm pg$ pnpm add ts-prorm-orm pg$ yarn add ts-prorm-orm pg$ bun add ts-prorm-orm pgpg is the same driver the postgres dialect uses. pg-copy-streams is optional: only the features that need it load it.import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'questdb',
host: 'localhost',
database: 'qdb',
username: 'admin',
password: process.env.QUESTDB_PASSWORD,
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'questdb',
url: 'questdb://admin:quest@localhost:8812/qdb',
});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' } });[!WARNING] No row-level delete
QuestDB does not support row-level DELETE. Remove data by dropping time partitions (ALTER TABLE ... DROP PARTITION ...) or truncating the table (pass { truncate: true }).
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.[!WARNING] A plain INSERT
The engine has no conflict clause, so this upsert inserts and never updates an existing row.
await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});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 questdb has none. Run the work in a single transaction, or restructure it so nothing nests.
STRING → STRING, auto-increment INTEGER → INT, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → UUID, JSON → STRING. The type comparison lines the major engines up side by side.import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'questdb',
host: 'localhost',
port: 8812, // optional — defaults to 8812 for QuestDB
database: 'qdb',
username: 'admin',
password: 'quest',
});
await prorm.connect();port, the dialect defaults to QuestDB's pgwire port 8812.buildCreateTableSQL(tableName, columns, options). The emitted clause order is:CREATE TABLE [IF NOT EXISTS] name (cols...)
[TIMESTAMP(designated) PARTITION BY UNIT [WAL|BYPASS WAL]]
[DEDUP UPSERT KEYS(...)]PARTITION BYtimestamp (the column name) and partitionBy in the table options. PARTITION BY is only emitted inside the timestamp branch — QuestDB only partitions tables that declare a designated timestamp. When timestamp is set but partitionBy is omitted, the unit defaults to NONE.QuestDBPartitionUnit): NONE | YEAR | MONTH | DAY | HOUR | WEEK.dialect.buildCreateTableSQL(
'trades',
{
ts: { type: DataTypes.DATE() },
price: { type: DataTypes.DOUBLE() },
},
{ timestamp: 'ts', partitionBy: 'DAY' },
);SYMBOL type (CAPACITY / CACHE)SYMBOL is QuestDB's interned dictionary type for low-cardinality repeated strings (ticker symbols, device ids). A column is emitted as SYMBOL when any of these hold:symbol: true on the column definition, orkey is 'SYMBOL', or'SYMBOL' (case-insensitive).symbolCapacity → CAPACITY n (distinct-value hint), and symbolCache: true → CACHE (keep the dictionary in memory).dialect.buildCreateTableSQL('trades', {
ticker: { type: DataTypes.STRING(), symbol: true, symbolCapacity: 256, symbolCache: true },
});symbol: true overrides the declared type — the STRING above becomes SYMBOL.wal controls the write-ahead log: true → WAL, false → BYPASS WAL. When wal is omitted, no WAL clause is emitted (the server default applies). The clause is only emitted inside the timestamp branch.dialect.buildCreateTableSQL(
'wal_trades',
{ ts: { type: DataTypes.DATE() } },
{ timestamp: 'ts', partitionBy: 'HOUR', wal: true },
);dialect.buildCreateTableSQL(
'nowal_trades',
{ ts: { type: DataTypes.DATE() } },
{ timestamp: 'ts', partitionBy: 'HOUR', wal: false },
);DEDUP UPSERT KEYSON CONFLICT, row de-duplication is declared on the table via DEDUP UPSERT KEYS(...) (WAL tables only). Rows sharing the given key columns are upserted; the designated timestamp must be one of the keys. Supply dedupUpsertKeys (only emitted when the array is non-empty, inside the timestamp branch).dialect.buildCreateTableSQL(
'trades',
{
ts: { type: DataTypes.DATE() },
ticker: { type: DataTypes.STRING(), symbol: true },
},
{ timestamp: 'ts', partitionBy: 'DAY', wal: true, dedupUpsertKeys: ['ts', 'ticker'] },
);getDataTypeSql() overrides the inherited Postgres mapping to use QuestDB's type vocabulary. It handles both DataTypes.* objects (via the type's key) and string shorthands (e.g. 'BIGINT', 'DATETIME').ORM type (DataTypes.* / key) | QuestDB type |
|---|---|
INTEGER / INT | INT |
BIGINT | LONG |
FLOAT / REAL | FLOAT |
DOUBLE / DECIMAL / NUMERIC / DOUBLE PRECISION | DOUBLE |
BOOLEAN / BOOL | BOOLEAN |
STRING / TEXT / CHAR / VARCHAR | STRING |
SYMBOL | SYMBOL |
DATE / DATEONLY / TIME / DATETIME / TIMESTAMP | TIMESTAMP |
UUID | UUID |
BLOB | BINARY |
JSON / JSONB | STRING (QuestDB has no native JSON type) |
| anything else (object) | falls back to super.getDataTypeSql() (Postgres mapping) |
dialect.getDataTypeSql(DataTypes.BIGINT()); // 'LONG'
dialect.getDataTypeSql(DataTypes.INTEGER()); // 'INT'
dialect.getDataTypeSql(DataTypes.DOUBLE()); // 'DOUBLE'
dialect.getDataTypeSql(DataTypes.FLOAT()); // 'FLOAT'
dialect.getDataTypeSql(DataTypes.STRING()); // 'STRING'
dialect.getDataTypeSql(DataTypes.DATE()); // 'TIMESTAMP'
dialect.getDataTypeSql(DataTypes.BOOLEAN()); // 'BOOLEAN'
dialect.getDataTypeSql(DataTypes.UUID()); // 'UUID'
dialect.getDataTypeSql(DataTypes.BLOB()); // 'BINARY'
dialect.getDataTypeSql('SYMBOL'); // 'SYMBOL'
dialect.getDataTypeSql('DATETIME'); // 'TIMESTAMP'
dialect.getDataTypeSql('BIGINT'); // 'LONG'CREATE TABLE / ADD COLUMNbuildQuestColumnSql) emits only "name" TYPE — every constraint field on the column definition (primaryKey, unique, references, allowNull, defaultValue, autoIncrement) is deliberately ignored.dialect.buildCreateTableSQL('quotes', {
id: { type: DataTypes.BIGINT(), primaryKey: true, autoIncrement: true },
symbol_id: {
type: DataTypes.BIGINT(),
unique: true,
references: { table: 'symbols', field: 'id' },
},
});PRIMARY KEY, UNIQUE, FOREIGN KEY, or REFERENCES.addForeignKey() → throws QuestDB does not support FOREIGN KEY constraints.addConstraint() / createConstraint() → throw QuestDB does not support table constraints (PRIMARY KEY / FOREIGN KEY / UNIQUE / CHECK). Use DEDUP UPSERT KEYS on CREATE TABLE for row de-duplication.ON CONFLICT, no RETURNINGbuildInsertQuery() emits a plain parameterized INSERT with $1, $2, ... placeholders. Literal values are inlined rather than parameterized. Any upsert / conflictFields / returning options are ignored.dialect.buildInsertQuery('trades', {
ts: '2024-01-01T00:00:00Z',
ticker: 'AAPL',
price: 100.5,
});buildUpsertQuery() delegates to buildInsertQuery() and emits a plain INSERT (no ON CONFLICT, no DO UPDATE). Conflict resolution comes from the table's DEDUP UPSERT KEYS(...) configuration, not the statement — conflictFields and updateOnDuplicate are ignored.LIMIT, no RETURNINGbuildUpdateQuery() supports UPDATE t SET ... WHERE ... but omits LIMIT and RETURNING even if passed.dialect.buildUpdateQuery('trades', { price: 200 }, { ticker: 'AAPL' }, { limit: 5, returning: true });DELETE. buildDeleteQuery():{ truncate: true } → returns TRUNCATE TABLE "trades".QuestDB does not support row-level DELETE. Remove data by dropping time partitions (ALTER TABLE ... DROP PARTITION ...) or truncating the table (pass { truncate: true }).dialect.buildDeleteQuery('trades', {}, { truncate: true }).sql; // 'TRUNCATE TABLE "trades"'
dialect.buildDeleteQuery('trades', { ticker: 'AAPL' }); // throwsALTER TABLE ... DROP PARTITION ...).OFFSET keywordbuildLimitOffset(limit, offset) uses QuestDB's LIMIT forms (there is no OFFSET keyword):LIMIT nLIMIT lo, hi where lo = offset and hi = offset + limitdialect.buildLimitOffset(10); // ' LIMIT 10'
dialect.buildLimitOffset(10, 20); // ' LIMIT 20, 30'
dialect.buildLimitOffset(undefined, 20); // ''SELECT with { limit: 100, offset: 200 } produces LIMIT 200, 300 (no OFFSET).buildAddColumnSQL() reuses the QuestDB column builder, so NOT NULL / DEFAULT and other constraints are dropped, and SYMBOL options are honored.dialect.buildAddColumnSQL('trades', 'volume', { type: DataTypes.BIGINT(), allowNull: false, defaultValue: 0 });
// ALTER TABLE "trades" ADD COLUMN "volume" LONG
dialect.buildAddColumnSQL('trades', 'ticker', { type: DataTypes.STRING(), symbol: true, symbolCapacity: 128, symbolCache: true });
// ALTER TABLE "trades" ADD COLUMN "ticker" SYMBOL CAPACITY 128 CACHEALTER TABLECREATE INDEX. Indexes exist only on SYMBOL columns, added/removed with ALTER TABLE ... ALTER COLUMN ... ADD/DROP INDEX. addIndex / createIndex iterate each field; the index name and PG-only options (unique/using/expression/where/include) are ignored. For removal, the indexName/columnName argument is treated as the column name.dialect.buildAddIndexSQL('trades', 'ticker'); // ALTER TABLE "trades" ALTER COLUMN "ticker" ADD INDEX
dialect.buildAddIndexSQL('trades', 'ticker', 512); // ALTER TABLE "trades" ALTER COLUMN "ticker" ADD INDEX CAPACITY 512
dialect.buildDropIndexSQL('trades', 'ticker'); // ALTER TABLE "trades" ALTER COLUMN "ticker" DROP INDEXRENAME TABLE, not ALTER TABLE ... RENAME TO.dialect.buildRenameTableSQL('old_trades', 'trades'); // RENAME TABLE "old_trades" TO "trades"questdb.test.ts, questdb-crud.test.ts): buildCreateTableSQL, getDataTypeSql, buildInsertQuery, buildUpsertQuery, buildUpdateQuery, buildDeleteQuery, buildLimitOffset, buildAddColumnSQL, buildAddIndexSQL, buildDropIndexSQL, buildRenameTableSQL, and the throwing constraint methods. dialect.name === 'questdb' and dialect.library === 'pg' are also asserted.PostgresDialect or route through query(), and no test opens a QuestDB connection:connect()/disconnect().PARTITION BY unit, DEDUP key set, WAL/BYPASS WAL combination, or TRUNCATE/ADD INDEX/RENAME TABLE is accepted at runtime).SELECT result mapping beyond the LIMIT form, LISTEN/NOTIFY, schema/extension helpers) as it behaves against QuestDB specifically.WEEK partition unit is accepted by the type but is not covered by a test.PostgresDialect, but the PostgreSQL-only parts of itsSELECT are not QuestDB SQL, so they are rendered natively or refused.lock option is refused:QuestDB has no row-level locking, so `lock` cannot be honoured. Remove the `lock` option; a lock the caller believes it holds but does not is worse than an error.Op.contains as@>, Op.overlap as &&, ?, range operators, @@) are refused with anUnsupportedWhereError, and the statement is not sent:Cannot render WHERE condition at tags.$contains for questdb: the operator '$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.[!NOTE]
Before 4.9.2 this dialect sent PostgreSQL'sFOR SHARE/SKIP LOCKED
clauses and the@>/&&operators unchanged, and the server rejected
them.
QuestDBDialect extends PostgresDialect (postgres) andObject.getOwnPropertyNames(QuestDBDialect.prototype),| Area | Overridden |
|---|---|
| DML builders | buildDeleteQuery, buildInsertQuery, buildLimitOffset, buildSampleByClause, buildSampleBySelect, buildSetWalSQL, buildUpdateQuery, buildUpsertQuery |
| Types | buildQuestColumnSql, canonicalTypeSql, ensureEnumTypes, getDataTypeSql, legacyQuestTypeSql, mapNativeType, rawQuestTypeSql, unsupportedType |
| DDL and schema | addColumn, addConstraint, addForeignKey, addIndex, attachPartitions, buildAddColumnSQL, buildAddIndexSQL, buildCreateTableSQL, buildDropIndexSQL, buildDropPartitionSQL, buildRenameTableSQL, changeColumn, createConstraint, createIndex, createTable, detachPartitions, disableDedup, dropIndex, dropPartition, dropPartitions, enableDedup, removeIndex, renameTable |
| Introspection | describeTable, showConstraints, showIndexes, showTables |
| Other | buildAttachPartitionSQL, buildChangeColumnSQL, buildDetachPartitionSQL, buildDisableDedupSQL, buildEnableDedupSQL, buildLatestOnClause, buildResumeWalSQL, lockClauseSpec, questStringRewrite, resumeWal, setWal, unsupportedWhereOperators |
capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | questdb |
|---|---|
| Transactions atomic | yes |
| Savepoints | no — a nested transaction() throws |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable yes; constraint deferral yes |
| RETURNING | not emitted; returning is accepted and ignored |
| Upsert | a plain INSERT: the engine has no conflict clause (see the page) |
| JSON column | STRING (JSONB → STRING) |
| Vector column | refused — QuestDB has no column type for DataTypes.VECTOR: use ARRAY(DOUBLE), which QuestDB stores as DOUBLE[]. |
| Array column | refused — QuestDB has no column type for DataTypes.ARRAY: QuestDB arrays hold DOUBLE only: ARRAY(DOUBLE). |
| WHERE builder | 46 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family unknown: triggers no; procedures no; functions no; materialized views no; sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN present; createExtension() present |
tests/dialects/questdb.test.ts exercises the dialect with a mocked driver, so nothing touches a server. A live instance: prorm db up --dialect questdb (see Local databases).FindOptions becomes SQL