Amazon Redshift Data API Dialect
Overview#
The Redshift Data API
runs SQL against a provisioned Redshift cluster or a Redshift Serverless
workgroup over HTTPS, with no driver, no connection and no VPC route to the
cluster. The grammar is Redshift's, so RedshiftDataDialect extends
RedshiftDialect and inherits every builder: DISTKEY/SORTKEY DDL,
SUPER, IDENTITY, COPY/UNLOAD, datashares and the staging-table upsert.
Only the transport is replaced.| Property | Value |
|---|
| Dialect name | redshiftdata |
| Extends | RedshiftDialect |
| Library | @aws-sdk/client-redshift-data (optional peer) |
| Default port | none — the SDK calls the regional HTTPS endpoint |
| Driver | @aws-sdk/client-redshift-data |
| Cloud SDK | @aws-sdk/client-redshift-data |
| Local runtime | none — hosted service only |
The inherited 5439 (and host, port, username, password, ssl, pool
settings) are ignored.The SDK is loaded on connect(); importing or constructing the dialect loads
nothing. If it is missing, connect() says which package to install.Quick start#
Install#
$ npm install ts-prorm-orm @aws-sdk/client-redshift-data
$ pnpm add ts-prorm-orm @aws-sdk/client-redshift-data
$ yarn add ts-prorm-orm @aws-sdk/client-redshift-data
$ bun add ts-prorm-orm @aws-sdk/client-redshift-data
Connect#
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'redshiftdata',
database: 'dev',
dialectOptions: {
workgroupName: 'analytics', // or clusterIdentifier
region: 'us-east-1',
},
});
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 statements
upsert() also reads the row by primary key before and after the write, to return the instance.The staging table's name is generated per call; <timestamp>_<random> stands in for it.await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});
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 redshiftdata has none. Run the work in a single transaction, or restructure it so nothing nests.
[!CAUTION] Not atomic
Each statement is its own request: a failure part-way through leaves the earlier statements applied. transaction({ requireAtomic: true }) refuses this dialect instead.
Type mapping for the portable descriptors: 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.Connection#
const db = new Prorm({
dialect: 'redshiftdata',
database: 'dev',
dialectOptions: {
workgroupName: 'analytics', // or clusterIdentifier: 'my-cluster'
secretArn: 'arn:aws:secretsmanager:…', // optional, see below
region: 'us-east-1',
},
});
connect() validates the options and builds a RedshiftDataClient; it makes
no call (the cheapest probe would be a billed statement). Pass client to use
your own client (anything with send(command)), credentials / endpoint
for the SDK.Authentication#
Each method is a documented combination of request fields. database is
always required.| Target | Fields | Method |
|---|
| Provisioned cluster | clusterIdentifier, secretArn | Secrets Manager (secret holds username/password) |
| Provisioned cluster | clusterIdentifier, dbUser | Temporary credentials — redshift:GetClusterCredentials |
| Provisioned cluster | clusterIdentifier | IAM identity (IAM:<name>) — redshift:GetClusterCredentialsWithIAM |
| Serverless workgroup | workgroupName, secretArn | Secrets Manager |
| Serverless workgroup | workgroupName | IAM identity — redshift-serverless:GetCredentials |
Refused before any call: no database; neither or both of
clusterIdentifier and workgroupName; dbUser together with secretArn;
dbUser with a workgroup (AWS documents it for clusters only).
resolveTarget() returns the fields and the method without touching the SDK.How a statement runs#
1.
ExecuteStatement (or BatchExecuteStatement) returns an Id.
2.
DescribeStatement is polled — 100 ms, backing off ×1.5 to
maxPollIntervalMs (2 s) — until FINISHED, FAILED or ABORTED.
waitTimeSeconds (1–30) turns on server-side long polling.
3.
If HasResultSet, GetStatementResult is paged with NextToken;
otherwise rowCount is ResultRows (-1 becomes 0).
FAILED/ABORTED throw RedshiftDataError with the server's message and the
statementId. After queryTimeoutMs (default 5 minutes, at most the Data
API's 24-hour query limit) the statement is stopped with CancelStatement and
the call throws — an abandoned statement would otherwise keep running.Values come back from the Field union: isNull → null, longValue,
doubleValue, booleanValue, stringValue as-is, blobValue → Buffer.
DECIMAL, dates and timestamps arrive as strings (the Data API's own mapping).
longValue is a JavaScript number, so BIGINT values beyond 2^53 lose
precision — cast to VARCHAR in SQL if that matters. Results are fetched as
JSON; the CSV format (GetStatementResultV2) is not used.queryStream() pages with LIMIT/OFFSET over query() (inherited).Values and parameters#
Builder values ($1, $2, …) are inlined by the inherited
replaceReplacements(), exactly as the pg-based Redshift dialect sends
them. The Data API's own named parameters are not used for them because they
are strings only, cannot be SQL NULL (AWS: the API "interprets it as the
literal string NULL"), cannot be empty, and in a batch every parameter must
be used by at least one statement.For hand-written :name SQL you can still send Data API parameters:await db.dialect.query('SELECT * FROM sales WHERE region = :region', {
parameters: { region: 'EMEA' },
});
null/undefined and empty values are refused before a request is spent, as
is combining parameters with replacements.Transactions: batch() is the atomic path#
The Data API has no interactive transactions. Its atomic unit is
BatchExecuteStatement: up to 40 statements, run serially, by default "as
a single transaction. If any SQL statement fails, all work is rolled back."const results = await dialect.batch([
dialect.buildInsertQuery('orders', { id: 1, total: 10 }),
dialect.buildUpdateQuery('stock', { qty: 4 }, { sku: 'A' }),
{ sql: 'SELECT count(*) AS n FROM orders' },
]);
Takes the { sql, values } pairs the builders return; returns one
QueryResult per statement, read from the sub-statement ids <id>:1,
<id>:2, ….
executionMode: 'AUTO_COMMIT' commits each statement separately.
parameters are shared by all statements.
More than 40 statements is refused (split, knowing each batch is its own
transaction, or load with COPY).
upsert() and bulkUpsert() send the inherited staging-table sequence
(temp table, INSERT, DELETE, INSERT) or MERGE as one batch, so it keeps
its single session and single transaction.startTransaction() behaves as in the rqlite dialect: it returns a
bookkeeping shell whose statements run (and commit) immediately, so code
written against the transaction API keeps working; rollback() logs a warning
that nothing was undone. Savepoints are refused.Limits (verified against the AWS documentation)#
| Limit | Value | Enforced here |
|---|
Statements per BatchExecuteStatement | 1–40 | yes |
| Query statement size | 200 KB | yes (UTF-8 bytes, 200 × 1024) |
| Query duration | 24 hours | queryTimeoutMs ≤ 24 h |
| Result size | 500 MB after gzip | server |
| Result retention | 24 hours | server |
Active (SUBMITTED/STARTED) queries | 500 per cluster | server |
WaitTimeSeconds | 1–30 | yes |
Exported as REDSHIFT_DATA_MAX_BATCH_STATEMENTS,
REDSHIFT_DATA_MAX_STATEMENT_BYTES, REDSHIFT_DATA_MAX_QUERY_DURATION_MS,
REDSHIFT_DATA_MAX_RESULT_BYTES, REDSHIFT_DATA_RESULT_RETENTION_MS,
REDSHIFT_DATA_MAX_WAIT_TIME_SECONDS, REDSHIFT_DATA_MAX_SESSION_KEEP_ALIVE_SECONDS.Not used#
Session reuse (SessionKeepAliveSeconds, SessionId, up to 24 hours,
one query at a time per session) exists, but this dialect does not build
interactive transactions on it: every statement is independent.
ResultFormat: 'CSV' / GetStatementResultV2, ClientToken (the SDK
generates one for retries), IAM Identity Center.
References#
What differs from Redshift#
RedshiftDataDialect extends RedshiftDialect (redshift) and
overrides 23 of its methods (Object.getOwnPropertyNames(RedshiftDataDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.| Area | Overridden |
|---|
| Connection and execution | commitTransaction, connect, disconnect, getConnection, isConnected, query, rollbackTransaction, startTransaction |
| DDL and schema | createSavepointSQL |
| Introspection | assertStatementSize, assertTimings, resolveTarget |
| Other | batch, bulkUpsert, client, command, commonInput, requestTarget, resultOf, runAsBatch, toParameters, upsert, waitFor |
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 | redshiftdata |
|---|
| Transactions atomic | no — each statement is its own request; requireAtomic refuses it |
| 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 — redshiftdata 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 |
Testing#
tests/dialects/redshiftdata.test.ts exercises the dialect with a mocked driver, so nothing touches a server.See also#
redshift — the parent dialect this one extends Modified at 2026-10-04 00:40:16