[!WARNING]
Support varies by engine, and most of these builders do not check their
input. Passing'INITIALLY_DEFERRED'where the type says'INITIALLY DEFERRED'
emitsDEFERRABLE INITIALLY_DEFERREDrather than raising, so the value only
fails when the database sees it. Take the accepted values below literally.
Several of them also interpolate names without quoting (see the table at the
end), so pass only names you control.
import {
buildIdentitySQL,
buildGeneratedAlwaysSQL,
buildExclusionSQL,
generateReplicaIdentitySQL,
} from 'ts-prorm-orm';
export async function up(prorm: { query(sql: string): Promise<unknown> }) {
await prorm.query('CREATE EXTENSION IF NOT EXISTS btree_gist'); // `room WITH =` in a GiST index
await prorm.query(
[
'CREATE TABLE bookings (',
` id bigint ${buildIdentitySQL({ type: 'ALWAYS' })} PRIMARY KEY,`,
' room int NOT NULL,',
' during tstzrange NOT NULL,',
' nightly_rate numeric(10,2) NOT NULL,',
' nights int NOT NULL,',
` total numeric(12,2) ${buildGeneratedAlwaysSQL({ as: 'nightly_rate * nights', stored: true })},`,
` ${buildExclusionSQL('bookings', {
name: 'no_overlap',
using: 'gist',
elements: ['room WITH =', 'during WITH &&'],
})}`,
')',
].join('\n'),
);
await prorm.query(`ALTER TABLE bookings ${generateReplicaIdentitySQL({ type: 'FULL' })}`);
}import { buildGeneratedAlwaysSQL } from 'ts-prorm-orm';
buildGeneratedAlwaysSQL({ as: 'price * qty', stored: true });
// GENERATED ALWAYS AS (price * qty) STOREDstored: false gives a virtual column, recomputed on read. MySQL supportsas is raw SQL, passed through as written. For a portable expression,fn() and col() together with a dialect instance, and theimport { buildGeneratedAlwaysSQL, fn, col } from 'ts-prorm-orm';
const dialect = prorm.getDialectInstance()!;
// `{ ...fn() }`: see the note below on the `as` type
buildGeneratedAlwaysSQL({ as: { ...fn('LOWER', col('email')) }, stored: true }, dialect);[!NOTE]
A dialect name is not enough for the structured form:buildGeneratedAlwaysSQL({ as: fn(…) }, 'postgres')throwsdialect.escapeId is not a function. Passprorm.getDialectInstance().
Theasoption is typed as an object with an index signature, which theFnExpressionandOpExpressioninterfaces do not satisfy, so passingfn(…)directly is a TypeScript error even though it works at runtime.
Spreading it into an object literal satisfies the type.On a model, @Generated({ as: op('*', col('unitPrice'), col('quantity')) })
declares a computed column from an expression, andsync()compiles it for
the connected engine.
SERIAL / AUTO_INCREMENT.import { buildIdentitySQL } from 'ts-prorm-orm';
buildIdentitySQL({ type: 'ALWAYS', start: 1, increment: 1 });
// GENERATED ALWAYS AS IDENTITY ( START WITH 1 INCREMENT BY 1 )
buildIdentitySQL({ type: 'BY DEFAULT', start: 1000 });
// GENERATED BY DEFAULT AS IDENTITY ( START WITH 1000 )type is 'ALWAYS' or 'BY DEFAULT'. ALWAYS refuses an explicit insert intoBY DEFAULT is the one toimport { generateColumnCollateSQL } from 'ts-prorm-orm';
generateColumnCollateSQL('name', 'en_US.utf8');
// MODIFY name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE en_US.utf8ALTER TABLE … MODIFY syntax, and it always declares the columnVARCHAR(255), so it would shorten a longer column. Collation decides sortUNIQUE: no two rows may satisfy the givenimport { buildExclusionSQL } from 'ts-prorm-orm';
buildExclusionSQL('bookings', {
name: 'no_overlap',
using: 'gist',
elements: ['room WITH =', 'during WITH &&'],
});
// CONSTRAINT "no_overlap" EXCLUDE USING gist (room WITH =, during WITH &&){ expr, operator, opclass }) and a where are compiled, with the operatorExclusionOperator. Without a name, the constraint is namedexcl_<table>_<timestamp>, which differs on every run, so always pass one in aimport { buildDeferrableSQL } from 'ts-prorm-orm';
buildDeferrableSQL({ deferrable: 'INITIALLY DEFERRED' });
// DEFERRABLE INITIALLY DEFERRED'NOT DEFERRABLE', 'INITIALLY IMMEDIATE', 'INITIALLY DEFERRED'.INITIALLY IMMEDIATE constraint to deferred for one transaction.import { generateIndexHintSQL } from 'ts-prorm-orm';
generateIndexHintSQL({ useIndex: ['idx_created'], forceIndex: ['idx_status'] }, 'mysql');
// USE INDEX (idx_created) FORCE INDEX (idx_status)dialect argument | Output |
|---|---|
'mysql', 'mariadb' | USE INDEX (idx_created) FORCE INDEX (idx_status) |
'sqlserver' | WITH (INDEX(idx_created)) (only useIndex) |
'mssql', 'postgres', anything else | undefined |
useIndex suggests, forceIndex insists, ignoreIndex excludes. Reach for[!CAUTION]
The SQL Server branch matches the string'sqlserver', not the dialect name'mssql'thatprorm.getDialect()returns. PassinggetDialect()on SQL
Server therefore returnsundefined, and the hint is silently dropped.
import { generateSpatialIndexSQL } from 'ts-prorm-orm';
generateSpatialIndexSQL('places', 'location', 'places_geo', prorm.getDialect());CREATE INDEX "places_geo" ON "places" ("location") INDEXTYPE IS
MDSYS.SPATIAL_INDEX, and an engine with no spatial index throws. On SQLite theplaces.import { generateClusteredSQL } from 'ts-prorm-orm';
generateClusteredSQL({ type: 'clustered', fillFactor: 90 });
// CLUSTERED (FILLFACTOR = 90)type is 'clustered' or 'nonclustered', lower case. A clustered indeximport { generateStatisticsSQL } from 'ts-prorm-orm';
generateStatisticsSQL({ for: 'columns', columns: ['status', 'country'], target: 500 }, 'orders');
// ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500, ALTER COLUMN country SET STATISTICS 500ANALYZE orders afterwards; the new target applies fromimport { generateDistinctOnSQL } from 'ts-prorm-orm';
generateDistinctOnSQL(['user_id']);
// DISTINCT ON (user_id)ORDER BY.import { generateDistinctOnSQL } from 'ts-prorm-orm';
const latest = await prorm.query(
`SELECT ${generateDistinctOnSQL(['user_id'])} * FROM orders ORDER BY user_id, created_at DESC`,
);
// SELECT DISTINCT ON (user_id) * FROM orders ORDER BY user_id, created_at DESCORDER BY must start with the DISTINCT ON columns; the columns after themimport { generateTablesampleSQL } from 'ts-prorm-orm';
generateTablesampleSQL({ method: 'BERNOULLI', percentage: 10 });
// TABLESAMPLE BERNOULLI(10)
generateTablesampleSQL({ method: 'SYSTEM', percentage: 1, repeatable: 42 });
// TABLESAMPLE SYSTEM(1) REPEATABLE(42)BERNOULLI scans and keeps each row with the given probability; SYSTEM picksSYSTEM is usually the right trade. repeatable fixes the seed,import { generateCursorSQL } from 'ts-prorm-orm';
generateCursorSQL({ scroll: true, holdable: 'HOLD' }, 'c1');
// DECLARE c1 CURSOR SCROLL WITH HOLD FORholdable is 'HOLD' or 'WITHOUT HOLD'. A held cursor survives theimport { generateJSONExtractSQL, generateJSONSetSQL, generateJSONRemoveSQL } from 'ts-prorm-orm';
generateJSONExtractSQL('data', '$.address.city');
// JSON_EXTRACT(`data`, '$.address.city')
generateJSONExtractSQL('data', '$.address.city', true);
// JSON_UNQUOTE(JSON_EXTRACT(`data`, '$.address.city'))
generateJSONSetSQL('data', '$.address.city', "'Leeds'");
// JSON_SET(`data`, '$.address.city', 'Leeds')
generateJSONRemoveSQL('data', '$.address.zip');
// JSON_REMOVE(`data`, '$.address.zip')generateJSONSetSQL is spliced in as SQL, so quote it yourself. ForjsonExtract(), and forimport { generateInheritsSQL } from 'ts-prorm-orm';
generateInheritsSQL({ parentTable: 'events' });
// INHERITS (events)ONLY.import { generateTablespaceSQL } from 'ts-prorm-orm';
generateTablespaceSQL({ name: 'fast_ssd' }, prorm.getDialect());| Dialect | Output |
|---|---|
| PostgreSQL | TABLESPACE fast_ssd |
| MySQL, MariaDB | TABLESPACE = fast_ssd |
| SQLite and the rest | undefined |
import { generateCompressionSQL } from 'ts-prorm-orm';
generateCompressionSQL({ algorithm: 'lz4' });
// COMPRESSION lz4 (COMPRESS_LEVEL 6)'pglz' or 'lz4'. This applies to oversized values moved out of line, not toimport { generateReplicaIdentitySQL } from 'ts-prorm-orm';
generateReplicaIdentitySQL({ type: 'FULL' });
// REPLICA IDENTITY FULL
generateReplicaIdentitySQL({ type: 'INDEX', index: 'orders_pkey' });
// REPLICA IDENTITY INDEX orders_pkeyFULLimport { generateStatementTimeoutSQL, generateIdleInTransactionTimeoutSQL } from 'ts-prorm-orm';
generateStatementTimeoutSQL(5000);
// SET session statement_timeout = '5000ms'
generateIdleInTransactionTimeoutSQL(60000);
// SET session idle_in_transaction_session_timeout = '60000ms'| Builder | Identifiers | Dialect-aware |
|---|---|---|
buildExclusionSQL | constraint name quoted; string elements verbatim | PostgreSQL only |
generateSpatialIndexSQL | quoted per dialect (SQLite's rtree column is not) | yes |
buildGeneratedAlwaysSQL | string as verbatim; fn()/col() quoted | with a dialect instance |
generateStatisticsSQL, generateDistinctOnSQL, generateInheritsSQL, generateTablespaceSQL, generateCursorSQL, generateReplicaIdentitySQL, generateIndexHintSQL | verbatim | only where the table above says so |
generateJSONExtractSQL / Set / Remove | backticks (MySQL) | no |
| Looking for | Page |
|---|---|
| Views, materialized views, triggers, procedures, sequences, row-level security, partitions | Schema objects |
| Check constraints, foreign keys, unique and partial indexes | Indexes & constraints |
fn(), col(), literal(), window functions, aggregates | SQL functions |
| Session settings, isolation levels, pragmas | SQL constants |
| Raw statements and bound parameters | Raw queries |
| DDL from code, outside a migration | Query interface |