pg driver and mirrors much of the Postgres dialect's query building. That shared heritage is convenient, but Redshift diverges from Postgres in ways that matter a great deal in practice — this guide focuses on those differences and on the analytical features that have no Postgres equivalent.redshift.Status: Experimental. The dialect implements the full CRUD/DDL surface plus Redshift-specific extras (COPY/UNLOAD, DISTKEY/SORTKEY, datashares, VACUUM/ANALYZE). Some DDL that Redshift accepts but does not enforce (see below) is emitted faithfully rather than silently dropped.
pg driver connects and the SQL looks familiar, but the storage engine, planner, and constraint behavior are all different.CREATE INDEX. Physical layout is governed by table-level DISTSTYLE/DISTKEY (distribution) and SORTKEY (on-disk ordering) instead.PRIMARY KEY, UNIQUE, and FOREIGN KEY DDL is accepted and stored for the planner, but never enforced at write time.COPY FROM S3, and the idiomatic export path is UNLOAD TO S3.| Property | Value |
|---|---|
| Dialect name | redshift |
| Extends | nothing — implements Dialect directly |
| Driver | pg |
| Cloud SDK | none |
| Default port | 5439 |
| Local runtime | none — hosted service only |
$ npm install ts-prorm-orm pg$ pnpm add ts-prorm-orm pg$ yarn add ts-prorm-orm pg$ bun add ts-prorm-orm pgimport { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'redshift',
host: 'my-cluster.abc123.us-east-1.redshift.amazonaws.com',
database: 'dev',
username: 'admin',
password: process.env.DB_PASSWORD,
dialectOptions: {
ssl: { rejectUnauthorized: true },
},
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'redshift',
url: 'redshift://admin:secret@my-cluster.abc123.us-east-1.redshift.amazonaws.com:5439/dev',
dialectOptions: {
ssl: { rejectUnauthorized: true },
},
});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' } });
await User.destroy({ where: { id: 1 } });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.<timestamp>_<random> stands in for it.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 redshift has none. Run the work in a single transaction, or restructure it so nothing nests.
STRING → VARCHAR(255), auto-increment INTEGER → INTEGER, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → VARCHAR(36), JSON → SUPER. The type comparison lines the major engines up side by side.dialect: 'redshift'. The dialect connects over the Postgres wire protocol on Redshift's default port 5439 (not 5432).import { Prorm, DataTypes } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'redshift',
host: 'my-cluster.abc123.us-east-1.redshift.amazonaws.com',
port: 5439,
database: 'analytics',
username: 'admin',
password: process.env.REDSHIFT_PASSWORD,
dialectOptions: {
ssl: { rejectUnauthorized: false },
// Cluster-wide defaults for tables created without an explicit key:
distKey: 'user_id',
sortKey: ['created_at'],
},
});
await prorm.authenticate();max, idleTimeoutMillis, connectionTimeoutMillis, statementTimeout, queryTimeout, ssl) are passed straight through to the pg pool. The distKey / sortKey entries under dialectOptions are Redshift-specific defaults applied to tables created without their own keys.5439 if omitted; the pool defaults to max: 10.queryStream() pages through results with repeated LIMIT/OFFSET queries rather than holding a whole result set in memory — appropriate for warehouse-sized reads.analyze(), and space from deleted/updated rows is reclaimed with vacuum() (see Maintenance).INSERT works for compatibility but is slow at scale; use copyFromS3() for real loads.createTable (typed as RedshiftTableOptions).createTable with key options, copyFromS3, unloadToS3, upsert, vacuum, analyze, ...) live on the dialect instance, which you reach via getDialectInstance(). Some of them (COPY/UNLOAD, datashares) are not part of the generic Dialect interface, so cast the instance to reach them:// The RedshiftDialect type is not re-exported from the package root; cast to
// reach the Redshift-only methods.
const dialect = prorm.getDialectInstance() as any;
await dialect.createTable(
'events',
{
event_id: { type: DataTypes.BIGINT, autoIncrement: true },
user_id: { type: DataTypes.BIGINT },
event_type: { type: DataTypes.STRING, encode: 'ZSTD' }, // column compression
created_at: { type: DataTypes.DATE },
},
{
diststyle: 'KEY', // EVEN | KEY | ALL | AUTO
distKey: 'user_id', // required when diststyle is 'KEY'
sortKey: ['created_at'], // COMPOUND by default
sortkeyType: 'COMPOUND', // or 'INTERLEAVED'
},
);diststyle: 'KEY' requires distKey; 'ALL' replicates the table to every node (good for small dimension tables); 'EVEN' round-robins; 'AUTO' lets Redshift decide.sortKey accepts a single column or an array. sortkeyType is COMPOUND (default) or INTERLEAVED.encode on a column definition ('AZ64', 'ZSTD', 'LZO', 'BYTEDICT', 'DELTA', 'RAW', ...). The value is passed through verbatim, so future encodings work without a dialect update.options.indexes to createTable, or calling createPartitionedTable() / createPartition(), throws a DatabaseError pointing you at DISTKEY/SORTKEY. Redshift has no declarative partitioning.COPY FROM S3, exposed as copyFromS3() (a Redshift-specific method, not part of the generic Dialect interface). Authenticate with either an IAM role ARN (recommended) or static credentials — supplying both, or neither, throws.await dialect.copyFromS3('events', 's3://my-bucket/events/2026-07-05/', {
iamRole: 'arn:aws:iam::123456789012:role/RedshiftCopyRole',
format: 'PARQUET', // CSV (default) | JSON | PARQUET | AVRO | ORC
region: 'us-east-1',
compression: 'GZIP', // GZIP | BZIP2 | ZSTD
maxError: 100, // continue past a threshold of bad rows
});delimiter and ignoreHeader; for JSON/AVRO pass jsonOption (a jsonpaths file path or 'auto'). The extra field appends raw COPY options verbatim as an escape hatch.unloadToS3(), exports the result of a query:await dialect.unloadToS3(
'SELECT user_id, count(*) FROM events GROUP BY user_id',
's3://my-bucket/exports/user_counts/',
{
iamRole: 'arn:aws:iam::123456789012:role/RedshiftUnloadRole',
format: 'PARQUET', // PARQUET | CSV | JSON (default: delimited text)
parallel: true, // set false to force a single output file
manifest: true,
allowOverwrite: true,
},
);bulkInsert() / buildInsertQuery() still work via plain multi-row INSERT for ORM compatibility, but they are not the right tool for large loads.INSERT ... ON CONFLICT, and native MERGE only arrived in 2023. Because of that, buildInsertQuery does not generate ON CONFLICT (calling the internal insert-upsert path throws with guidance), and the default upsert uses AWS's documented staging-table pattern instead:await dialect.upsert('events', {
event_id: 42,
user_id: 7,
event_type: 'click',
}, {
conflictFields: ['event_id'],
});BEGIN/COMMIT:bulkUpsert() builds one staging table fed by a single multi-row INSERT for a whole array of rows.MERGE with useMerge: true (optionally restrict the updated columns with updateColumns):await dialect.upsert('events', row, { conflictFields: ['event_id'], useMerge: true });getDataTypeSql maps prorm DataTypes to Redshift's type system. The mappings that differ from Postgres:prorm DataType | Redshift type | Note |
|---|---|---|
STRING | VARCHAR(255) | length honored |
TEXT | VARCHAR(65535) | Redshift has no unbounded TEXT |
FLOAT | REAL | |
DOUBLE | DOUBLE PRECISION | |
DATE | TIMESTAMP | date+time |
DATEONLY | DATE | |
BLOB | VARBYTE | no BYTEA |
JSON / JSONB | SUPER | semi-structured type |
ARRAY | SUPER | no native array types |
UUID | VARCHAR(36) | no native UUID |
ENUM | VARCHAR(n) | no native ENUM; widened to fit values |
SUPER type is Redshift's answer to JSON and nested/array data. Array JS values are escaped with JSON_PARSE('...') so they round-trip into SUPER columns.IDENTITY(seed, step) clause rather than Postgres SERIAL/BIGSERIAL (Redshift has no SERIAL type and no sequence objects). The column keeps its declared width — a SMALLINT identity stays SMALLINT IDENTITY, it is not silently widened. Seed/step come from an optional identity field on the column definition and default to (1, 1).await dialect.vacuum('events', { mode: 'FULL', boost: true }); // FULL | SORT ONLY | DELETE ONLY | REINDEX
await dialect.analyze('events', { predicateColumns: true }); // or { columns: ['user_id'] }
await dialect.analyzeCompression('events'); // recommends column encodingsvacuum() also accepts toPercent (1–100) to stop once a table is sufficiently sorted. analyze() rejects passing both columns and predicateColumns.READ COMMITTED (default) and SERIALIZABLE. It has no MVCC snapshot mode equivalent to REPEATABLE READ and no dirty-read READ UNCOMMITTED. Requesting either of the unsupported levels is rejected outright (rather than silently downgraded to a different, unrequested guarantee). Transactions may additionally be marked readOnly via RedshiftTransactionOptions.PRIMARY KEY / UNIQUE / FOREIGN KEY DDL is emitted and stored for the planner and for ERD/documentation tooling, but inserts/updates that violate them succeed silently. Enforce integrity in application code. CHECK constraints are not supported at all and throw.SERIAL/sequences. Auto-increment is IDENTITY(seed, step); createSequence() throws.CREATE EXTENSION, no triggers, no row-level-security policies. All throw a clear DatabaseError. (Redshift's answer to external/federated data is Spectrum's CREATE EXTERNAL SCHEMA and federated queries, exposed separately.)SUPER instead of JSON/JSONB, VARBYTE instead of BYTEA, no native TEXT/UUID/ENUM/array types.ON CONFLICT. Upserts use the staging-table pattern (or opt-in native MERGE).vacuum()/analyze() yourself.AUTO REFRESH, BACKUP NO, and their own DISTKEY/SORTKEY), and a limited form of plpgsql stored procedures (CREATE OR REPLACE PROCEDURE ... LANGUAGE plpgsql), all implemented much like Postgres.RedshiftTableOptions, RedshiftColumnDefinition, CopyFromS3Options, UnloadOptions, RedshiftUpsertOptions, and related option interfaces are declared alongside RedshiftDialect in the dialect source.capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | redshift |
|---|---|
| Transactions atomic | yes |
| Savepoints | no — a nested transaction() throws |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only no; deferrable no; constraint deferral no |
| RETURNING | RETURNING on INSERT |
| Upsert | a plain INSERT: the engine has no conflict clause (see the page) |
| JSON column | SUPER (JSONB → SUPER) |
| Vector column | refused — redshift has no column type for DataTypes.VECTOR: Redshift has no vector type. |
| Array column | SUPER |
| WHERE builder | 35 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family postgres: triggers yes; procedures yes; functions multiStatement; materialized views native; sequences yes; enums named; RLS policy |
| Streaming | queryStream() present; LISTEN absent; createExtension() present |
tests/dialects/redshift.test.ts exercises the dialect with a mocked driver, so nothing touches a server.FindOptions becomes SQL