Reference for the teradata SQL dialect (Teradata Vantage).
Source: teradata
Tests: teradata
Teradata Vantage is not a PostgreSQL or MySQL fork — its own proprietary
engine, wire protocol and SQL dialect. But the shape of that SQL where the
ORM spends most of its time (double-quoted identifiers, '...' strings with
doubled apostrophes, WHERE/GROUP BY/HAVING/JOIN as PostgreSQL's
builders already emit them, and a MERGE statement for upserts) is ANSI and
matches PostgreSQL closely enough that TeradataDialect extends
PostgresDialect and reuses its where/order/join/CTE builders — every value
still leaves those builders as a $n placeholder with its value beside it.
Nothing about the wire is PostgreSQL's, though, so the whole connection
layer, escaping, data types, pagination, table DDL and every non-relational
PostgreSQL feature is replaced or refused.
| Property | Value |
|---|---|
| Dialect name | teradata |
| Extends | PostgresDialect |
| Driver | teradatasql (official Teradata SQL Driver for Node.js) |
| Default port | 1025 (dbs_port) |
Registered with Prorm | Yes — SqlDialect.Teradata = 'teradata', lazily loaded from ./teradata as TeradataDialect (src/dialects/loaders.ts) |
| Cloud SDK | none |
| Local runtime | none — hosted service only |
$ npm install ts-prorm-orm teradatasql
$ pnpm add ts-prorm-orm teradatasql
$ yarn add ts-prorm-orm teradatasql
$ bun add ts-prorm-orm teradatasql
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'teradata',
host: 'td.example.com',
database: 'sales',
username: 'dbc',
password: process.env.TD_PASSWORD,
});
import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'teradata',
url: 'teradata://dbc:secret@td.example.com:1025/sales',
});
A URL is folded into 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 below
[!WARNING]
db.sync()cannot create this table
Teradata does not support CREATE TABLE IF NOT EXISTS. Check DBC.TablesV (or catch error 3803, "table already exists") before creating.
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,
});
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.
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 } });
-- insert
INSERT INTO "users" ("email", "name") VALUES ('ada@example.com', 'Ada');
-- update
UPDATE "users" SET "name" = 'Grace', "updatedAt" = TIMESTAMP '2026-09-27 05:01:42.230000' WHERE "email" = 'ada@example.com';
-- delete
DELETE FROM "users" WHERE "id" = 1;
await User.upsert({ id: 1, email: 'ada@example.com', name: 'Ada Lovelace' });
MERGE statementMERGE INTO "users" AS tgt USING (SELECT 1 AS "id", 'ada@example.com' AS "email", 'Ada Lovelace' AS "name") AS src ON tgt."id" = src."id" WHEN MATCHED THEN UPDATE SET "email" = src."email", "name" = src."name" WHEN NOT MATCHED THEN INSERT ("id", "email", "name") VALUES (src."id", src."email", src."name");
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,
});
SELECT * FROM "users" WHERE "name" LIKE 'A%' QUALIFY ROW_NUMBER() OVER (ORDER BY "id" ASC) BETWEEN 41 AND 60 ORDER BY "id" ASC;
[!NOTE] No JSON operator in the WHERE builder
DataTypes.JSON()maps toJSON, but this dialect's WHERE builder refusesOp.containson it: Teradata does not support the @> / <@ containment operators. The condition used a PostgreSQL-only operator. On Teradata, LIKE on a NOT CASESPECIFIC column is already case-insensitive; JSON values are read with JSONExtractValue.
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 });
});
INSERT INTO "users" ("email", "name") VALUES ('grace@example.com', 'Grace');
UPDATE "users" SET "name" = 'Grace Hopper', "updatedAt" = TIMESTAMP '2026-09-27 05:01:42.230000' WHERE "email" = 'grace@example.com';
-- driver: commit() (teradatasql)
[!WARNING] No savepoints
Nested transactions need savepoints, and teradata has none. Run the work in a single transaction, or restructure it so nothing nests.
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);
SELECT * FROM "users" SAMPLE 0.1;
Type mapping for the portable descriptors: STRING → VARCHAR(255), auto-increment INTEGER → INTEGER, BOOLEAN → BYTEINT, DATE → TIMESTAMP(6), UUID → CHAR(36), JSON → JSON. The type comparison lines the major engines up side by side.
import { Prorm, SqlDialect } from 'ts-prorm-orm';
const orm = new Prorm({
dialect: SqlDialect.Teradata, // or the string 'teradata'
host: 'td.example',
// port defaults to 1025
username: 'dbc',
password: process.env.TD_PASSWORD,
database: 'sales',
dialectOptions: { tmode: 'ANSI', logmech: 'LDAP' },
});
Or construct the dialect directly:
const d = new Prorm({
dialect: 'teradata',
host: 'td.example',
username: 'dbc',
});
await d.connect();
| Option | Default | Effect |
|---|---|---|
host | 'localhost' | driver host |
port | 1025 | driver dbs_port |
username | — | driver user |
password | — | driver password |
database | — | driver database |
tmode | — (database default) | session mode: 'TERA', 'ANSI', or 'DEFAULT' |
logmech | — | logon mechanism ('TD2', 'LDAP', 'JWT', 'BROWSER', ...) |
encryptdata | — | encrypt data in transit (stringified) |
connectParams | — | any further teradatasql connection parameters, passed through verbatim (every value stringified) |
The teradatasql module is loaded lazily with require() on the first
connect(); if it is not installed, connect() throws with an install
hint (npm install teradatasql, 64-bit Node.js 18.20+).
The driver follows the DB-API shape: connect(params) → connection,
con.cursor() → cursor, cur.execute(sql, params) with ? markers,
cur.fetchall() → row arrays, cur.description → column names. query()
rewrites the builders' $n (or :name) placeholders outside string literals
to ? markers and hands the values to the driver as real bind parameters —
nothing is inlined on the wire:
const { sql, values } = d.buildSelectQuery({ tableName: 'users', where: { name: "O'Brien", active: true } });
await d.query(sql, { replacements: values });
// executed: 'SELECT * FROM "users" WHERE "name" = ? AND "active" = ?', params: ["O'Brien", 1]
getDatabaseVersion() reads DBC.DBCInfoV (InfoKey = 'VERSION').
TeradataDialect extends PostgresDialect (postgres) and
overrides 109 of its methods (Object.getOwnPropertyNames(TeradataDialect.prototype),
computed by the scratch test); everything else is inherited unchanged.
| Area | Overridden |
|---|---|
| Connection and execution | commitTransaction, connect, disconnect, getConnection, isConnected, listen, notify, query, queryStream, rollbackTransaction, startTransaction, unlisten |
| DML builders | buildDeleteQuery, buildInsertQuery, buildLimitOffset, buildMergeSql, buildSampleClause, buildSelectQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, bulkInsert, escape, quoteTable, replaceReplacements |
| Types | buildColumnSql, canonicalTypeSql, createEnumType, ensureEnumTypes, getDataTypeSql, mapNativeType, unsupportedType |
| DDL and schema | addColumn, addIndex, attachPartition, buildCreateDatabaseSql, buildCreateIndexSql, buildCreateTableSql, changeColumn, changeOwner, createDatabaseSQL, createDomain, createExtension, createForeignDataWrapper, createForeignServer, createForeignTable, createFulltextIndex, createGINIndex, createGISTIndex, createIndex, createMaterializedView, createPartition, createPartitionedTable, createPolicy, createProcedure, createSavepointSQL, createSchema, createSequence, createSpatialIndex, createStoredProcedure, createTable, createTrigger, createVectorIndex, createView, detachPartition, dropDatabaseSQL, dropExtension, dropIndex, dropSchema, dropSequence, dropTable, enableRLS, refreshMaterializedView, removeColumn, removeIndex, renameColumn, renameTable |
| Introspection | assertIndexOptions, assertNoReturning, describeTable, getDatabaseVersion, getExtensions, hasExtension, hasPolicy, hasSequence, listSchemas, listSequences, showAllSchemas, showTables, showViews |
| Other | buildConnectParams, buildRangePartitionSql, columnNames, connectionFor, copyFrom, copyTo, currSequenceValue, formatDefault, mainSelectInsertIndex, nextSequenceValue, openConnection, pgAdvisoryLock, pgTryAdvisoryLock, rangeBound, refuse, releaseSavepointSQL, rollbackToSavepointSQL, toDriverValue, toWire |
Values compiled to text (not bound) are escaped with Teradata literal rules —
booleans are 1/0 (no BOOLEAN type), dates are typed TIMESTAMP '...'
literals in UTC, and bytes are a '...'XB hex literal:
d.escape("it's"); // 'it''s'
d.escape(true); // 1
d.escape(new Date(Date.UTC(2024, 4, 6, 7, 8, 9, 10)));
// TIMESTAMP '2024-05-06 07:08:09.010000'
d.escape(Buffer.from([0xde, 0xad])); // 'DEAD'XB
d.escape([1, 2]); // throws: array literals
| ORM | Teradata | Note |
|---|---|---|
STRING(n) | VARCHAR(n) | |
TEXT / CITEXT | CLOB | not comparable/indexable; use STRING for searchable text |
CHAR(n) | CHAR(n) | |
TINYINT | BYTEINT | |
SMALLINT / MEDIUMINT / INTEGER | SMALLINT / INTEGER / INTEGER | |
BIGINT | BIGINT | |
FLOAT / DOUBLE / REAL | FLOAT | Teradata's FLOAT is an 8-byte double |
DECIMAL / NUMERIC | DECIMAL(p,s) | p ≤ 38, else throws |
BOOLEAN | BYTEINT | no BOOLEAN type; 0/1 |
DATE | TIMESTAMP(n) | precision defaults to 6 |
DATEONLY | DATE | |
TIME | TIME(n) | precision defaults to 6 |
UUID | CHAR(36) | |
JSON / JSONB | JSON | native JSON type (16.x+) |
BLOB | BLOB | |
ENUM | VARCHAR(longest value) | no enum types |
GEOMETRY | ST_Geometry | |
'DATETIME' / 'BOOLEAN' (string types) | TIMESTAMP / BYTEINT | pseudo-type strings are rewritten |
Types with no Teradata counterpart (ARRAY, HSTORE, ...) throw naming the
type rather than emit something Teradata rejects.
buildCreateTableSql is pure and synchronous, and covers Teradata's table
kinds, distribution and partitioning:
d.buildCreateTableSql('sales', {
id: { type: { key: 'BIGINT' }, allowNull: false },
d: { type: { key: 'DATEONLY' }, allowNull: false },
}, {
tableKind: 'SET',
fallback: false,
primaryIndex: ['id'],
uniquePrimaryIndex: true,
partitionByRange: {
column: 'd', start: '2024-01-01', end: new Date(Date.UTC(2024, 11, 31)),
each: { value: 1, unit: 'MONTH' }, noRange: true, unknown: true,
},
});
CREATE SET TABLE "sales", NO FALLBACK ("id" BIGINT NOT NULL, "d" DATE NOT NULL)
UNIQUE PRIMARY INDEX ("id")
PARTITION BY RANGE_N("d" BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
EACH INTERVAL '1' MONTH, NO RANGE, UNKNOWN)
SET vs MULTISET — defaults to MULTISET: a SET table silentlyPRIMARY INDEX (...) / UNIQUE PRIMARY INDEX / NO PRIMARY INDEX — theNO PRIMARY INDEX requires MULTISETPARTITION BY RANGE_N(...) — partitionByRange only; a rawpartitionBy string is refused in favor of it.VOLATILE / GLOBAL TEMPORARY — mutually exclusive; both default theirON COMMIT to PRESERVE ROWS (Teradata's own default, DELETE ROWS, would[NO] FALLBACK.PRIMARY KEY / UNIQUE column must be NOT NULL — UNIQUE on aGENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT BY 1 NO CYCLE) instead of SERIAL.createTable also runs each options.indexes entry through
buildCreateIndexSql and, if given, a COMMENT ON TABLE.
dropTable, showTables, describeTableNo DROP TABLE IF EXISTS — dropTable({ ifExists: true }) first checks
DBC.TablesV:
await d.dropTable('gone', { ifExists: true });
// SELECT 1 AS present FROM DBC.TablesV WHERE DatabaseName = DATABASE AND TableName = ?
showTables() / showViews() query DBC.TablesV by TableKind;
describeTable() reads DBC.ColumnsV and DBC.IndicesV.
Teradata puts the columns before ON:
d.buildCreateIndexSql('t', { name: 'ix', fields: ['a', 'b'], unique: true });
// CREATE UNIQUE INDEX "ix" ("a", "b") ON "t"
addIndex/createIndex refuse using (access methods), a non-empty where
(partial indexes), include columns, and concurrently — Teradata secondary
indexes have one kind and take their own locks.
Teradata has no schemas — a database/user is carved out of a parent's PERM
space:
d.buildCreateDatabaseSql({ name: 'app', from: 'dbc', permBytes: 1e9, spoolBytes: 5e8, fallback: false });
// CREATE DATABASE "app" FROM "dbc" AS PERMANENT = 1000000000, SPOOL = 500000000, NO FALLBACK
createSchema() throws and points at buildCreateDatabaseSql; dropSchema()
maps to DROP DATABASE (refusing cascade — run DELETE DATABASE first);
showAllSchemas()/listSchemas() read DBC.DatabasesV.
Teradata has no LIMIT/OFFSET. A limit alone becomes SELECT TOP n; an
offset is expressed with QUALIFY ROW_NUMBER() OVER (ORDER BY ...), which
needs an order to be deterministic:
d.buildSelectQuery({ tableName: 't', limit: 10, order: [['id', 'ASC']] }).sql;
// SELECT TOP 10 * FROM "t" ORDER BY "id" ASC
d.buildSelectQuery({ tableName: 't', where: { a: 1 }, limit: 10, offset: 20, order: [['id', 'DESC']] }).sql;
// SELECT * FROM "t" WHERE "a" = $1 QUALIFY ROW_NUMBER() OVER (ORDER BY "id" DESC)
// BETWEEN 21 AND 30 ORDER BY "id" DESC
d.buildSelectQuery({ tableName: 't', offset: 5, order: [['id', 'ASC']] }).sql;
// ... QUALIFY ROW_NUMBER() OVER (ORDER BY "id" ASC) > 5
An offset without an order, or a limit together with DISTINCT, throws by
name instead of reaching the parser (TOP cannot combine with DISTINCT, and
QUALIFY numbers rows before DISTINCT removes duplicates).
SAMPLE — Teradata's own row sampling, via teradataSample:
d.buildSelectQuery({ tableName: 't', teradataSample: { rows: 100 } }).sql;
// SELECT * FROM "t" SAMPLE 100
d.buildSelectQuery({
tableName: 't',
teradataSample: { fraction: 0.25, withReplacement: true, randomizedAllocation: true },
order: [['id', 'ASC']],
}).sql;
// SELECT * FROM "t" SAMPLE WITH REPLACEMENT RANDOMIZED ALLOCATION 0.25 ORDER BY "id" ASC
The shared sample option (PostgreSQL TABLESAMPLE BERNOULLI) is translated
to a SAMPLE fraction:
d.buildSelectQuery({ tableName: 't', sample: { method: 'BERNOULLI', percent: 10 } }).sql;
// SELECT * FROM "t" SAMPLE 0.1
TABLESAMPLE SYSTEM (block sampling) and a REPEATABLE seed have no
Teradata equivalent and throw. SAMPLE and pagination (TOP/QUALIFY)
cannot be combined — sample in a derived table first, then page it — and
neither can combine with a UNION built by this dialect.
LOCKING ... FOR <level> — the request modifier for dirty reads, via
locking:
d.buildSelectQuery({ tableName: 't', locking: 'ACCESS', limit: 1 }).sql;
// LOCKING TABLE "t" FOR ACCESS SELECT TOP 1 * FROM "t"
locking must be one of ACCESS/READ/WRITE/EXCLUSIVE.
buildSelectQuery({ lock: true }) (PostgreSQL's FOR UPDATE/FOR SHARE)
throws — Teradata has no row-level lock clause.
The inherited PostgreSQL where-builder can emit operators Teradata does not
have (ILIKE, ->/->>/#>, @>/<@, &&, :: casts, ~/~*
regexes). buildWhereClause checks the built SQL (outside string literals and
quoted identifiers) and throws naming the operator:
d.buildWhereClause({ name: { [Op.iLike]: '%a%' } });
// throws: Teradata does not support ILIKE (case-insensitive LIKE)
In the default TERA session mode, plain LIKE on a NOT CASESPECIFIC
column is already case-insensitive, which is why ILIKE is unneeded rather
than reimplemented.
Insert — refuses RETURNING:
d.buildInsertQuery('t', { a: 1, b: new Literal('CURRENT_TIMESTAMP') });
// { sql: 'INSERT INTO "t" ("a", "b") VALUES ($1, CURRENT_TIMESTAMP)', values: [1] }
Upsert → MERGE — Teradata has no ON CONFLICT; buildUpsertQuery
(and buildInsertQuery({ upsert: true })) build a MERGE. conflictFields
is required (the ON condition must fix the target's primary index with
equality) and its columns are never updated:
d.buildUpsertQuery('t', { id: 1, name: 'x' }, { conflictFields: ['id'] });
MERGE INTO "t" AS tgt USING (SELECT $1 AS "id", $2 AS "name") AS src ON tgt."id" = src."id"
WHEN MATCHED THEN UPDATE SET "name" = src."name"
WHEN NOT MATCHED THEN INSERT ("id", "name") VALUES (src."id", src."name")
Update / delete — refuse UPDATE ... LIMIT and DELETE ... LIMIT
(narrow the WHERE clause instead); truncate maps to Teradata's fast-path
DELETE FROM t ALL:
d.buildDeleteQuery('t', {}, { truncate: true }).sql; // DELETE FROM "t" ALL
Bulk insert — one parameterized INSERT executed with a batch of row
arrays bound to ? markers in one round trip (Teradata has no multi-row
VALUES list); a Literal value in any row throws, since every row shares
one set of markers:
await d.bulkInsert('t', [{ a: 1, b: true }, { a: 2, b: false }]);
// executed: 'INSERT INTO "t" ("a", "b") VALUES (?, ?)', params: [[1, 1], [2, 0]]
A dedicated connection with con.autocommit = false, finished with
con.commit()/con.rollback(). Only READ UNCOMMITTED and SERIALIZABLE
isolation levels are offered; anything else throws.
const tx = await d.startTransaction({ isolationLevel: 'READ UNCOMMITTED' });
// SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
Teradata has no savepoints — createSavepointSQL/releaseSavepointSQL/
rollbackToSavepointSQL all throw.
addColumn, removeColumn, renameColumn (ALTER TABLE ... RENAME x TO y), and renameTable (RENAME TABLE old TO new) work as expected.
changeColumn throws: Teradata cannot retype a column in general — ALTER TABLE ... ADD on an existing column can only widen a VARCHAR, change a
title/format, or toggle a few attributes, not change the type — so the
suggested path is add a new column, copy the data, drop the old one, rename.
| Call | Why |
|---|---|
createSchema | Teradata namespaces are databases carved from a parent's PERM space; use buildCreateDatabaseSql. |
dropSchema({ cascade: true }) | No CASCADE; run DELETE DATABASE first. |
createTable({ ifNotExists: true }) | No CREATE TABLE IF NOT EXISTS; check DBC.TablesV (or catch error 3803) first. |
dropTable({ cascade: true }) | No CASCADE; drop dependent views and join indexes first. |
changeColumn | Teradata cannot retype a column via ALTER TABLE. |
buildSelectQuery({ lock: true }) | No FOR UPDATE/FOR SHARE; use locking: 'WRITE' (or 'ACCESS'). |
buildSelectQuery with offset and no order | QUALIFY ROW_NUMBER() pagination needs a deterministic order. |
buildSelectQuery with limit + distinct | TOP cannot combine with DISTINCT. |
buildSelectQuery with TOP/QUALIFY + SAMPLE | Sample in a derived table first, then page it. |
buildInsertQuery/buildUpdateQuery/buildDeleteQuery({ returning: true }) | No RETURNING; read affected rows back with a SELECT. |
buildUpdateQuery/buildDeleteQuery({ limit }) | No UPDATE/DELETE ... LIMIT; narrow the WHERE clause. |
bulkInsert with a Literal value | Every row in the batch shares one set of ? markers. |
createSavepointSQL/releaseSavepointSQL/rollbackToSavepointSQL | No savepoints; a transaction commits or rolls back as a whole. |
startTransaction with an isolation level other than READ UNCOMMITTED/SERIALIZABLE | Teradata offers only those two. |
createDatabaseSQL | Use buildCreateDatabaseSql({ name, from, permBytes }). |
createSequence/dropSequence/nextSequenceValue/currSequenceValue | No sequences; use an identity column (autoIncrement: true). |
createTrigger | Teradata triggers carry their body inline; not generated here. |
createStoredProcedure/createProcedure | Teradata SPL is not generated here. |
createMaterializedView/refreshMaterializedView | Teradata's analogue is a join index (CREATE JOIN INDEX), maintained automatically. |
createExtension/dropExtension | No CREATE/DROP EXTENSION. |
createEnumType | An ENUM column is stored as VARCHAR; add a CHECK constraint instead. |
createDomain | Declare the type and constraint on each column. |
createPolicy/enableRLS | No row-level security; restrict rows with views and GRANTs. |
listen/unlisten/notify | No LISTEN/NOTIFY; poll a table or use QueryGrid / an external bus. |
pgAdvisoryLock/pgTryAdvisoryLock | No advisory locks; use a LOCKING request modifier. |
copyFrom/copyTo | No COPY; use bulkInsert, or Teradata FastLoad/FastExport/TPT. |
createPartitionedTable/createPartition/attachPartition/detachPartition | A partitioned Teradata table is one table; partitions come from PARTITION BY RANGE_N, passed via partitionByRange. |
createGINIndex/createGISTIndex/createVectorIndex/createFulltextIndex | Teradata has hash-based secondary and join indexes only; no GIN/GiST/vector/full-text machinery. |
createSpatialIndex | ST_Geometry columns are indexed by Teradata itself. |
createForeignDataWrapper/createForeignServer/createForeignTable | Federated access is Teradata QueryGrid, configured outside SQL DDL. |
changeOwner | Ownership follows the database hierarchy; move space with GIVE. |
array bind values / array literals (escape, driver binding) | Teradata ARRAY columns are user-defined types; bind or escape the elements individually. |
non-finite numbers (escape) | Store NULL or a sentinel instead. |
unmapped DataTypes (ARRAY, HSTORE, ...) | No built-in Teradata equivalent; declare the column with an explicit type string. |
DECIMAL/NUMERIC with precision > 38 | The maximum DECIMAL precision is 38. |
tests/dialects/teradata.test.tsteradatasql module (jest.mock('teradatasql', ..., { virtual: true })); nothing here has been run against a live VantagequeryStream has no server-side cursor to lean on — teradatasql exposesfetchall/fetchone on an executed request — so it fetches the fullhasSequence() and getExtensions() answer false/[] without a serverComputed 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 | teradata |
|---|---|
| Transactions atomic | yes |
| Savepoints | no — a nested transaction() throws |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only yes; deferrable yes; constraint deferral yes |
| RETURNING | refused — Teradata does not support RETURNING. Read the affected rows back with a SELECT (inside the same transaction if it matters). |
| Upsert | MERGE INTO … WHEN MATCHED / WHEN NOT MATCHED |
| JSON column | JSON (JSONB → JSON) |
| Vector column | refused — Teradata does not support the column type 'VECTOR'. Store the vector as JSON or a VARBYTE. |
| Array column | refused — Teradata does not support the column type 'ARRAY'. Store the value as JSON, or create an ARRAY or structured UDT and use its name as a raw type. |
| WHERE builder | 35 operator keys; $and/$or/$not; $exists: no; $in subquery: no; expression operands: no |
| 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 |