Overview#
| Property | Value |
|---|
| Dialect name | trino |
| Extends | PostgresDialect (postgres) |
| Driver | pg |
| Cloud SDK | none |
| Default port | 8080 |
| Local runtime | self-hosted; no image in this repo |
Trino (formerly PrestoSQL) is a distributed, federated ANSI-SQL query engine. It is not a storage engine of its own — it runs SQL over pluggable connectors (Hive, Iceberg, MySQL, PostgreSQL, Kafka, …).Because its SQL surface is ANSI-flavored and it quotes identifiers with double quotes (like PostgreSQL), TrinoDialect extends PostgresDialect and overrides only where Trino differs:export class TrinoDialect extends PostgresDialect {
readonly name = 'trino';
// ...
}
Key defaults and differences (all implemented in the source):Default port 8080 — Trino's HTTP/query port, not PostgreSQL's 5432. Injected by the constructor via super({ port: 8080, ...config }).
Three-part catalog.schema.table naming when a catalog is configured.
Connector table properties via CREATE TABLE ... WITH (...).
CREATE TABLE AS SELECT (CTAS) as the primary way to materialize a table.
No key constraints (PRIMARY KEY / FOREIGN KEY / UNIQUE / DEFAULT), no identity/SERIAL, no upsert, no indexes.
Quick start#
Install#
$ npm install ts-prorm-orm pg
$ pnpm add ts-prorm-orm pg
$ yarn add ts-prorm-orm pg
$ bun add ts-prorm-orm pg
pg is the same driver the postgres dialect uses. pg-copy-streams is optional: only the features that need it load it.[!CAUTION] A SQL-generation layer
The driver the catalog lists, pg, is the one the parent dialect loads; it does not speak Trino's own wire protocol, so connecting to a live server is unverified. The builders — everything under Generated SQL — are what this dialect is for. See Not-yet-verified.
Connect#
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'trino',
host: 'trino.internal',
username: 'analyst',
dialectOptions: {
catalog: 'hive',
schema: 'analytics',
},
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'trino',
url: 'trino://analyst@trino.internal:8080/hive',
dialectOptions: {
catalog: 'hive',
schema: 'analytics',
},
});
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' });
MERGE statement
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 trino has none. Run the work in a single transaction, or restructure it so nothing nests.
Table sampling#
sample on the select builder reads a random fraction of the table, for estimates over large tables:const sample = db.getDialectInstance()!.buildSelectQuery({
tableName: 'users',
sample: { method: 'BERNOULLI', percent: 10 },
});
await db.query(sample.sql);
Type mapping for the portable descriptors: STRING → VARCHAR, auto-increment INTEGER → INTEGER, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → UUID, JSON → JSON. The type comparison lines the major engines up side by side.Connection#
Trino-specific options extend the PostgreSQL options with a connector catalog and a default schema:export interface TrinoDialectOptions extends PostgresDialectOptions {
/** Connector catalog (e.g. 'hive', 'iceberg'). Enables catalog.schema.table naming. */
catalog?: string;
/** Default schema used when a query/table doesn't specify one. */
schema?: string;
}
When constructing through Prorm, dialect-specific extras (like catalog and schema) pass through dialectOptions:import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'trino',
host: 'trino.internal',
port: 8080, // optional; the dialect defaults to 8080
username: 'analyst',
database: 'hive', // PostgreSQL-style connection field
dialectOptions: {
catalog: 'hive', // enables "catalog"."schema"."table" naming
schema: 'analytics', // default schema when a query omits one
},
});
The catalog option#
A Trino table lives in a connector catalog and a schema. catalog is what turns on Trino's fully-qualified three-part naming:With a catalog configured, quoteTable emits "catalog"."schema"."table" (schema included only when known).
Without a catalog, it falls back to PostgreSQL's schema.table behavior (a bare "table" when no schema is given).
quoteTable(tableName: string, schema?: string): string {
if (this.trinoCatalog) {
const effectiveSchema = schema ?? this.trinoSchema;
const parts = [this.escapeId(this.trinoCatalog)];
if (effectiveSchema) {
parts.push(this.escapeId(effectiveSchema));
}
parts.push(this.escapeId(tableName));
return parts.join('.');
}
return super.quoteTable(tableName, schema);
}
Examples (from the tests):new TrinoDialect({ catalog: 'iceberg', schema: 'default' })
.quoteTable('t', 'staging'); // => "iceberg"."staging"."t"
new TrinoDialect({}).quoteTable('events'); // => "events" (no catalog -> bare name)
A per-call schema argument overrides the configured default schema.3. Trino-specific behavior#
catalog.schema.table naming#
Every DDL/CRUD builder qualifies the target table through quoteTable, so once a catalog is set, statements target "catalog"."schema"."table":Connector table properties — WITH (...)#
buildCreateTableSQL appends a connector-properties clause built by buildTablePropertiesClause, which handles format, partitioning, and arbitrary tableProperties:export interface TrinoTableOptions extends TableOptions {
format?: string; // -> format = '...'
partitioning?: string[]; // -> partitioning = ARRAY['...']
tableProperties?: Record<string, string | number | boolean>;
schema?: string;
}
Building a table with a file format and partitioning:d.buildCreateTableSQL(
't',
{ id: { type: 'BIGINT' }, day: { type: 'VARCHAR' } },
{ format: 'ORC', partitioning: ['day'] },
);
Arbitrary tableProperties — string/number/boolean rendering#
buildTablePropertiesClause renders each tableProperties entry as key = value, single-quoting strings (escaping embedded quotes) and emitting numbers and booleans raw:if (typeof value === 'string') {
props.push(`${key} = '${value.replace(/'/g, "''")}'`);
} else {
props.push(`${key} = ${value}`);
}
d.buildCreateTableSQL(
't',
{ id: { type: 'BIGINT' } },
{ tableProperties: { bucket_count: 8, transactional: true, sorted_by: 'id' } },
);
renders the properties as:(format and partitioning, when present, are emitted first; tableProperties follow in object order.)CREATE TABLE AS SELECT (CTAS)#
buildCreateTableAsSelectSQL materializes a table from a raw SELECT. It supports ifNotExists, comment, the same WITH (...) connector properties, and a trailing WITH NO DATA (create structure only, copy no rows):export interface TrinoCTASOptions extends TrinoTableOptions {
withNoData?: boolean; // emit WITH NO DATA
}
d.buildCreateTableAsSelectSQL('summary', 'SELECT 1 AS n', {
format: 'PARQUET',
withNoData: true,
});
createTableAsSelect(tableName, selectQuery, options) is the async wrapper that executes the built statement.Trino orders OFFSET before LIMIT (unlike PostgreSQL's LIMIT n OFFSET m) and allows a standalone OFFSET:d.buildLimitOffset(10, 20); // ' OFFSET 20 LIMIT 10'
d.buildLimitOffset(undefined, 5); // ' OFFSET 5'
d.buildLimitOffset(10); // ' LIMIT 10'
d.buildLimitOffset(); // ''
4. Type mapping#
getDataTypeSql maps ORM types to Trino spellings. The cross-dialect DATETIME pseudo-type (emitted by the model-sync layer for timestamp columns) is rewritten to TIMESTAMP, preserving any precision.| ORM type / key | Trino SQL | Notes |
|---|
STRING | VARCHAR | Unbounded — no length emitted |
TEXT | VARCHAR | Unbounded |
CHAR | CHAR(n) / CHAR | Length used when present |
INTEGER | INTEGER | No SERIAL; autoIncrement ignored |
BIGINT | BIGINT | |
FLOAT | REAL | |
DOUBLE | DOUBLE | Not DOUBLE PRECISION |
DECIMAL | DECIMAL(precision, scale) | Defaults DECIMAL(10, 0) |
BOOLEAN | BOOLEAN | |
DATE | TIMESTAMP | Mapped to timestamp |
DATEONLY | DATE | |
TIME | TIME | |
BLOB | VARBINARY | |
JSON | JSON | |
JSONB | JSON | |
UUID | UUID | |
ARRAY | ARRAY(elementType) | Falls back to ARRAY(VARCHAR) when the element type is unknown |
'DATETIME' / 'DATETIME(n)' (string pseudo-type) | TIMESTAMP / TIMESTAMP(n) | Precision preserved |
| unknown object / non-object | VARCHAR | Default fallback |
A raw string data type that is not DATETIME is passed through unchanged.Verified assertions from the tests:d.getDataTypeSql({ key: 'STRING' }); // 'VARCHAR'
d.getDataTypeSql({ key: 'TEXT' }); // 'VARCHAR'
d.getDataTypeSql({ key: 'FLOAT' }); // 'REAL'
d.getDataTypeSql({ key: 'DOUBLE' }); // 'DOUBLE'
d.getDataTypeSql({ key: 'BOOLEAN' }); // 'BOOLEAN'
d.getDataTypeSql({ key: 'BIGINT' }); // 'BIGINT'
d.getDataTypeSql({ key: 'DATE' }); // 'TIMESTAMP'
d.getDataTypeSql({ key: 'DATEONLY' }); // 'DATE'
d.getDataTypeSql({ key: 'JSON' }); // 'JSON'
d.getDataTypeSql({ key: 'JSONB' }); // 'JSON'
d.getDataTypeSql({ key: 'BLOB' }); // 'VARBINARY'
d.getDataTypeSql({ key: 'ARRAY', type: { key: 'BIGINT' }});// 'ARRAY(BIGINT)'
d.getDataTypeSql({ key: 'ARRAY' }); // 'ARRAY(VARCHAR)'
d.getDataTypeSql('DATETIME'); // 'TIMESTAMP'
d.getDataTypeSql('DATETIME(6)'); // 'TIMESTAMP(6)'
5. Unsupported operations#
Trino neither supports nor enforces key constraints, identity columns, upserts, or indexes. The dialect handles each explicitly.Key constraints — omitted (not thrown)#
Column definitions are built by getTrinoColumnDefinitionSql, which emits only the name, type, an optional NOT NULL, and an optional COMMENT. It silently ignores primaryKey, unique, references (FOREIGN KEY), defaultValue, and autoIncrement:protected getTrinoColumnDefinitionSql(columnName, definition): string {
let sql = `${this.escapeId(columnName)} ${this.getDataTypeSql(definition.type)}`;
if (definition.allowNull === false) sql += ' NOT NULL';
if (definition.comment) sql += ` COMMENT '...'`;
return sql;
}
So a table declared with PK/FK/UNIQUE/DEFAULT metadata still produces constraint-free DDL:d.buildCreateTableSQL('t', {
id: { type: 'BIGINT', primaryKey: true, autoIncrement: true },
orgId: { type: 'BIGINT', references: { table: 'orgs', field: 'id' } },
name: { type: 'VARCHAR', unique: true, defaultValue: 'x' },
});
// Output contains NONE of: PRIMARY KEY, FOREIGN KEY, REFERENCES, UNIQUE, SERIAL, DEFAULT
NOT NULL and IF NOT EXISTS are honored. ADD COLUMN uses the same builder, so added columns also carry no constraints:No identity / SERIAL#
INTEGER maps to INTEGER and the autoIncrement flag is ignored — Trino has no SERIAL/BIGSERIAL/IDENTITY. Surrogate key values must be supplied by the application or source connector.No upsert / ON CONFLICT — throws#
Both buildInsertQuery (with options.upsert) and buildUpsertQuery throw rather than emitting invalid INSERT ... ON CONFLICT:plain.buildInsertQuery('users', { id: 1 }, { upsert: true, conflictFields: ['id'] });
// throws: "Trino does not support upsert / ON CONFLICT. ..."
plain.buildUpsertQuery('users', { id: 1 }, { conflictFields: ['id'] });
// throws: "Trino does not support upsert / ON CONFLICT. ..."
The error message suggests using INSERT, or MERGE (on connectors that support it) via raw SQL.No RETURNING, no LIMIT on write statements#
INSERT, UPDATE, and DELETE never emit RETURNING or LIMIT, even when those options are passed:plain.buildInsertQuery('users', { id: 1, name: 'ann' });
// INSERT INTO "users" ("id", "name") VALUES ($1, $2) (no RETURNING)
cataloged.buildUpdateQuery('users', { name: 'bob' }, { id: 5 }, { returning: true, limit: 10 });
// UPDATE "hive"."sales"."users" SET "name" = $1 WHERE ... (no RETURNING, no LIMIT)
cataloged.buildDeleteQuery('users', { id: 5 }, { returning: true, limit: 3 });
// DELETE FROM "hive"."sales"."users" WHERE ... (no RETURNING, no LIMIT)
No TRUNCATE — degrades to DELETE#
Trino has no TRUNCATE; a truncate request becomes an unqualified DELETE FROM:plain.buildDeleteQuery('users', {}, { truncate: true });
// DELETE FROM "users"
No CASCADE on DROP TABLE#
dropTable supports IF EXISTS but never emits CASCADE (there are no dependent constraints):plain.dropTable('users', { ifExists: true, cascade: true });
// DROP TABLE IF EXISTS "users"
No indexes — throws#
Trino has no CREATE INDEX; data layout is controlled by connector table properties (partitioning, bucketed_by, …). Every index entry point throws via noIndexes(...):await plain.addIndex('t', 'i', ['c']);
// throws: "Trino does not support indexes (addIndex is unavailable). ..."
6. Not-yet-verified#
The Trino test suites run without a database connection — they assert only on generated SQL strings and thrown errors. The following are covered by the source and tests:SQL-generation verified (no live Trino):catalog.schema.table naming and per-call schema override (quoteTable).
CREATE TABLE with WITH (...) (format, partitioning, arbitrary tableProperties — string/number/boolean rendering).
Omission of PRIMARY KEY / FOREIGN KEY / UNIQUE / DEFAULT / SERIAL; honoring of NOT NULL and IF NOT EXISTS.
CREATE TABLE AS SELECT including WITH NO DATA.
The full type-mapping table above.
INSERT / UPDATE / DELETE SQL (catalog-qualified, no RETURNING/LIMIT), truncate → DELETE FROM, DROP TABLE without CASCADE, ADD/DROP COLUMN.
Pagination ordering (OFFSET before LIMIT, standalone OFFSET).
Upsert and all index operations throwing.
Needs a live Trino cluster (not exercised here):Actual connection/handshake over port 8080 (the dialect inherits PostgreSQL's pg-based connection machinery, which is not the Trino wire protocol — real connectivity is unverified by these tests).
Runtime execution of any statement (createTable, createTableAsSelect, addColumn, removeColumn, dropTable, and CRUD): UPDATE/DELETE support in particular is connector-dependent at execution time.
Whether specific WITH (...) properties, file formats, and partitioning schemes are accepted by a given connector (Hive, Iceberg, …).
MERGE-based upsert on connectors that support it (suggested by the error message but not implemented by the dialect).
Row locks and PostgreSQL-only operators#
The dialect extends PostgresDialect, but the PostgreSQL-only parts of its
SELECT are not Trino SQL, so they are rendered natively or refused.Trino has no row-level locking, so any lock option is refused:Trino 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.
The PostgreSQL JSON, array, range and full-text operators (Op.contains as
@>, Op.overlap as &&, ?, range operators, @@) are refused with an
UnsupportedWhereError, and the statement is not sent:Cannot render WHERE condition at tags.$contains for trino: 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's FOR SHARE / SKIP LOCKED
clauses and the @> / && operators unchanged, and the server rejected
them.
What differs from Postgres#
TrinoDialect extends PostgresDialect (postgres) and
overrides 45 of its methods (Object.getOwnPropertyNames(TrinoDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.| Area | Overridden |
|---|
| DML builders | buildDeleteQuery, buildInsertQuery, buildLimitOffset, buildUpdateQuery, buildUpsertMergeQuery, buildUpsertQuery, quoteTable |
| Types | binaryTypeSql, canonicalTypeSql, characterTypeSql, collectionTypeSql, documentTypeSql, enumTypeSql, extensionTypeSql, getTrinoColumnDefinitionSql, integerTypeSql, legacyDataTypeSql, legacyInvalidTypeNames, legacyTypeReplaced, namedEnumTypes, networkTypeSql, numericTypeSql, rangeTypeSql, spatialTypeSql, temporalTypeSql, unknownTypeSql, vectorTypeSql |
| DDL and schema | addColumn, addIndex, buildCreateTableAsSelectSQL, buildCreateTableSQL, buildTablePropertiesClause, createExpressionIndex, createIndex, createPartialIndex, createTable, createTableAsSelect, dropIndex, dropTable, removeColumn, removeIndex |
| Introspection | resolveCtasQuery |
| Other | lockClauseSpec, noIndexes, unsupportedWhereOperators |
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 | trino |
|---|
| 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 | MERGE INTO … WHEN MATCHED / WHEN NOT MATCHED |
| JSON column | JSON (JSONB → JSON) |
| Vector column | ARRAY(REAL) |
| Array column | ARRAY(INTEGER) |
| 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 |
Testing#
tests/dialects/trino.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