mssql driver as SQL Server, so the prorm Synapse dialect extends MSSQLDialect and inherits its connection handling, parameter binding and query building.synapse.Status: Experimental. SQL generation is covered by connection-free unit tests; nothing has been executed against a live pool (see Verification status).
DISTRIBUTION (how rows are spread), the storage index (columnstore / rowstore / heap) and range PARTITION boundaries are all declared inline in CREATE TABLE ... WITH (...).CHECK constraints at all. PRIMARY KEY and UNIQUE exist only in their NOT ENFORCED spellings, recorded for the optimizer and never checked at write time.CREATE TABLE AS SELECT is fully parallel and sets the new table's distribution and storage in one statement.| Property | Value |
|---|---|
| Dialect name | synapse |
| Extends | MSSQLDialect (mssql) |
| Driver | mssql |
| Cloud SDK | @azure/identity (Entra only) |
| Default port | 1433 |
| Local runtime | none — hosted service only |
$ npm install ts-prorm-orm mssql$ pnpm add ts-prorm-orm mssql$ yarn add ts-prorm-orm mssql$ bun add ts-prorm-orm mssqlmssql is the same driver the mssql dialect uses.import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'synapse',
host: 'my-workspace.sql.azuresynapse.net',
database: 'my_pool',
username: 'sqladminuser',
password: process.env.SYNAPSE_PASSWORD,
dialectOptions: {
encrypt: true,
},
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'synapse',
url: 'synapse://sqladminuser:secret@my-workspace.sql.azuresynapse.net:1433/my_pool',
dialectOptions: {
encrypt: 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) },
}, { timestamps: false });
await db.sync(); // CREATE TABLE — see Generated SQL below[!NOTE] Why timestamps: false
A non-literal DEFAULT on column "createdAt" (CURRENT_TIMESTAMP) is not supported by Azure Synapse Analytics (dedicated SQL pool). A dedicated SQL pool requires the DEFAULT expression to be a literal constant — function calls and CURRENT_TIMESTAMP are rejected. Supply the value from the application on INSERT instead.
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,
});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' });MERGE statementupsert() also reads the row by primary key before and after the write, to return the instance.[!WARNING] No OFFSETpagination
OFFSET ... FETCH pagination is not supported by Azure Synapse Analytics (dedicated SQL pool). A dedicated SQL pool's ORDER BY clause has no OFFSET/FETCH. Page with a ROW_NUMBER() window function in a subquery or CTE and filter on the resulting row number.
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 synapse has none. Run the work in a single transaction, or restructure it so nothing nests.
STRING → NVARCHAR(255), auto-increment INTEGER → INT, BOOLEAN → BIT, DATE → DATETIME2, UUID → UNIQUEIDENTIFIER, JSON → NVARCHAR(MAX). The type comparison lines the major engines up side by side.dialect: 'synapse'. Connection options are SQL Server's — same driver, same TDS port 1433 — at the workspace's SQL endpoint, plus the Entra ID logins below.import { Prorm } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'synapse',
host: 'my-workspace.sql.azuresynapse.net',
port: 1433,
database: 'my_pool',
username: 'sqladminuser',
password: process.env.SYNAPSE_PASSWORD,
dialectOptions: { encrypt: true },
});entra (a token from DefaultAzureCredential, or the credential you pick), tokenProvider (any token source), or a tedious-native authentication block. Give at most one. An Entra login replaces username/password and requires verified TLS (encrypt: true, trustServerCertificate: false, the defaults it applies); a certificate that is not verified is refused at construction. Needs @azure/identity installed next to the driver.const prorm = new Prorm({
dialect: 'synapse',
host: 'my-workspace.sql.azuresynapse.net',
database: 'my_pool',
dialectOptions: {
entra: { credential: { type: 'managed-identity', clientId: process.env.AZURE_CLIENT_ID } },
},
});dialect.authenticationMode reports 'entra-token', 'tedious-native' or 'sql'.const dialect = new SynapseDialect({ /* ... */ });
dialect.name; // 'synapse'
dialect.library; // 'mssql' — inherited; there is no separate Synapse drivername override is written readonly name = 'synapse' as SynapseDialect['name']. The runtime value is 'synapse'; the cast only satisfies the property-override type check against the base's string-literal type without editing MSSQLDialect.SynapseDialect extends MSSQLDialect (mssql) andObject.getOwnPropertyNames(SynapseDialect.prototype),| Area | Overridden |
|---|---|
| Connection and execution | queryStream |
| DML builders | buildDeleteQuery, buildInsertQuery, buildLimitOffset, buildSelectQuery, buildUpdateQuery, buildUpsertQuery, bulkInsert |
| Types | buildSynapseColumnSql, getDataTypeSql |
| DDL and schema | addColumn, addForeignKey, addIndex, addPartition, attachPartition, buildCreateMaterializedViewSQL, buildCreateTableSQL, buildCtasSQL, buildDistributionClause, buildPartitionClause, buildTableIndexClause, buildTableWithClause, changeColumn, createColumnstoreIndex, createComputedColumn, createConstraint, createDatabaseSQL, createExpressionIndex, createFulltextIndex, createIdentityColumn, createMaterializedView, createPartialIndex, createPartition, createPartitionedTable, createSavepointSQL, createSequence, createSpatialIndex, createStatisticsSQL, createStoredProcedure, createTable, createTableAs, createTrigger, createView, detachPartition, disableSystemVersioning, dropDatabaseSQL, dropIfObjectExistsSQL, dropMaterializedView, dropPartition, dropSchema, dropSequence, dropTable, dropTemporalTable, dropTrigger, dropView, refreshMaterializedView, renameTable, splitPartitionSQL |
| Introspection | assertLiteralDefault, assertNoReturning, hasSequence, hasTrigger, listSequences, showMaterializedViews |
| Other | buildSynapseConstraintSql, commentColumn, commentTable, formatBoundary, insert, mergePartitionSQL, nextSequenceValue, noSpatial, releaseSavepointSQL, rollbackToSavepointSQL, stAsText, stContains, stDWithin, stDistance, stGeomFromText, stIntersects, stWithin, switchPartitionSQL, updateStatisticsSQL |
await dialect.createTable(
'FactSales',
{
saleId: { type: 'BIGINT', allowNull: false },
region: { type: 'NVARCHAR(50)', allowNull: false },
orderDate: { type: 'DATE', allowNull: false },
amount: { type: 'DECIMAL(18,2)' },
},
{
distribution: { type: 'HASH', columns: 'region' },
index: { type: 'CLUSTERED COLUMNSTORE INDEX' },
partition: { column: 'orderDate', range: 'RIGHT', boundaries: ['2024-01-01', '2025-01-01'] },
},
);distribution| Value | Emits | Use for |
|---|---|---|
{ type: 'ROUND_ROBIN' } | DISTRIBUTION = ROUND_ROBIN | Staging tables; the platform default |
{ type: 'HASH', columns: 'c' } | DISTRIBUTION = HASH([c]) | Large fact tables joined or grouped on c |
{ type: 'HASH', columns: ['a','b'] } | DISTRIBUTION = HASH(([a], [b])) | Multi-column distribution keys |
{ type: 'REPLICATE' } | DISTRIBUTION = REPLICATE | Small dimension tables |
HASH((a, b)) and HASH(a, b) are different productions.index — { type: 'CLUSTERED COLUMNSTORE INDEX', order?: string[] } (the default; order emits the ordered form), { type: 'CLUSTERED INDEX', columns: [...] }, or { type: 'HEAP' }.partition — { column, range?: 'LEFT' | 'RIGHT', boundaries }. LEFT is the Synapse default. Unlike SQL Server, there is no separate PARTITION FUNCTION/PARTITION SCHEME; createPartitionedTable() therefore works on this dialect where it throws on mssql.dialect.splitPartitionSQL('FactSales', '2026-01-01'); // ALTER TABLE ... SPLIT RANGE (...)
dialect.mergePartitionSQL('FactSales', '2024-01-01'); // ALTER TABLE ... MERGE RANGE (...)
dialect.switchPartitionSQL('Stage', 1, 'FactSales', 3); // ALTER TABLE ... SWITCH PARTITION 1 TO ...createPartition, attachPartition, detachPartition, dropPartition, addPartition) throws: Synapse partitions are unnamed boundary ranges, not separate relations.{ id: { type: 'INT', allowNull: false, primaryKey: true } }
// [id] INT NOT NULL PRIMARY KEY NONCLUSTERED NOT ENFORCEDNONCLUSTERED and NOT ENFORCED are both mandatory — a bare PRIMARY KEY, which the SQL Server base emits, is a parse error. Table-level constraints put NOT ENFORCED after the column list: CONSTRAINT [pk] PRIMARY KEY NONCLUSTERED ([a], [b]) NOT ENFORCED.FOREIGN KEY and CHECK throw, from createTable, createConstraint and addForeignKey alike.DEFAULT must be a literal constant. The base dialect passes a string default through as raw SQL, so a model carrying defaultValue: 'GETDATE()' creates fine on SQL Server and fails on Synapse; this dialect rejects it at build time instead. A DEFAULT on the hash-distribution column is also refused.IDENTITYIDENTITY(1,1) is supported and is emitted before the column constraints, which is the order the Synapse grammar uses (SQL Server's base builder emits it last). SET IDENTITY_INSERT works, and insert()/bulkInsert() still accept identityInsert: true.IDENTITY column to an existing table throws (there is no way to backfill per-distribution ranges), as does createIdentityColumn(); recreate the table with CTAS instead.TOP, never OFFSET/FETCHORDER BY grammar has no <offset_fetch> production at all.dialect.buildSelectQuery({ tableName: 'FactSales', limit: 10 });
// SELECT TOP(10) * FROM [FactSales]
dialect.buildSelectQuery({ tableName: 'FactSales', limit: 10, offset: 20 });
// throws: OFFSET ... FETCH pagination is not supported by Azure Synapse Analytics (dedicated SQL pool).buildLimitOffset() throws for both forms rather than degrading. queryStream() throws for the same reason: the generic streaming helper pages by re-issuing the query with successive OFFSETs. Page with a ROW_NUMBER() window function in a subquery or CTE.OUTPUT clausereturning throws on buildInsertQuery, insert, buildUpdateQuery, buildDeleteQuery, buildUpsertQuery and bulkInsert. A dedicated pool cannot return affected rows from a DML statement; re-select them afterwards, on a business key rather than on an identity value.bulkInsert() issues one parameterized INSERT per row, wrapped in a single transaction. The base dialect batches rows into a multi-row table value constructor, which Synapse rejects outright ("Table value constructor is not supported in Azure Synapse Analytics").INSERT against an MPP engine is slow by construction.await dialect.createTableAs(
'AggSales',
'SELECT region, SUM(amount) AS total FROM [FactSales] GROUP BY region',
{ distribution: { type: 'HASH', columns: 'region' }, index: { type: 'CLUSTERED COLUMNSTORE INDEX' } },
);
// CREATE TABLE [AggSales] WITH (CLUSTERED COLUMNSTORE INDEX, DISTRIBUTION = HASH([region])) AS SELECT ...WITH (...) clause is required by the grammar, so a call that names no distribution still gets DISTRIBUTION = ROUND_ROBIN spelled out. ORDER BY is not allowed in a CTAS select.await dialect.createMaterializedView({
name: 'mvSales',
schema: 'dbo',
query: 'SELECT region, SUM(amount) AS total FROM [FactSales] GROUP BY region',
distribution: { type: 'HASH', columns: 'region' },
});
// CREATE MATERIALIZED VIEW [dbo].[mvSales] WITH (DISTRIBUTION = HASH([region])) AS SELECT ...refreshMaterializedView() emits ALTER MATERIALIZED VIEW ... REBUILD. REPLICATE distribution is refused. showMaterializedViews() looks for a clustered columnstore index (sys.indexes.type = 5) rather than the unique clustered rowstore index the base looks for, which a Synapse materialized view never has.SCHEMABINDING and no CREATE OR ALTER, so createView(..., { replace: true }) emits a guarded DROP VIEW followed by CREATE VIEW.addIndex() builds a plain CREATE [CLUSTERED|NONCLUSTERED] INDEX name ON table (cols). unique, where (filtered), include (covering) and expression each throw by name — none exist in the dedicated pool's CREATE INDEX surface, and there are no computed columns to emulate an expression index with.createColumnstoreIndex() supports the clustered form plus ORDER (cols); the nonclustered columnstore form and COMPRESSION_DELAY throw. Full-text and spatial indexes throw.NVARCHAR(MAX), VARBINARY(MAX), UNIQUEIDENTIFIER and DATETIMEOFFSET, which a dedicated pool does support.getDataTypeSql() throws for the types it does not: GEOMETRY, GEOGRAPHY, HIERARCHYID, XML, SQL_VARIANT, ROWVERSION/TIMESTAMP and the legacy TEXT/NTEXT/IMAGE LOB types. The st* spatial helpers throw for the same reason. Table types and table variables (DECLARE @t TABLE) do not exist either; use a #temp table.| Synapse | Emitted instead of the base's | |
|---|---|---|
DROP TABLE/VIEW/MATERIALIZED VIEW with ifExists | No IF EXISTS | IF OBJECT_ID('T', 'U') IS NOT NULL DROP TABLE [T] |
dropSchema with ifExists | No IF EXISTS | IF SCHEMA_ID('s') IS NOT NULL DROP SCHEMA [s] |
renameTable | No sp_rename for tables | RENAME OBJECT [old] TO [new] |
createStoredProcedure | No CREATE OR ALTER; CREATE PROCEDURE must start its batch | Guard issued as its own statement, then a plain CREATE PROCEDURE |
changeColumn | ALTER COLUMN takes only type/collation/nullability | Nullability always spelled out; DEFAULT/PRIMARY KEY dropped |
| Row locks | No table-hint syntax | throws |
| Recursive CTEs | Not supported | throws (plain CTEs work) |
| Savepoints | no — a nested transaction() throws | |
| Table/column comments | No sp_addextendedproperty | throws |
CREATE/DROP DATABASE | Azure control-plane operation | throws |
CREATE SECURITY POLICY), CROSS/OUTER APPLY, OPENJSON, the JSON scalar functions, window functions, MERGE, SELECT ... INTO and DELETE/UPDATE ... FROM ... JOIN are all supported and inherited unchanged.dialect.createStatisticsSQL('FactSales', 'st_region', ['region']);
dialect.updateStatisticsSQL('FactSales');azure-sqldw-latest version of the relevant T-SQL reference page rather than against general SQL Server knowledge. The load-bearing ones are quote-backed in the source comments: the absent <offset_fetch> production in the Synapse ORDER BY syntax block; "Table value constructor is not supported in Azure Synapse Analytics"; "a common table expression that includes references to itself … isn't supported"; the OUTPUT clause's "applies to" list omitting Synapse; the FROM grammar's missing table-hint production; IF EXISTS documented as SQL Server only; OR ALTER present only in the SQL Server and Fabric procedure/view blocks; "PRIMARY KEY is only supported when NONCLUSTERED and NOT ENFORCED are both used".tests/dialects/synapse.test.ts that assert exact SQL strings. Several tests additionally assert the base MSSQLDialect's divergent output next to the Synapse one, so the specific regression each override prevents is pinned rather than merely described.WITH (...) clause ordering, the ordered clustered columnstore ORDER (...) form, and the RENAME OBJECT and ALTER MATERIALIZED VIEW ... REBUILD statements.MERGE. The docs are genuinely ambiguous — the feature overview marks it "Yes (preview)" while the MERGE reference lists Synapse without a preview flag and documents distribution-corruption bugs fixed in specific server builds. WHEN NOT MATCHED THEN INSERT is also documented as unsupported against a table with an IDENTITY column, which this dialect does not detect. Check your pool's build before relying on upserts.BEGIN/COMMIT/ROLLBACK and isolation level, and the SAVE TRANSACTION reference page could not be retrieved. If a pool does accept SAVE TRANSACTION, this dialect is over-restrictive there.sp_addextendedproperty, likewise refused on the strength of Synapse's absence from its "applies to" list as reported by search rather than a directly fetched page.renameColumn() is inherited unchanged, emitting sp_rename ... 'COLUMN'. That matches the shape of the documented Synapse column-rename support, which is in preview and cannot be used on the distribution column; it is not confirmed available on every pool.PRIMARY KEY/UNIQUE NOT ENFORCED column must be NOT NULL, and whether it may be the distribution column. Neither is settled by the documentation, so neither is checked.capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | synapse |
|---|---|
| Transactions atomic | yes |
| Savepoints | no — a nested transaction() throws |
| Isolation | up to SERIALIZABLE; native levels SNAPSHOT |
| Transaction modes | read-only no; deferrable no; constraint deferral no |
| RETURNING | refused — The OUTPUT clause (returning on INSERT) is not supported by Azure Synapse Analytics (dedicated SQL pool). A dedicated SQL pool cannot return affected rows fro |
| Upsert | MERGE INTO … WHEN MATCHED / WHEN NOT MATCHED |
| JSON column | NVARCHAR(MAX) (JSONB → NVARCHAR(MAX)) |
| Vector column | refused — The VECTOR data type is not supported by Azure Synapse Analytics (dedicated SQL pool). A dedicated SQL pool has no vector type. Store the embedding as NVARCHAR( |
| Array column | NVARCHAR(255) |
| WHERE builder | 18 operator keys; $and/$or/$not; $exists: yes; $in subquery: no; expression operands: no |
| SQL objects | family mssql: triggers no; procedures yes; functions multiStatement; materialized views emulated (indexedView); sequences yes; enums check; RLS no |
| Streaming | queryStream() present; LISTEN absent; createExtension() present |
tests/dialects/synapse.test.ts exercises the dialect with a mocked driver, so nothing touches a server.FindOptions becomes SQL