@mikro-orm/mssql 7.1.0-dev.5 → 7.1.0-dev.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  import { AbstractSqlConnection, type TransactionEventBroadcaster } from '@mikro-orm/sql';
2
2
  import { type ControlledTransaction, MssqlDialect } from 'kysely';
3
+ import { type Routine, type Transaction } from '@mikro-orm/core';
3
4
  import type { ConnectionConfiguration } from 'tedious';
4
5
  /** Microsoft SQL Server database connection using the `tedious` driver. */
5
6
  export declare class MsSqlConnection extends AbstractSqlConnection {
6
7
  createKyselyDialect(overrides: ConnectionConfiguration): MssqlDialect;
7
8
  private mapOptions;
9
+ callRoutine<T>(routine: Routine, args?: Record<string, unknown>, ctx?: Transaction): Promise<T>;
8
10
  commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster): Promise<void>;
9
11
  protected transformRawResult<T>(res: any, method: 'all' | 'get' | 'run'): T;
10
12
  }
@@ -1,4 +1,4 @@
1
- import { AbstractSqlConnection, Utils } from '@mikro-orm/sql';
1
+ import { AbstractSqlConnection, DatabaseSchema, Utils, } from '@mikro-orm/sql';
2
2
  import { MssqlDialect } from 'kysely';
3
3
  import * as Tedious from 'tedious';
4
4
  import * as Tarn from 'tarn';
@@ -55,6 +55,53 @@ export class MsSqlConnection extends AbstractSqlConnection {
55
55
  }
56
56
  return Utils.mergeConfig(ret, overrides);
57
57
  }
