@mikro-orm/mssql 7.1.0-dev.2 → 7.1.0-dev.21

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.
@@ -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 */
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);
@@ -136,6 +139,7 @@ export class MsSqlPlatform extends AbstractSqlPlatform {
136
139
  return 'uniqueidentifier';
137
140
  }
138
141
  validateMetadata(meta) {
142
+ super.validateMetadata(meta);
139
143
  for (const prop of meta.props) {
140
144
  if ((prop.runtimeType === 'string' || ['string', 'nvarchar'].includes(prop.type)) &&
141
145
  !['uuid'].includes(prop.type) &&
@@ -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 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,12 @@ 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
+ private getSchemaQualifiedName;
33
+ getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
26
34
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[], ctx?: Transaction): Promise<void>;
27
35
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
28
36
  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';`;
@@ -148,6 +179,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
148
179
  col.name as column_name,
149
180
  schema_name(t.schema_id) as schema_name,
150
181
  (case when filter_definition is not null then concat('where ', filter_definition) else null end) as expression,
182
+ filter_definition as filter_definition,
151
183
  ind.is_disabled as is_disabled,
152
184
  ind.type as index_type,
153
185
  ind.fill_factor as fill_factor,
@@ -192,15 +224,31 @@ export class MsSqlSchemaHelper extends SchemaHelper {
192
224
  if (index.fill_factor > 0) {
193
225
  indexDef.fillFactor = index.fill_factor;
194
226
  }
195
- if (index.column_name?.match(/[(): ,"'`]/) || index.expression?.match(/where /i)) {
196
- indexDef.expression = index.expression; // required for the `getCreateIndexSQL()` call
227
+ /* v8 ignore next: function-based / computed-column introspection path, same as pre-PR */
228
+ if (index.column_name?.match(/[(): ,"'`]/)) {
229
+ indexDef.expression = index.expression;
197
230
  indexDef.expression = this.getCreateIndexSQL(index.table_name, indexDef, !!index.expression);
198
231
  }
232
+ else if (index.filter_definition) {
233
+ // Auto-NOT-NULL stripping runs post-mapIndexes (needs the consolidated column list).
234
+ indexDef.where = index.filter_definition;
235
+ }
199
236
  ret[key] ??= [];
200
237
  ret[key].push(indexDef);
201
238
  }
202
239
  for (const key of Object.keys(ret)) {
203
240
  ret[key] = await this.mapIndexes(ret[key]);
241
+ for (const idx of ret[key]) {
242
+ if (idx.where) {
243
+ const stripped = this.stripAutoNotNullFilter(idx.where, idx.columnNames, MsSqlSchemaHelper.AUTO_NOT_NULL_RE);
244
+ if (stripped === '') {
245
+ delete idx.where;
246
+ }
247
+ else {
248
+ idx.where = stripped;
249
+ }
250
+ }
251
+ }
204
252
  }
205
253
  return ret;
206
254
  }
@@ -285,6 +333,82 @@ export class MsSqlSchemaHelper extends SchemaHelper {
285
333
  }
286
334
  return ret;
287
335
  }
336
+ /** Generates SQL to create an MSSQL trigger. MSSQL supports AFTER and INSTEAD OF only. */
337
+ createTrigger(table, trigger) {
338
+ if (trigger.expression) {
339
+ return trigger.expression;
340
+ }
341
+ /* v8 ignore next 3 */
342
+ if (trigger.timing === 'before') {
343
+ throw new Error(`MSSQL does not support BEFORE triggers. Use AFTER or INSTEAD OF for trigger "${trigger.name}".`);
344
+ }
345
+ const timing = trigger.timing.toUpperCase();
346
+ const events = trigger.events.map(e => e.toUpperCase()).join(', ');
347
+ const qualifiedName = this.getSchemaQualifiedName(table, trigger.name);
348
+ return `create trigger ${qualifiedName} on ${table.getQuotedName()} ${timing} ${events} as begin ${trigger.body}; end`;
349
+ }
350
+ /** Generates SQL to drop an MSSQL trigger. */
351
+ dropTrigger(table, trigger) {
352
+ return `drop trigger if exists ${this.getSchemaQualifiedName(table, trigger.name)}`;
353
+ }
354
+ getSchemaQualifiedName(table, name) {
355
+ const defaultSchema = this.platform.getDefaultSchemaName();
356
+ if (table.schema && table.schema !== defaultSchema) {
357
+ return `${this.quote(table.schema)}.${this.quote(name)}`;
358
+ }
359
+ return this.quote(name);
360
+ }
361
+ async getAllTriggers(connection, tablesBySchemas) {
362
+ const conditions = [];
363
+ for (const [schema, tables] of tablesBySchemas) {
364
+ const names = tables.map(t => this.platform.quoteValue(t.table_name)).join(', ');
365
+ const schemaName = this.platform.quoteValue(schema ?? this.platform.getDefaultSchemaName());
366
+ conditions.push(`(schema_name(p.schema_id) = ${schemaName} and p.name in (${names}))`);
367
+ }
368
+ const sql = `select t.name as trigger_name, schema_name(p.schema_id) as schema_name,
369
+ p.name as table_name, te.type_desc as event,
370
+ case when t.is_instead_of_trigger = 1 then 'INSTEAD OF' else 'AFTER' end as timing,
371
+ object_definition(t.object_id) as definition
372
+ from sys.triggers t
373
+ join sys.trigger_events te on t.object_id = te.object_id
374
+ join sys.objects p on t.parent_id = p.object_id
375
+ where (${conditions.join(' or ')})
376
+ order by t.name, te.type_desc`;
377
+ const allTriggers = await connection.execute(sql);
378
+ const ret = {};
379
+ const triggerMap = new Map();
380
+ for (const row of allTriggers) {
381
+ const key = this.getTableKey(row);
382
+ const dedupeKey = `${key}:${row.trigger_name}`;
383
+ const event = row.event.toLowerCase();
384
+ if (triggerMap.has(dedupeKey)) {
385
+ const existing = triggerMap.get(dedupeKey);
386
+ if (!existing.events.includes(event)) {
387
+ existing.events.push(event);
388
+ }
389
+ continue;
390
+ }
391
+ // Parse body from full trigger definition
392
+ let body = '';
393
+ if (row.definition) {
394
+ const bodyMatch = /\bas\s+begin\s+([\s\S]*)\s*end\s*;?\s*$/i.exec(row.definition);
395
+ if (bodyMatch) {
396
+ body = bodyMatch[1].trim().replace(/;\s*$/, '');
397
+ }
398
+ }
399
+ ret[key] ??= [];
400
+ const trigger = {
401
+ name: row.trigger_name,
402
+ timing: row.timing.toLowerCase(),
403
+ events: [event],
404
+ forEach: 'row', // MSSQL has no FOR EACH ROW/STATEMENT syntax; match the metadata default to avoid false diffs
405
+ body,
406
+ };
407
+ ret[key].push(trigger);
408
+ triggerMap.set(dedupeKey, trigger);
409
+ }
410
+ return ret;
411
+ }
288
412
  async loadInformationSchema(schema, connection, tables, schemas, ctx) {
289
413
  if (tables.length === 0) {
290
414
  return;
@@ -294,12 +418,16 @@ export class MsSqlSchemaHelper extends SchemaHelper {
294
418
  const indexes = await this.getAllIndexes(connection, tablesBySchema, ctx);
295
419
  const checks = await this.getAllChecks(connection, tablesBySchema, ctx);
296
420
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
421
+ const triggers = await this.getAllTriggers(connection, tablesBySchema);
297
422
  for (const t of tables) {
298
423
  const key = this.getTableKey(t);
299
424
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
300
425
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
301
426
  const enums = this.getEnumDefinitions(checks[key] ?? []);
302
427
  table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums);
428
+ if (triggers[key]) {
429
+ table.setTriggers(triggers[key]);
430
+ }
303
431
  }
304
432
  }
305
433
  getPreAlterTable(tableDiff, safe) {
@@ -460,7 +588,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
460
588
  const clustered = index.clustered ? 'clustered ' : '';
461
589
  let sql = `create ${index.unique ? 'unique ' : ''}${clustered}index ${keyName} on ${this.quote(tableName)} `;
462
590
  if (index.expression && partialExpression) {
463
- return sql + `(${index.expression})` + this.getMsSqlIndexSuffix(index);
591
+ return sql + `(${index.expression})` + this.getMsSqlIndexSuffix(index) + this.getIndexWhereClause(index);
464
592
  }
465
593
  // Build column list with advanced options
466
594
  const columns = this.getIndexColumns(index);
@@ -469,7 +597,7 @@ export class MsSqlSchemaHelper extends SchemaHelper {
469
597
  if (index.include?.length) {
470
598
  sql += ` include (${index.include.map(c => this.quote(c)).join(', ')})`;
471
599
  }
472
- sql += this.getMsSqlIndexSuffix(index);
600
+ sql += this.getMsSqlIndexSuffix(index) + this.getIndexWhereClause(index);
473
601
  // Disabled indexes need to be created first, then disabled
474
602
  if (index.disabled) {
475
603
  sql += `;\nalter index ${keyName} on ${this.quote(tableName)} disable`;
@@ -514,13 +642,16 @@ export class MsSqlSchemaHelper extends SchemaHelper {
514
642
  if (index.expression) {
515
643
  return index.expression;
516
644
  }
517
- const needsWhereClause = index.unique && index.columnNames.some(column => table.getColumn(column)?.nullable);
518
- if (!needsWhereClause) {
645
+ const needsAutoNotNull = index.unique && index.columnNames.some(column => table.getColumn(column)?.nullable);
646
+ if (!needsAutoNotNull) {
519
647
  return this.getCreateIndexSQL(table.getShortestName(), index);
520
648
  }
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 ');
649
+ // Strip `index.where` from the base SQL so we can combine it with the auto NOT-NULL guard
650
+ // ourselves, wrapping the user predicate in parens to defuse operator precedence
651
+ // (a bare `a = 1 or b = 2 and [col] is not null` would bind as `a = 1 or (b = 2 and )`).
652
+ let sql = this.getCreateIndexSQL(table.getShortestName(), { ...index, where: undefined, disabled: false });
653
+ const autoNotNull = index.columnNames.map(c => `${this.quote(c)} is not null`).join(' and ');
654
+ sql += index.where ? ` where (${index.where}) and ${autoNotNull}` : ` where ${autoNotNull}`;
524
655
  if (index.disabled) {
525
656
  sql += `;\nalter index ${this.quote(index.keyName)} on ${table.getQuotedName()} disable`;
526
657
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/mssql",
3
- "version": "7.1.0-dev.2",
3
+ "version": "7.1.0-dev.21",
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.2",
50
+ "@mikro-orm/sql": "7.1.0-dev.21",
51
51
  "kysely": "0.28.16",
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.12"
58
58
  },
59
59
  "peerDependencies": {
60
- "@mikro-orm/core": "7.1.0-dev.2"
60
+ "@mikro-orm/core": "7.1.0-dev.21"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">= 22.17.0"