import { LockMode, QueryFlag, RawQueryFragment, Utils } from '@mikro-orm/core'; import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js'; import { QueryType } from '../../query/enums.js'; /** @internal */ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder { compile() { if (!this.type) { throw new Error('No query type provided'); } this.parts.length = 0; this.params.length = 0; if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) { this.parts.push(`set identity_insert ${this.getTableName()} on;`); } const { prefix, suffix } = this.appendOutputTable(); if (prefix) { this.parts.push(prefix); } if (this.options.comment) { this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`)); } this.compileCtes(); if (this.options.onConflict && !Utils.isEmpty(Utils.asArray(this.options.data)[0])) { this.compileUpsert(); } else { switch (this.type) { case QueryType.SELECT: case QueryType.COUNT: this.compileSelect(); break; case QueryType.INSERT: this.compileInsert(); break; case QueryType.UPDATE: this.compileUpdate(); break; case QueryType.DELETE: this.compileDelete(); break; case QueryType.TRUNCATE: this.compileTruncate(); break; } if (suffix) { this.parts[this.parts.length - 1] += ';'; this.parts.push(suffix); } else if ([QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE].includes(this.type)) { this.parts[this.parts.length - 1] += '; select @@rowcount;'; } } if (this.options.flags?.has(QueryFlag.IDENTITY_INSERT)) { this.parts.push(`set identity_insert ${this.getTableName()} off;`); } return this.combineParts(); } compileInsert() { if (!this.options.data) { throw new Error('No data provided'); } this.parts.push('insert'); this.addHintComment(); this.parts.push(`into ${this.getTableName()}`); if (Object.keys(this.options.data).length === 0) { this.addOutputClause('inserted'); this.parts.push('default values'); return; } const parts = this.processInsertData(); if (this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) { this.parts[this.parts.length - 2] += ' into #out '; } this.parts.push(parts.join(', ')); } appendOutputTable() { if (!this.options.flags?.has(QueryFlag.OUTPUT_TABLE)) { return { prefix: '', suffix: '' }; } const returningFields = this.options.returning; const selections = returningFields.map(field => `[t].${this.platform.quoteIdentifier(field)}`).join(','); return { prefix: `select top(0) ${selections} into #out from ${this.getTableName()} as t left join ${this.getTableName()} on 0 = 1;`, suffix: `select ${selections} from #out as t; drop table #out`, }; } compileUpsert() { const clause = this.options.onConflict; const dataAsArray = Utils.asArray(this.options.data); const keys = Object.keys(dataAsArray[0]); const values = keys.map(() => '?'); const parts = []; for (const data of dataAsArray) { for (const key of keys) { this.params.push(data[key]); } parts.push(`(${values.join(', ')})`); } this.parts.push(`merge into ${this.getTableName()}`); this.parts.push(`using (values ${parts.join(', ')}) as tsource(${keys.map(key => this.quote(key)).join(', ')})`); if (clause.fields instanceof RawQueryFragment) { this.parts.push(clause.fields.sql); this.params.push(...clause.fields.params); } else if (clause.fields.length > 0) { const fields = clause.fields.map(field => { const col = this.quote(field); return `${this.getTableName()}.${col} = tsource.${col}`; }); this.parts.push(`on ${fields.join(' and ')}`); } const sourceColumns = keys.map(field => `tsource.${this.quote(field)}`).join(', '); const destinationColumns = keys.map(field => this.quote(field)).join(', '); this.parts.push(`when not matched then insert (${destinationColumns}) values (${sourceColumns})`); if (!clause.ignore) { this.parts.push('when matched'); if (clause.where) { this.parts.push(`and ${clause.where.sql}`); this.params.push(...clause.where.params); } this.parts.push('then update set'); if (!clause.merge || Array.isArray(clause.merge)) { const parts = (clause.merge || keys) .filter(field => !Array.isArray(clause.fields) || !clause.fields.includes(field)) .map(column => `${this.quote(column)} = tsource.${this.quote(column)}`); this.parts.push(parts.join(', ')); } else if (typeof clause.merge === 'object') { const parts = Object.entries(clause.merge).map(([key, value]) => { this.params.push(value); return `${this.getTableName()}.${this.quote(key)} = ?`; }); this.parts.push(parts.join(', ')); } } this.addOutputClause('inserted'); this.parts[this.parts.length - 1] += ';'; } compileSelect() { this.parts.push('select'); if (this.options.limit != null && this.options.offset == null) { this.parts.push(`top (?)`); this.params.push(this.options.limit); } this.addHintComment(); this.parts.push(`${this.getFields()} from ${this.getTableName()}`); this.addLockClause(); if (this.options.joins) { for (const join of this.options.joins) { this.parts.push(join.sql); this.params.push(...join.params); } } if (this.options.where?.sql.trim()) { this.parts.push(`where ${this.options.where.sql}`); this.params.push(...this.options.where.params); } if (this.options.groupBy) { const fields = this.options.groupBy.map(field => this.quote(field)); this.parts.push(`group by ${fields.join(', ')}`); } if (this.options.having) { this.parts.push(`having ${this.options.having.sql}`); this.params.push(...this.options.having.params); } if (this.options.orderBy) { this.parts.push(`order by ${this.options.orderBy}`); } if (this.options.offset != null) { /* v8 ignore next */ if (!this.options.orderBy) { throw new Error('Order by clause is required for pagination'); } this.parts.push(`offset ? rows`); this.params.push(this.options.offset); if (this.options.limit != null) { this.parts.push(`fetch next ? rows only`); this.params.push(this.options.limit); } } } addLockClause() { if ( !this.options.lockMode || ![LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_WRITE].includes(this.options.lockMode) ) { return; } const map = { [LockMode.PESSIMISTIC_READ]: 'with (holdlock)', [LockMode.PESSIMISTIC_WRITE]: 'with (updlock)', }; if (this.options.lockMode !== LockMode.OPTIMISTIC) { this.parts.push(map[this.options.lockMode]); } } compileTruncate() { const tableName = this.getTableName(); const sql = `delete from ${tableName}; declare @count int = case @@rowcount when 0 then 1 else 0 end; dbcc checkident ('${tableName.replace(/[[\]]/g, '')}', reseed, @count)`; this.parts.push(sql); } /** MSSQL has no RECURSIVE keyword — CTEs are implicitly recursive. */ getCteKeyword(_hasRecursive) { return 'with'; } }