import { LockMode, raw, RawQueryFragment, Utils } from '@mikro-orm/core'; import { QueryType } from './enums.js'; /** @internal */ export class NativeQueryBuilder { platform; type; parts = []; params = []; options = {}; constructor(platform) { this.platform = platform; } select(fields) { this.type = QueryType.SELECT; this.options.select ??= []; this.options.select.push(...Utils.asArray(fields)); return this; } count(fields = '*', distinct) { this.type = QueryType.COUNT; this.options.select = Utils.asArray(fields); this.options.distinct = distinct; return this; } into(tableName, options) { return this.from(tableName, options); } from(tableName, options) { if (tableName instanceof NativeQueryBuilder) { tableName = tableName.toRaw(); } if (typeof tableName === 'string') { const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' '; const alias = options?.alias ? `${asKeyword}${this.platform.quoteIdentifier(options.alias)}` : ''; const schema = options?.schema && options.schema !== this.platform.getDefaultSchemaName() ? `${options.schema}.` : ''; tableName = this.quote(schema + tableName) + alias; } this.options.tableName = tableName; this.options.indexHint = options?.indexHint; return this; } where(sql, params) { this.options.where = { sql, params }; return this; } having(sql, params) { this.options.having = { sql, params }; return this; } groupBy(groupBy) { this.options.groupBy = groupBy; return this; } join(sql, params) { this.options.joins ??= []; this.options.joins.push({ sql, params }); return this; } orderBy(orderBy) { this.options.orderBy = orderBy; return this; } /** * The sub-query is compiled eagerly at call time — later mutations to the * sub-query builder will not be reflected in this CTE. */ with(name, query, options) { return this.addCte(name, query, options); } /** * Adds a recursive CTE (`WITH RECURSIVE` on PostgreSQL/MySQL/SQLite, plain `WITH` on MSSQL). * The sub-query is compiled eagerly — later mutations will not be reflected. */ withRecursive(name, query, options) { return this.addCte(name, query, options, true); } addCte(name, query, options, recursive) { this.options.ctes ??= []; if (this.options.ctes.some(cte => cte.name === name)) { throw new Error(`CTE with name '${name}' already exists`); } const { sql, params } = query instanceof NativeQueryBuilder ? query.compile() : { sql: query.sql, params: [...query.params] }; this.options.ctes.push({ name, sql, params, recursive, columns: options?.columns, materialized: options?.materialized, }); return this; } toString() { const { sql, params } = this.compile(); return this.platform.formatQuery(sql, params); } compile() { if (!this.type) { throw new Error('No query type provided'); } this.parts.length = 0; this.params.length = 0; if (this.options.comment) { this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`)); } this.compileCtes(); 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; } this.addOnConflictClause(); if (this.options.returning && this.platform.usesReturningStatement()) { const fields = this.options.returning.map(field => this.quote(field)); this.parts.push(`returning ${fields.join(', ')}`); } this.addLockClause(); return this.combineParts(); } addLockClause() { if (!this.options.lockMode) { return; } if ( [LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_PARTIAL_READ, LockMode.PESSIMISTIC_READ_OR_FAIL].includes( this.options.lockMode, ) ) { this.parts.push('for share'); } if ( [LockMode.PESSIMISTIC_WRITE, LockMode.PESSIMISTIC_PARTIAL_WRITE, LockMode.PESSIMISTIC_WRITE_OR_FAIL].includes( this.options.lockMode, ) ) { this.parts.push('for update'); } if (this.options.lockTables?.length) { const fields = this.options.lockTables.map(field => this.quote(field)); this.parts.push(`of ${fields.join(', ')}`); } if ([LockMode.PESSIMISTIC_PARTIAL_READ, LockMode.PESSIMISTIC_PARTIAL_WRITE].includes(this.options.lockMode)) { this.parts.push('skip locked'); } if ([LockMode.PESSIMISTIC_READ_OR_FAIL, LockMode.PESSIMISTIC_WRITE_OR_FAIL].includes(this.options.lockMode)) { this.parts.push('nowait'); } } addOnConflictClause() { const clause = this.options.onConflict; if (!clause) { return; } this.parts.push('on conflict'); 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 => this.quote(field)); this.parts.push(`(${fields.join(', ')})`); } if (clause.ignore) { this.parts.push('do nothing'); } if (Utils.isObject(clause.merge)) { this.parts.push('do update set'); const fields = Object.keys(clause.merge).map(field => { this.params.push(clause.merge[field]); return `${this.quote(field)} = ?`; }); this.parts.push(fields.join(', ')); } else if (clause.merge) { this.parts.push('do update set'); if (clause.merge.length) { const fields = clause.merge.map(field => `${this.quote(field)} = excluded.${this.quote(field)}`); this.parts.push(fields.join(', ')); } else { const dataAsArray = Utils.asArray(this.options.data); const keys = Object.keys(dataAsArray[0]); const fields = keys.map(field => `${this.quote(field)} = excluded.${this.quote(field)}`); this.parts.push(fields.join(', ')); } } if (clause.where) { this.parts.push(`where ${clause.where.sql}`); this.params.push(...clause.where.params); } } combineParts() { let sql = this.parts.join(' '); if (this.options.wrap) { const [a, b] = this.options.wrap; sql = `${a}${sql}${b}`; } return { sql, params: this.params }; } limit(limit) { this.options.limit = limit; return this; } offset(offset) { this.options.offset = offset; return this; } insert(data) { this.type = QueryType.INSERT; this.options.data = data; return this; } update(data) { this.type = QueryType.UPDATE; this.options.data ??= {}; Object.assign(this.options.data, data); return this; } delete() { this.type = QueryType.DELETE; return this; } truncate() { this.type = QueryType.TRUNCATE; return this; } distinct() { this.options.distinct = true; return this; } distinctOn(fields) { this.options.distinctOn = fields; return this; } onConflict(options) { this.options.onConflict = options; return options; } returning(fields) { this.options.returning = fields; return this; } lockMode(lockMode, lockTables) { this.options.lockMode = lockMode; this.options.lockTables = lockTables; return this; } comment(comment) { this.options.comment ??= []; this.options.comment.push(...Utils.asArray(comment)); return this; } hintComment(comment) { this.options.hintComment ??= []; this.options.hintComment.push(...Utils.asArray(comment)); return this; } setFlags(flags) { this.options.flags = flags; return this; } clear(clause) { delete this.options[clause]; return this; } wrap(prefix, suffix) { this.options.wrap = [prefix, suffix]; return this; } as(alias) { this.wrap('(', `) as ${this.platform.quoteIdentifier(alias)}`); return this; } toRaw() { const { sql, params } = this.compile(); return raw(sql, params); } compileSelect() { this.parts.push('select'); this.addHintComment(); this.parts.push(`${this.getFields()} from ${this.getTableName()}`); 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.options.where.params.forEach(p => this.params.push(p)); } 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.limit != null) { this.parts.push(`limit ?`); this.params.push(this.options.limit); } if (this.options.offset != null) { this.parts.push(`offset ?`); this.params.push(this.options.offset); } } getFields() { if (!this.options.select || this.options.select.length === 0) { throw new Error('No fields selected'); } let fields = this.options.select.map(field => this.quote(field)).join(', '); if (this.options.distinct) { fields = `distinct ${fields}`; } else if (this.options.distinctOn) { fields = `distinct on (${this.options.distinctOn.map(field => this.quote(field)).join(', ')}) ${fields}`; } if (this.type === QueryType.COUNT) { fields = `count(${fields}) as ${this.quote('count')}`; } return fields; } 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(); this.parts.push(parts.join(', ')); } addOutputClause(type) { if (this.options.returning && this.platform.usesOutputStatement()) { const fields = this.options.returning.map(field => `${type}.${this.quote(field)}`); this.parts.push(`output ${fields.join(', ')}`); } } processInsertData() { const dataAsArray = Utils.asArray(this.options.data); const keys = Object.keys(dataAsArray[0]); const values = keys.map(() => '?'); const parts = []; this.parts.push(`(${keys.map(key => this.quote(key)).join(', ')})`); this.addOutputClause('inserted'); this.parts.push('values'); for (const data of dataAsArray) { for (const key of keys) { if (typeof data[key] === 'undefined') { this.params.push(this.platform.usesDefaultKeyword() ? raw('default') : null); } else { this.params.push(data[key]); } } parts.push(`(${values.join(', ')})`); } return parts; } compileUpdate() { if (!this.options.data || Object.keys(this.options.data).length === 0) { throw new Error('No data provided'); } this.parts.push('update'); this.addHintComment(); this.parts.push(this.getTableName()); if (this.options.joins) { for (const join of this.options.joins) { this.parts.push(join.sql); this.params.push(...join.params); } } this.parts.push('set'); if (this.options.data) { const parts = []; for (const key of Object.keys(this.options.data)) { parts.push(`${this.quote(key)} = ?`); this.params.push(this.options.data[key]); } this.parts.push(parts.join(', ')); } this.addOutputClause('inserted'); if (this.options.where?.sql.trim()) { this.parts.push(`where ${this.options.where.sql}`); this.params.push(...this.options.where.params); } } compileDelete() { this.parts.push('delete'); this.addHintComment(); this.parts.push(`from ${this.getTableName()}`); this.addOutputClause('deleted'); if (this.options.where?.sql.trim()) { this.parts.push(`where ${this.options.where.sql}`); this.params.push(...this.options.where.params); } } compileTruncate() { const sql = `truncate table ${this.getTableName()}`; this.parts.push(sql); } addHintComment() { if (this.options.hintComment) { this.parts.push(`/*+ ${this.options.hintComment.join(' ')} */`); } } compileCtes() { const ctes = this.options.ctes; if (!ctes || ctes.length === 0) { return; } const hasRecursive = ctes.some(cte => cte.recursive); const keyword = this.getCteKeyword(hasRecursive); const cteParts = []; for (const cte of ctes) { let part = this.quote(cte.name); if (cte.columns?.length) { part += ` (${cte.columns.map(c => this.quote(c)).join(', ')})`; } part += ' as'; if (cte.materialized === true) { part += ' materialized'; } else if (cte.materialized === false) { part += ' not materialized'; } part += ` (${cte.sql})`; this.params.push(...cte.params); cteParts.push(part); } this.parts.push(`${keyword} ${cteParts.join(', ')}`); } getCteKeyword(hasRecursive) { return hasRecursive ? 'with recursive' : 'with'; } getTableName() { if (!this.options.tableName) { throw new Error('No table name provided'); } const indexHint = this.options.indexHint ? ' ' + this.options.indexHint : ''; if (this.options.tableName instanceof RawQueryFragment) { this.params.push(...this.options.tableName.params); return this.options.tableName.sql + indexHint; } return this.options.tableName + indexHint; } quote(id) { if (id instanceof RawQueryFragment) { return this.platform.formatQuery(id.sql, id.params); } if (id instanceof NativeQueryBuilder) { const { sql, params } = id.compile(); return this.platform.formatQuery(sql, params); } if (id.endsWith('.*')) { const schema = this.platform.quoteIdentifier(id.substring(0, id.indexOf('.'))); return schema + '.*'; } if (id.toLowerCase().includes(' as ')) { const parts = id.split(/ as /i); const a = this.platform.quoteIdentifier(parts[0]); const b = this.platform.quoteIdentifier(parts[1]); const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' '; return `${a}${asKeyword}${b}`; } if (id === '*') { return id; } return this.platform.quoteIdentifier(id); } }