58
+ async callRoutine(routine, args = {}, ctx) {
59
+ if (routine.type === 'function') {
60
+ return this.callRoutineFunction(routine, args, ctx);
61
+ }
62
+ // MSSQL scalar UDF calls must be schema-qualified — `select sql_hash(...)` fails to parse.
63
+ const schema = routine.schema ?? this.platform.getDefaultSchemaName() ?? 'dbo';
64
+ const qualified = `${this.platform.quoteIdentifier(schema)}.${this.platform.quoteIdentifier(routine.name)}`;
65
+ // T-SQL session variables don't persist across execute() calls (different pool connections),
66
+ // so DECLARE/SET/EXEC/SELECT must go through as a single batch.
67
+ const declareLines = [];
68
+ const setLines = [];
69
+ const setValues = [];
70
+ const callArgs = [];
71
+ const inValues = [];
72
+ const outVars = [];
73
+ routine.params.forEach((p, i) => {
74
+ if (p.direction === 'in') {
75
+ callArgs.push('?');
76
+ inValues.push(this.convertRoutineInbound(args[p.name], p));
77
+ return;
78
+ }
79
+ const varName = `@_mikro_orm_routine_${i}`;
80
+ // Logical aliases like `'string'`/`'number'` aren't valid T-SQL types — translate them
81
+ // through the platform's type system, matching the DDL side in `DatabaseSchema`.
82
+ const declType = DatabaseSchema.resolveRoutineColumnType(p.type, this.platform);
83
+ declareLines.push(`declare ${varName} ${declType}`);
84
+ outVars.push({ name: p.name, varName, param: p });
85
+ if (p.direction === 'inout') {
86
+ setLines.push(`set ${varName} = ?`);
87
+ setValues.push(this.convertRoutineInbound(args[p.name], p));
88
+ }
89
+ callArgs.push(`${varName} output`);
90
+ });
91
+ const allValues = [...setValues, ...inValues];
92
+ const batch = [...declareLines, ...setLines, `exec ${qualified} ${callArgs.join(', ')}`];
93
+ if (outVars.length > 0) {
94
+ const selectClause = outVars.map(o => `${o.varName} as ${this.platform.quoteIdentifier(o.name)}`).join(', ');
95
+ batch.push(`select ${selectClause}`);
96
+ }
97
+ const result = await this.execute(batch.join('; '), allValues, 'all', ctx);
98
+ if (outVars.length === 0) {
99
+ return undefined;
100
+ }
101
+ const rows = result;
102
+ this.applyRoutineOutParams(rows[0] ?? {}, outVars.map(o => o.param), args);
103
+ return undefined;
104
+ }
58
105
  async commit(ctx, eventBroadcaster) {
59
106
  if ('savepointName' in ctx) {
60
107
  return;
@@ -1,5 +1,5 @@
1
- import { type AnyEntity, type EntityClass, type EntitySchema, MikroORM, type Options, type IDatabaseDriver, type EntityManager, type EntityManagerType } from '@mikro-orm/core';
2
- import type { SqlEntityManager } from '@mikro-orm/sql';
1
+ import { type AnyEntity, type EntityClass, type EntitySchema, type MikroORM, type Options, type IDatabaseDriver, type EntityManager, type EntityManagerType } from '@mikro-orm/core';
2
+ import { SqlMikroORM, type SqlEntityManager } from '@mikro-orm/sql';
3
3
  import { MsSqlDriver } from './MsSqlDriver.js';
4
4
  /** Configuration options for the MSSQL driver. */
5
5
  export type MsSqlOptions<EM extends SqlEntityManager<MsSqlDriver> = SqlEntityManager<MsSqlDriver>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> = Partial<Options<MsSqlDriver, EM, Entities>>;
@@ -8,7 +8,7 @@ export declare function defineMsSqlConfig<EM extends SqlEntityManager<MsSqlDrive
8
8
  /**
9
9
  * @inheritDoc
10
10
  */
11
- export declare class MsSqlMikroORM<EM extends SqlEntityManager<MsSqlDriver> = SqlEntityManager<MsSqlDriver>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> extends MikroORM<MsSqlDriver, EM, Entities> {
11
+ export declare class MsSqlMikroORM<EM extends SqlEntityManager<MsSqlDriver> = SqlEntityManager<MsSqlDriver>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> extends SqlMikroORM<MsSqlDriver, EM, Entities> {
12
12
  /**
13
13
  * @inheritDoc
14
14
  */
package/MsSqlMikroORM.js CHANGED
@@ -1,4 +1,5 @@
1
- import { defineConfig, MikroORM, } from '@mikro-orm/core';
1
+ import { defineConfig, } from '@mikro-orm/core';
2
+ import { SqlMikroORM } from '@mikro-orm/sql';
2
3
  import { MsSqlDriver } from './MsSqlDriver.js';
3
4
  /** Creates a type-safe configuration object for the MSSQL driver. */
4
5
  export function defineMsSqlConfig(options) {
@@ -7,7 +8,7 @@ export function defineMsSqlConfig(options) {
7
8
  /**
8
9
  * @inheritDoc
9
10
  */
10
- export class MsSqlMikroORM extends MikroORM {
11
+ export class MsSqlMikroORM extends SqlMikroORM {
11
12
  /**
12
13
  * @inheritDoc
13
14
  */
@@ -8,6 +8,7 @@ export declare class MsSqlPlatform extends AbstractSqlPlatform {
8
8
  #private;
9
9
  protected readonly schemaHelper: MsSqlSchemaHelper;
10
10
  protected readonly exceptionConverter: MsSqlExceptionConverter;
11
+ formatIndexHint(indexNames: string[]): string;
11
12
  /** @inheritDoc */
12
13
  lookupExtensions(orm: MikroORM<MsSqlDriver>): void;
13
14
  /** @inheritDoc */
@@ -20,6 +21,8 @@ export declare class MsSqlPlatform extends AbstractSqlPlatform {
20
21
  convertDateToJSValue(value: string | Date): string;
21
22
  convertsJsonAutomatically(): boolean;
22
23
  indexForeignKeys(): boolean;
24
+ /** SQL Server identifier limit (sysname). */
25
+ getMaxIdentifierLength(): number;
23
26
  supportsSchemas(): boolean;
24
27
  getCurrentTimestampSQL(length: number): string;
25
28
  getDateTimeTypeDeclarationSQL(column: {
package/MsSqlPlatform.js CHANGED
@@ -16,6 +16,9 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
16
16
  bigint: 'bigint',
17
17
  boolean: 'bit',
18
18
  };
19
+ formatIndexHint(indexNames) {
20
+ return `with (index(${indexNames.join(', ')}))`;
21
+ }
19
22
  /** @inheritDoc */
20
23
  lookupExtensions(orm) {
21
24
  MsSqlSchemaGenerator.register(orm);
@@ -52,6 +55,10 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
52
55
  indexForeignKeys() {
53
56
  return false;
54
57
  }
58
+ /** SQL Server identifier limit (sysname). */
59
+ getMaxIdentifierLength() {
60
+ return 128;
61
+ }
55
62
  supportsSchemas() {
56
63
  return true;
57
64
  }
@@ -136,6 +143,7 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
136
143
  return 'uniqueidentifier';
137
144
  }
138
145
  validateMetadata(meta) {
146
+ super.validateMetadata(meta);
139
147
  for (const prop of meta.props) {
140
148
  if ((prop.runtimeType === 'string' || ['string', 'nvarchar'].includes(prop.type)) &&
141
149
  !['uuid'].includes(prop.type) &&
@@ -150,16 +158,17 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
150
158
  getSearchJsonPropertyKey(path, type, aliased, value) {
151
159
  const [a, ...b] = path;
152
160
  /* v8 ignore next */
153
- const root = this.quoteIdentifier(aliased ? `${ALIAS_REPLACEMENT}.${a}` : a);
161
+ const root = aliased ? `[${ALIAS_REPLACEMENT}].${this.quoteIdentifier(a)}` : this.quoteIdentifier(a);
154
162
  const types = {
155
163
  boolean: 'bit',
156
164
  };
157
165
  const cast = (key) => raw(type in types ? `cast(${key} as ${types[type]})` : key);
166
+ const jsonPath = this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`);
158
167
  /* v8 ignore next */
159
168
  if (path.length === 0) {
160
- return cast(`json_value(${root}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
169
+ return cast(`json_value(${root}, ${jsonPath})`);
161
170
  }
162
- return cast(`json_value(${root}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
171
+ return cast(`json_value(${root}, ${jsonPath})`);
163
172
  }
164
173
  getJsonArrayFromSQL(column, alias, properties) {
165
174
  const columns = properties
@@ -187,7 +196,7 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
187
196
  if (RawQueryFragment.isKnownFragment(id)) {
188
197
  return super.quoteIdentifier(id);
189
198
  }
190
- return `[${id.toString().replace('.', `].[`)}]`;
199
+ return `[${id.toString().replaceAll(']', ']]').replace('.', `].[`)}]`;
191
200
  }
192
201
  escape(value) {
193
202
  if (value instanceof UnicodeString) {
@@ -1,4 +1,4 @@
1
- import { type AbstractSqlConnection, type CheckDef, type Column, type DatabaseSchema, type DatabaseTable, type Dictionary, type ForeignKey, type IndexDef, SchemaHelper, type Table, type TableDifference, type Transaction, type Type } from '@mikro-orm/sql';
1
+ import { type AbstractSqlConnection, type CheckDef, type Column, type DatabaseSchema, type DatabaseTable, type Dictionary, type ForeignKey, type IndexDef, SchemaHelper, type Table, type TableDifference, type SqlTriggerDef, type SqlRoutineDef, type Transaction, type Type } from '@mikro-orm/sql';
2
2
  /** Schema introspection helper for Microsoft SQL Server. */
3
3
  export declare class MsSqlSchemaHelper extends SchemaHelper {
4
4
  static readonly DEFAULT_VALUES: {
@@ -6,6 +6,8 @@ export declare class MsSqlSchemaHelper extends SchemaHelper {
6
6
  false: string[];
7
7
  'getdate()': string[];
8
8
  };
9
+ private static readonly AUTO_NOT_NULL_RE;
10
+ protected get bracketQuotedIdentifiers(): boolean;
9
11
  getManagementDbName(): string;
10
12
  getDropDatabaseSQL(name: string): string;
11
13
  disableForeignKeysSQL(): string;
@@ -23,6 +25,21 @@ export declare class MsSqlSchemaHelper extends SchemaHelper {
23
25
  private getEnumDefinitions;
24
26
  private getChecksSQL;
25
27
  getAllChecks(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
28
+ /** Generates SQL to create an MSSQL trigger. MSSQL supports AFTER and INSTEAD OF only. */
29
+ createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
30
+ /** Generates SQL to drop an MSSQL trigger. */
31
+ dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
32
+ routineParamReference(name: string): string;
33
+ /** T-SQL's `OUTPUT` covers both OUT and INOUT; `sys.parameters.is_output` is true for both. */
34
+ normaliseRoutineParamDirection(direction: 'in' | 'out' | 'inout'): 'in' | 'out' | 'inout';
35
+ createRoutine(routine: SqlRoutineDef): string;
36
+ dropRoutine(routine: SqlRoutineDef): string;
37
+ getAllRoutines(connection: AbstractSqlConnection): Promise<SqlRoutineDef[]>;
38
+ private getAllRoutineParams;
39
+ private unwrapMsSqlBody;
40
+ private getSchemaQualifiedName;
41
+ getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
42
+ getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
26
43
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[], ctx?: Transaction): Promise<void>;
27
44
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
28
45
  getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
@@ -7,12 +7,43 @@ export class MsSqlSchemaHelper extends SchemaHelper {
7
7
  false: ['0'],
8
8
  'getdate()': ['current_timestamp'],
9
9
  };
10
+ // `stripAutoNotNullFilter` unwraps balanced per-clause parens before calling `.exec`, so we
11
+ // only need to match the bare form here — previously the pattern allowed independently
12
+ // optional leading/trailing parens, which accepted unbalanced strings like `([col] IS NOT NULL`.
13
+ static AUTO_NOT_NULL_RE = /^\[([^\]]+)\]\s+IS\s+NOT\s+NULL$/i;
14
+ // MSSQL `filter_definition` and `where` predicates use `[…]` bracket-quoting for identifiers,
15
+ // so `splitTopLevelAnd` must treat `[` as opening a quoted span (otherwise `[some and col]`
16
+ // would split mid-identifier).
17
+ get bracketQuotedIdentifiers() {
18
+ return true;
19
+ }
10
20
  getManagementDbName() {
11
21
  return 'master';
12
22
  }
13
23
  getDropDatabaseSQL(name) {
24
+ // `set offline` rejects all connections including the issuing session, so there is no
25
+ // single-user race window where a torn-down pool connection can reconnect between the
26
+ // mode switch and the drop (SQL Server error 3702 "currently in use"). Dropping an
27
+ // offline database leaves the underlying `.mdf`/`.ldf` files behind though, which
28
+ // makes a subsequent `create database` with the same name fail with error 5170
29
+ // ("file already exists"). Capture the physical paths up front and call
30
+ // `master.sys.xp_delete_files` after the drop to clean them up.
14
31
  const quoted = this.quote(name);
15
- return `if db_id(${this.platform.quoteValue(name)}) is not null begin alter database ${quoted} set single_user with rollback immediate; drop database ${quoted}; end`;
32
+ const literal = this.platform.quoteValue(name);
33
+ return (`if db_id(${literal}) is not null begin ` +
34
+ `declare @drop_files table (path nvarchar(260)); ` +
35
+ `insert into @drop_files (path) select physical_name from sys.master_files where database_id = db_id(${literal}); ` +
36
+ `alter database ${quoted} set offline with rollback immediate; ` +
37
+ `drop database ${quoted}; ` +
38
+ `declare @drop_path nvarchar(260); ` +
39
+ `declare drop_files_cursor cursor local fast_forward for select path from @drop_files; ` +
40
+ `open drop_files_cursor; fetch next from drop_files_cursor into @drop_path; ` +
41
+ `while @@fetch_status = 0 begin ` +
42
+ `begin try exec master.sys.xp_delete_files @drop_path; end try begin catch end catch; ` +
43
+ `fetch next from drop_files_cursor into @drop_path; ` +
44
+ `end ` +
45
+ `close drop_files_cursor; deallocate drop_files_cursor; ` +
46
+ `end`);
16
47
  }
17
48
  disableForeignKeysSQL() {
18
49
  return `exec sp_MSforeachtable 'alter table ? nocheck constraint all';`;
@@ -83,7 +114,8 @@ export class MsSqlSchemaHelper extends SchemaHelper {
83
114
  numeric_scale as numeric_scale,
84
115
  datetime_precision as datetime_precision,
85
116
  character_maximum_length as character_maximum_length,
86
- columnproperty(sc.object_id, column_name, 'IsIdentity') is_identity
117
+ columnproperty(sc.object_id, column_name, 'IsIdentity') is_identity,
118
+ nullif(ic.collation_name, convert(nvarchar(128), databasepropertyex(db_name(), 'Collation'))) as collation_name
87
119
  from information_schema.columns ic
88
120
  inner join sys.columns sc on sc.name = ic.column_name and sc.object_id = object_id(ic.table_schema + '.' + ic.table_name)
89
121
  left join sys.computed_columns cmp on cmp.name = ic.column_name and cmp.object_id = object_id(ic.table_schema + '.' + ic.table_name)
@@ -135,6 +167,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
135
167
  precision: col.numeric_precision,
136
168
  scale: col.numeric_scale,
137
169
  comment: col.column_comment,
170
+ collation: col.collation_name ?? undefined,
138
171
  generated,
139
172
  });
140
173
  }
@@ -148,6 +181,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
148
181
  col.name as column_name,
149
182
  schema_name(t.schema_id) as schema_name,
150
183
  (case when filter_definition is not null then concat('where ', filter_definition) else null end) as expression,
184
+ filter_definition as filter_definition,
151
185
  ind.is_disabled as is_disabled,
152
186
  ind.type as index_type,
153
187
  ind.fill_factor as fill_factor,
@@ -192,15 +226,31 @@ export class MsSqlSchemaHelper extends SchemaHelper {
192
226
  if (index.fill_factor > 0) {
193
227
  indexDef.fillFactor = index.fill_factor;
194
228
  }
195
- if (index.column_name?.match(/[(): ,"'`]/) || index.expression?.match(/where /i)) {
196
- indexDef.expression = index.expression; // required for the `getCreateIndexSQL()` call
229
+ /* v8 ignore next: function-based / computed-column introspection path, same as pre-PR */
230
+ if (index.column_name?.match(/[(): ,"'`]/)) {
231
+ indexDef.expression = index.expression;
197
232
  indexDef.expression = this.getCreateIndexSQL(index.table_name, indexDef, !!index.expression);
198
233
  }
234
+ else if (index.filter_definition) {
235
+ // Auto-NOT-NULL stripping runs post-mapIndexes (needs the consolidated column list).
236
+ indexDef.where = index.filter_definition;
237
+ }
199
238
  ret[key] ??= [];
200
239
  ret[key].push(indexDef);
201
240
  }
202
241
  for (const key of Object.keys(ret)) {
203
242
  ret[key] = await this.mapIndexes(ret[key]);
243
+ for (const idx of ret[key]) {
244
+ if (idx.where) {
245
+ const stripped = this.stripAutoNotNullFilter(idx.where, idx.columnNames, MsSqlSchemaHelper.AUTO_NOT_NULL_RE);
246
+ if (stripped === '') {
247
+ delete idx.where;
248
+ }
249
+ else {
250
+ idx.where = stripped;
251
+ }
252
+ }
253
+ }
204
254
  }
205
255
  return ret;
206
256
  }
@@ -285,6 +335,193 @@ export class MsSqlSchemaHelper extends SchemaHelper {
285
335
  }
286
336
  return ret;
287
337
  }
338
+ /** Generates SQL to create an MSSQL trigger. MSSQL supports AFTER and INSTEAD OF only. */
339
+ createTrigger(table, trigger) {
340
+ if (trigger.expression) {
341
+ return trigger.expression;
342
+ }
343
+ /* v8 ignore next 3 */
344
+ if (trigger.timing === 'before') {
345
+ throw new Error(`MSSQL does not support BEFORE triggers. Use AFTER or INSTEAD OF for trigger "${trigger.name}".`);
346
+ }
347
+ const timing = trigger.timing.toUpperCase();
348
+ const events = trigger.events.map(e => e.toUpperCase()).join(', ');
349
+ const qualifiedName = this.getSchemaQualifiedName(table, trigger.name);
350
+ return `create trigger ${qualifiedName} on ${table.getQuotedName()} ${timing} ${events} as begin ${trigger.body}; end`;
351
+ }
352
+ /** Generates SQL to drop an MSSQL trigger. */
353
+ dropTrigger(table, trigger) {
354
+ return `drop trigger if exists ${this.getSchemaQualifiedName(table, trigger.name)}`;
355
+ }
356
+ routineParamReference(name) {
357
+ return `@${name}`;
358
+ }
359
+ /** T-SQL's `OUTPUT` covers both OUT and INOUT; `sys.parameters.is_output` is true for both. */
360
+ normaliseRoutineParamDirection(direction) {
361
+ return direction === 'out' ? 'inout' : direction;
362
+ }
363
+ createRoutine(routine) {
364
+ if (routine.expression) {
365
+ return routine.expression;
366
+ }
367
+ const qualifiedName = this.qualifiedRoutineName(routine);
368
+ const params = routine.params
369
+ .map(p => {
370
+ const dir = p.direction === 'out' || p.direction === 'inout' ? ' OUTPUT' : '';
371
+ return `@${p.name} ${p.type}${dir}`;
372
+ })
373
+ .join(', ');
374
+ const body = this.wrapRoutineBody(routine.body ?? '');
375
+ if (routine.type === 'procedure') {
376
+ return `create or alter procedure ${qualifiedName} ${params} as ${body}`;
377
+ }
378
+ const returnType = routine.returns?.type ?? 'nvarchar(max)';
379
+ return `create or alter function ${qualifiedName}(${params}) returns ${returnType} as ${body}`;
380
+ }
381
+ dropRoutine(routine) {
382
+ const kind = routine.type === 'procedure' ? 'procedure' : 'function';
383
+ return `drop ${kind} if exists ${this.qualifiedRoutineName(routine)}`;
384
+ }
385
+ async getAllRoutines(connection) {
386
+ const sql = `
387
+ select
388
+ s.name as schema_name,
389
+ o.name as name,
390
+ case
391
+ when o.type = 'P' then 'procedure'
392
+ when o.type in ('FN', 'IF', 'TF') then 'function'
393
+ end as kind,
394
+ m.definition as definition,
395
+ ep.value as comment
396
+ from sys.objects o
397
+ join sys.schemas s on s.schema_id = o.schema_id
398
+ join sys.sql_modules m on m.object_id = o.object_id
399
+ left join sys.extended_properties ep
400
+ on ep.major_id = o.object_id and ep.minor_id = 0 and ep.name = 'MS_Description'
401
+ where o.type in ('P', 'FN', 'IF', 'TF')
402
+ and o.is_ms_shipped = 0
403
+ `;
404
+ const [rows, paramsAndReturns] = await Promise.all([
405
+ connection.execute(sql),
406
+ this.getAllRoutineParams(connection),
407
+ ]);
408
+ const { params, returns } = paramsAndReturns;
409
+ return rows.map(row => ({
410
+ name: row.name,
411
+ schema: row.schema_name,
412
+ type: row.kind,
413
+ body: this.unwrapMsSqlBody(row.definition),
414
+ comment: row.comment ?? undefined,
415
+ params: params.get(`${row.schema_name}.${row.name}`) ?? [],
416
+ returns: row.kind === 'function'
417
+ ? (returns.get(`${row.schema_name}.${row.name}`) ?? { type: 'nvarchar(max)', nullable: true })
418
+ : undefined,
419
+ }));
420
+ }
421
+ async getAllRoutineParams(connection) {
422
+ // `parameter_id = 0` is the function's return type; positive IDs are formal parameters.
423
+ const sql = `
424
+ select
425
+ s.name as schema_name,
426
+ o.name as routine_name,
427
+ p.name as param_name,
428
+ type_name(p.user_type_id) as type,
429
+ p.is_output as is_output,
430
+ p.parameter_id as position
431
+ from sys.parameters p
432
+ join sys.objects o on o.object_id = p.object_id
433
+ join sys.schemas s on s.schema_id = o.schema_id
434
+ where o.type in ('P', 'FN', 'IF', 'TF')
435
+ and o.is_ms_shipped = 0
436
+ order by o.object_id, p.parameter_id
437
+ `;
438
+ const rows = await connection.execute(sql);
439
+ const params = new Map();
440
+ const returns = new Map();
441
+ for (const row of rows) {
442
+ const key = `${row.schema_name}.${row.routine_name}`;
443
+ if (row.position === 0) {
444
+ returns.set(key, { type: row.type, nullable: true });
445
+ continue;
446
+ }
447
+ if (!params.has(key)) {
448
+ params.set(key, []);
449
+ }
450
+ // is_output is true for both OUT and INOUT; we always report `inout`. See normaliseRoutineParamDirection.
451
+ params.get(key).push({
452
+ name: row.param_name.replace(/^@/, ''),
453
+ type: row.type,
454
+ direction: row.is_output ? 'inout' : 'in',
455
+ });
456
+ }
457
+ return { params, returns };
458
+ }
459
+ unwrapMsSqlBody(definition) {
460
+ const asMatch = /\bas\s+([\s\S]*)$/i.exec(definition);
461
+ return this.stripRoutineBody(asMatch ? asMatch[1] : definition);
462
+ }
463
+ getSchemaQualifiedName(table, name) {
464
+ const defaultSchema = this.platform.getDefaultSchemaName();
465
+ if (table.schema && table.schema !== defaultSchema) {
466
+ return `${this.quote(table.schema)}.${this.quote(name)}`;
467
+ }
468
+ return this.quote(name);
469
+ }
470
+ async getDatabaseCollation(connection, ctx) {
471
+ const [row] = await connection.execute(`select convert(nvarchar(128), databasepropertyex(db_name(), 'Collation')) as collation`, [], 'all', ctx);
472
+ return row?.collation;
473
+ }
474
+ async getAllTriggers(connection, tablesBySchemas) {
475
+ const conditions = [];
476
+ for (const [schema, tables] of tablesBySchemas) {
477
+ const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
478
+ const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
479
+ conditions.push(`(schema_name(p.schema_id) = ${schemaName} and p.name in (${names}))`);
480
+ }
481
+ const sql = `select t.name as trigger_name, schema_name(p.schema_id) as schema_name,
482
+ p.name as table_name, te.type_desc as event,
483
+ case when t.is_instead_of_trigger = 1 then 'INSTEAD OF' else 'AFTER' end as timing,
484
+ object_definition(t.object_id) as definition
485
+ from sys.triggers t
486
+ join sys.trigger_events te on t.object_id = te.object_id
487
+ join sys.objects p on t.parent_id = p.object_id
488
+ where (${conditions.join(' or ')})
489
+ order by t.name, te.type_desc`;
490
+ const allTriggers = await connection.execute(sql);
491
+ const ret = {};
492
+ const triggerMap = new Map();
493
+ for (const row of allTriggers) {
494
+ const key = this.getTableKey(row);
495
+ const dedupeKey = `${key}:${row.trigger_name}`;
496
+ const event = row.event.toLowerCase();
497
+ if (triggerMap.has(dedupeKey)) {
498
+ const existing = triggerMap.get(dedupeKey);
499
+ if (!existing.events.includes(event)) {
500
+ existing.events.push(event);
501
+ }
502
+ continue;
503
+ }
504
+ // Parse body from full trigger definition
505
+ let body = '';
506
+ if (row.definition) {
507
+ const bodyMatch = /\bas\s+begin\s+([\s\S]*)\s*end\s*;?\s*$/i.exec(row.definition);
508
+ if (bodyMatch) {
509
+ body = bodyMatch[1].trim().replace(/;\s*$/, '');
510
+ }
511
+ }
512
+ ret[key] ??= [];
513
+ const trigger = {
514
+ name: row.trigger_name,
515
+ timing: row.timing.toLowerCase(),
516
+ events: [event],
517
+ forEach: 'row', // MSSQL has no FOR EACH ROW/STATEMENT syntax; match the metadata default to avoid false diffs
518
+ body,
519
+ };
520
+ ret[key].push(trigger);
521
+ triggerMap.set(dedupeKey, trigger);
522
+ }
523
+ return ret;
524
+ }
288
525
  async loadInformationSchema(schema, connection, tables, schemas, ctx) {
289
526
  if (tables.length === 0) {
290
527
  return;
@@ -294,12 +531,18 @@ export class MsSqlSchemaHelper extends SchemaHelper {
294
531
  const indexes = await this.getAllIndexes(connection, tablesBySchema, ctx);
295
532
  const checks = await this.getAllChecks(connection, tablesBySchema, ctx);
296
533
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
534
+ const triggers = await this.getAllTriggers(connection, tablesBySchema);
535
+ const dbCollation = await this.getDatabaseCollation(connection, ctx);
297
536
  for (const t of tables) {
298
537
  const key = this.getTableKey(t);
299
538
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
539
+ table.collation = dbCollation;
300
540
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
301
541
  const enums = this.getEnumDefinitions(checks[key] ?? []);
302
542
  table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums);
543
+ if (triggers[key]) {
544
+ table.setTriggers(triggers[key]);
545
+ }
303
546
  }
304
547
  }
305
548
  getPreAlterTable(tableDiff, safe) {
@@ -415,7 +658,11 @@ export class MsSqlSchemaHelper extends SchemaHelper {
415
658
  else {
416
659
  col.push(columnType);
417
660
  }
418
- Utils.runIfNotEmpty(() => col.push('identity(1,1)'), column.autoincrement);
661
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
662
+ // `IDENTITY(1,1)` is rejected inside `ALTER COLUMN`, so it must only be emitted when the
663
+ // change actually involves the identity attribute or is a fresh column (no `changedProperties`).
664
+ Utils.runIfNotEmpty(() => col.push('identity(1,1)'), column.autoincrement &&
665
+ (!changedProperties || changedProperties.has('autoincrement') || changedProperties.has('type')));
419
666
  Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
420
667
  Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
421
668
  if (column.autoincrement &&
@@ -437,7 +684,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
437
684
  const [constraint] = this.getDropDefaultsSQL(table.name, [column], table.schema);
438
685
  parts.push(constraint);
439
686
  }
440
- if (changedProperties.has('type') || changedProperties.has('nullable')) {
687
+ if (changedProperties.has('type') || changedProperties.has('nullable') || changedProperties.has('collation')) {
441
688
  const col = this.createTableColumn(column, table, changedProperties);
442
689
  parts.push(`alter table ${table.getQuotedName()} alter column ${col}`);
443
690
  }
@@ -460,7 +707,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
460
707
  const clustered = index.clustered ? 'clustered ' : '';
461
708
  let sql = `create ${index.unique ? 'unique ' : ''}${clustered}index ${keyName} on ${this.quote(tableName)} `;
462
709
  if (index.expression && partialExpression) {
463
- return sql + `(${index.expression})` + this.getMsSqlIndexSuffix(index);
710
+ return sql + `(${index.expression})` + this.getMsSqlIndexSuffix(index) + this.getIndexWhereClause(index);
464
711
  }
465
712
  // Build column list with advanced options
466
713
  const columns = this.getIndexColumns(index);
@@ -469,7 +716,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
469
716
  if (index.include?.length) {
470
717
  sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
471
718
  }
472
- sql += this.getMsSqlIndexSuffix(index);
719
+ sql += this.getMsSqlIndexSuffix(index) + this.getIndexWhereClause(index);
473
720
  // Disabled indexes need to be created first, then disabled
474
721
  if (index.disabled) {
475
722
  sql += `;\nalter index ${keyName} on ${this.quote(tableName)} disable`;
@@ -480,19 +727,17 @@ export class MsSqlSchemaHelper extends SchemaHelper {
480
727
  * Build the column list for a MSSQL index.
481
728
  */
482
729
  getIndexColumns(index) {
483
- if (index.columns?.length) {
484
- return index.columns
485
- .map(col => {
486
- let colDef = this.quote(col.name);
487
- // MSSQL supports sort order
488
- if (col.sort) {
489
- colDef += ` ${col.sort}`;
490
- }
491
- return colDef;
492
- })
493
- .join(', ');
494
- }
495
- return index.columnNames.map(c => this.quote(c)).join(', ');
730
+ return index.columnNames
731
+ .map(name => {
732
+ const col = index.columns?.find(c => c.name === name);
733
+ let colDef = this.quote(name);
734
+ // MSSQL supports sort order
735
+ if (col?.sort) {
736
+ colDef += ` ${col.sort}`;
737
+ }
738
+ return colDef;
739
+ })
740
+ .join(', ');
496
741
  }
497
742
  /**
498
743
  * Get MSSQL-specific index WITH options like fill factor.
@@ -514,13 +759,16 @@ export class MsSqlSchemaHelper extends SchemaHelper {
514
759
  if (index.expression) {
515
760
  return index.expression;
516
761
  }
517
- const needsWhereClause = index.unique && index.columnNames.some(column => table.getColumn(column)?.nullable);
518
- if (!needsWhereClause) {
762
+ const needsAutoNotNull = index.unique && index.columnNames.some(column => table.getColumn(column)?.nullable);
763
+ if (!needsAutoNotNull) {
519
764
  return this.getCreateIndexSQL(table.getShortestName(), index);
520
765
  }
521
- // Generate without disabled suffix, insert WHERE clause, then re-add disabled
522
- let sql = this.getCreateIndexSQL(table.getShortestName(), { ...index, disabled: false });
523
- sql += ' where ' + index.columnNames.map(c => `${this.quote(c)} is not null`).join(' and ');
766
+ // Strip `index.where` from the base SQL so we can combine it with the auto NOT-NULL guard
767
+ // ourselves, wrapping the user predicate in parens to defuse operator precedence
768
+ // (a bare `a = 1 or b = 2 and [col] is not null` would bind as `a = 1 or (b = 2 and )`).
769
+ let sql = this.getCreateIndexSQL(table.getShortestName(), { ...index, where: undefined, disabled: false });
770
+ const autoNotNull = index.columnNames.map(c => `${this.quote(c)} is not null`).join(' and ');
771
+ sql += index.where ? ` where (${index.where}) and ${autoNotNull}` : ` where ${autoNotNull}`;
524
772
  if (index.disabled) {
525
773
  sql += `;\nalter index ${this.quote(index.keyName)} on ${table.getQuotedName()} disable`;
526
774
  }
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
@@ -19,6 +19,7 @@ Install a driver package for your database:
19
19
 
20
20
  ```sh
21
21
  npm install @mikro-orm/postgresql # PostgreSQL
22
+ npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
22
23
  npm install @mikro-orm/mysql # MySQL
23
24
  npm install @mikro-orm/mariadb # MariaDB
24
25
  npm install @mikro-orm/sqlite # SQLite
package/index.d.ts CHANGED
@@ -5,3 +5,7 @@ export * from './MsSqlPlatform.js';
5
5
  export * from './MsSqlSchemaHelper.js';
6
6
  export * from './UnicodeStringType.js';
7
7
  export { MsSqlMikroORM as MikroORM, type MsSqlOptions as Options, defineMsSqlConfig as defineConfig, } from './MsSqlMikroORM.js';
8
+ import { type AbstractSqlDriver, SqlEntityManager } from '@mikro-orm/sql';
9
+ import type { MsSqlDriver } from './MsSqlDriver.js';
10
+ export type EntityManager<Driver extends AbstractSqlDriver = MsSqlDriver> = SqlEntityManager<Driver>;
11
+ export declare const EntityManager: typeof SqlEntityManager;
package/index.js CHANGED
@@ -5,3 +5,5 @@ export * from './MsSqlPlatform.js';
5
5
  export * from './MsSqlSchemaHelper.js';
6
6
  export * from './UnicodeStringType.js';
7
7
  export { MsSqlMikroORM as MikroORM, defineMsSqlConfig as defineConfig, } from './MsSqlMikroORM.js';
8
+ import { SqlEntityManager } from '@mikro-orm/sql';
9
+ export const EntityManager = SqlEntityManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/mssql",
3
- "version": "7.1.0-dev.5",
3
+ "version": "7.1.0-dev.50",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -47,17 +47,17 @@
47
47
  "copy": "node ../../scripts/copy.mjs"
48
48
  },
49
49
  "dependencies": {
50
- "@mikro-orm/sql": "7.1.0-dev.5",
51
- "kysely": "0.28.16",
50
+ "@mikro-orm/sql": "7.1.0-dev.50",
51
+ "kysely": "0.29.2",
52
52
  "tarn": "3.0.2",
53
53
  "tedious": "19.2.1",
54
54
  "tsqlstring": "1.0.1"
55
55
  },
56
56
  "devDependencies": {
57
- "@mikro-orm/core": "^7.0.11"
57
+ "@mikro-orm/core": "^7.0.17"
58
58
  },
59
59
  "peerDependencies": {
60
- "@mikro-orm/core": "7.1.0-dev.5"
60
+ "@mikro-orm/core": "7.1.0-dev.50"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">= 22.17.0"