UNPKG

typeorm

Version:

Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.

1,028 lines • 51.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.InsertQueryBuilder = void 0; const uuid_1 = require("uuid"); const DriverUtils_1 = require("../driver/DriverUtils"); const error_1 = require("../error"); const InsertValuesMissingError_1 = require("../error/InsertValuesMissingError"); const ReturningStatementNotSupportedError_1 = require("../error/ReturningStatementNotSupportedError"); const BroadcasterResult_1 = require("../subscriber/BroadcasterResult"); const InstanceChecker_1 = require("../util/InstanceChecker"); const ObjectUtils_1 = require("../util/ObjectUtils"); const QueryBuilder_1 = require("./QueryBuilder"); const InsertResult_1 = require("./result/InsertResult"); const ReturningResultsEntityUpdator_1 = require("./ReturningResultsEntityUpdator"); /** * Allows to build complex sql queries in a fashion way and execute those queries. */ class InsertQueryBuilder extends QueryBuilder_1.QueryBuilder { constructor() { super(...arguments); this["@instanceof"] = Symbol.for("InsertQueryBuilder"); } // ------------------------------------------------------------------------- // Public Implemented Methods // ------------------------------------------------------------------------- /** * Gets generated SQL query without parameters being replaced. */ getQuery() { let sql = this.createComment(); sql += this.createCteExpression(); sql += this.createInsertExpression(); return this.replacePropertyNamesForTheWholeQuery(sql.trim()); } /** * Executes sql generated by query builder and returns raw database results. */ async execute() { // console.time(".value sets"); const valueSets = this.getValueSets(); // console.timeEnd(".value sets"); // If user passed empty array of entities then we don't need to do // anything. // // Fixes GitHub issues #3111 and #5734. If we were to let this through // we would run into problems downstream, like subscribers getting // invoked with the empty array where they expect an entity, and SQL // queries with an empty VALUES clause. if (valueSets.length === 0) return new InsertResult_1.InsertResult(); // console.time("QueryBuilder.execute"); // console.time(".database stuff"); const queryRunner = this.obtainQueryRunner(); let transactionStartedByUs = false; try { // start transaction if it was enabled if (this.expressionMap.useTransaction === true && queryRunner.isTransactionActive === false) { await queryRunner.startTransaction(); transactionStartedByUs = true; } // console.timeEnd(".database stuff"); // call before insertion methods in listeners and subscribers if (this.expressionMap.callListeners === true && this.expressionMap.mainAlias.hasMetadata) { const broadcastResult = new BroadcasterResult_1.BroadcasterResult(); valueSets.forEach((valueSet) => { queryRunner.broadcaster.broadcastBeforeInsertEvent(broadcastResult, this.expressionMap.mainAlias.metadata, valueSet); }); await broadcastResult.wait(); } let declareSql = null; let selectOutputSql = null; // if update entity mode is enabled we may need extra columns for the returning statement // console.time(".prepare returning statement"); const returningResultsEntityUpdator = new ReturningResultsEntityUpdator_1.ReturningResultsEntityUpdator(queryRunner, this.expressionMap); const returningColumns = []; if (Array.isArray(this.expressionMap.returning) && this.expressionMap.mainAlias.hasMetadata) { for (const columnPath of this.expressionMap.returning) { returningColumns.push(...this.expressionMap.mainAlias.metadata.findColumnsWithPropertyPath(columnPath)); } } if (this.expressionMap.updateEntity === true && this.expressionMap.mainAlias.hasMetadata) { if (!(valueSets.length > 1 && this.connection.driver.options.type === "oracle")) { this.expressionMap.extraReturningColumns = this.expressionMap.mainAlias.metadata.getInsertionReturningColumns(); } returningColumns.push(...this.expressionMap.extraReturningColumns.filter((c) => !returningColumns.includes(c))); } if (returningColumns.length > 0 && this.connection.driver.options.type === "mssql") { declareSql = this.connection.driver.buildTableVariableDeclaration("@OutputTable", returningColumns); selectOutputSql = `SELECT * FROM @OutputTable`; } // console.timeEnd(".prepare returning statement"); // execute query // console.time(".getting query and parameters"); const [insertSql, parameters] = this.getQueryAndParameters(); // console.timeEnd(".getting query and parameters"); // console.time(".query execution by database"); const statements = [declareSql, insertSql, selectOutputSql]; const sql = statements.filter((s) => s != null).join(";\n\n"); const queryResult = await queryRunner.query(sql, parameters, true); const insertResult = InsertResult_1.InsertResult.from(queryResult); // console.timeEnd(".query execution by database"); // load returning results and set them to the entity if entity updation is enabled if (this.expressionMap.updateEntity === true && this.expressionMap.mainAlias.hasMetadata) { // console.time(".updating entity"); await returningResultsEntityUpdator.insert(insertResult, valueSets); // console.timeEnd(".updating entity"); } // call after insertion methods in listeners and subscribers if (this.expressionMap.callListeners === true && this.expressionMap.mainAlias.hasMetadata) { const broadcastResult = new BroadcasterResult_1.BroadcasterResult(); valueSets.forEach((valueSet) => { queryRunner.broadcaster.broadcastAfterInsertEvent(broadcastResult, this.expressionMap.mainAlias.metadata, valueSet); }); await broadcastResult.wait(); } // close transaction if we started it // console.time(".commit"); if (transactionStartedByUs) { await queryRunner.commitTransaction(); } // console.timeEnd(".commit"); return insertResult; } catch (error) { // rollback transaction if we started it if (transactionStartedByUs) { try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { } } throw error; } finally { // console.time(".releasing connection"); if (queryRunner !== this.queryRunner) { // means we created our own query runner await queryRunner.release(); } // console.timeEnd(".releasing connection"); // console.timeEnd("QueryBuilder.execute"); } } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Specifies INTO which entity's table insertion will be executed. */ into(entityTarget, columns) { entityTarget = InstanceChecker_1.InstanceChecker.isEntitySchema(entityTarget) ? entityTarget.options.name : entityTarget; const mainAlias = this.createFromAlias(entityTarget); this.expressionMap.setMainAlias(mainAlias); this.expressionMap.insertColumns = columns || []; return this; } /** * Values needs to be inserted into table. */ values(values) { this.expressionMap.valuesSet = values; return this; } /** * Optional returning/output clause. */ output(output) { return this.returning(output); } /** * Optional returning/output clause. */ returning(returning) { // not all databases support returning/output cause if (!this.connection.driver.isReturningSqlSupported("insert")) { throw new ReturningStatementNotSupportedError_1.ReturningStatementNotSupportedError(); } this.expressionMap.returning = returning; return this; } /** * Indicates if entity must be updated after insertion operations. * This may produce extra query or use RETURNING / OUTPUT statement (depend on database). * Enabled by default. */ updateEntity(enabled) { this.expressionMap.updateEntity = enabled; return this; } /** * Adds additional ON CONFLICT statement supported in postgres and cockroach. * * @deprecated Use `orIgnore` or `orUpdate` */ onConflict(statement) { this.expressionMap.onConflict = statement; return this; } /** * Adds additional ignore statement supported in databases. */ orIgnore(statement = true) { this.expressionMap.onIgnore = !!statement; return this; } /** * Adds additional update statement supported in databases. */ orUpdate(statementOrOverwrite, conflictTarget, orUpdateOptions) { const { where, parameters } = orUpdateOptions?.overwriteCondition ?? {}; let wheres; if (where) { const condition = this.getWhereCondition(where); if (Array.isArray(condition) ? condition.length !== 0 : condition) wheres = [{ type: "simple", condition: condition }]; } if (parameters) this.setParameters(parameters); if (!Array.isArray(statementOrOverwrite)) { this.expressionMap.onUpdate = { conflict: statementOrOverwrite?.conflict_target, columns: statementOrOverwrite?.columns, overwrite: statementOrOverwrite?.overwrite, skipUpdateIfNoValuesChanged: orUpdateOptions?.skipUpdateIfNoValuesChanged, upsertType: orUpdateOptions?.upsertType, overwriteCondition: wheres, }; return this; } this.expressionMap.onUpdate = { overwrite: statementOrOverwrite, conflict: conflictTarget, skipUpdateIfNoValuesChanged: orUpdateOptions?.skipUpdateIfNoValuesChanged, indexPredicate: orUpdateOptions?.indexPredicate, upsertType: orUpdateOptions?.upsertType, overwriteCondition: wheres, }; return this; } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- /** * Creates INSERT express used to perform insert query. */ createInsertExpression() { if (this.expressionMap.onUpdate || this.expressionMap.onIgnore) { if ((this.expressionMap.onUpdate?.upsertType ?? "merge-into") === "merge-into" && this.connection.driver.supportedUpsertTypes.includes("merge-into")) return this.createMergeExpression(); } const tableName = this.getTableName(this.getMainTableName()); const tableOrAliasName = this.alias !== this.getMainTableName() ? this.escape(this.alias) : tableName; const valuesExpression = this.createValuesExpression(); // its important to get values before returning expression because oracle rely on native parameters and ordering of them is important const returningExpression = this.connection.driver.options.type === "oracle" && this.getValueSets().length > 1 ? null : this.createReturningExpression("insert"); // oracle doesnt support returning with multi-row insert const columnsExpression = this.createColumnNamesExpression(); let query = "INSERT "; if (this.expressionMap.onUpdate?.upsertType === "primary-key") { query = "UPSERT "; } if (DriverUtils_1.DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql") { query += `${this.expressionMap.onIgnore ? " IGNORE " : ""}`; } query += `INTO ${tableName}`; if (this.alias !== this.getMainTableName() && DriverUtils_1.DriverUtils.isPostgresFamily(this.connection.driver)) { query += ` AS "${this.alias}"`; } // add columns expression if (columnsExpression) { query += `(${columnsExpression})`; } else { if (!valuesExpression && (DriverUtils_1.DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql")) // special syntax for mysql DEFAULT VALUES insertion query += "()"; } // add OUTPUT expression if (returningExpression && this.connection.driver.options.type === "mssql") { query += ` OUTPUT ${returningExpression}`; } // add VALUES expression if (valuesExpression) { if ((this.connection.driver.options.type === "oracle" || this.connection.driver.options.type === "sap") && this.getValueSets().length > 1) { query += ` ${valuesExpression}`; } else { query += ` VALUES ${valuesExpression}`; } } else { if (DriverUtils_1.DriverUtils.isMySQLFamily(this.connection.driver) || this.connection.driver.options.type === "aurora-mysql") { // special syntax for mysql DEFAULT VALUES insertion query += " VALUES ()"; } else { query += ` DEFAULT VALUES`; } } if (this.expressionMap.onUpdate?.upsertType !== "primary-key") { if (this.connection.driver.supportedUpsertTypes.includes("on-conflict-do-update")) { if (this.expressionMap.onIgnore) { query += " ON CONFLICT DO NOTHING "; } else if (this.expressionMap.onConflict) { query += ` ON CONFLICT ${this.expressionMap.onConflict} `; } else if (this.expressionMap.onUpdate) { const { overwrite, columns, conflict, skipUpdateIfNoValuesChanged, indexPredicate, } = this.expressionMap.onUpdate; let conflictTarget = "ON CONFLICT"; if (Array.isArray(conflict)) { conflictTarget += ` ( ${conflict .map((column) => this.escape(column)) .join(", ")} )`; if (indexPredicate && !DriverUtils_1.DriverUtils.isPostgresFamily(this.connection.driver)) { throw new error_1.TypeORMError(`indexPredicate option is not supported by the current database driver`); } if (indexPredicate && DriverUtils_1.DriverUtils.isPostgresFamily(this.connection.driver)) { conflictTarget += ` WHERE ( ${indexPredicate} )`; } } else if (conflict) { conflictTarget += ` ON CONSTRAINT ${this.escape(conflict)}`; } const updatePart = []; if (Array.isArray(overwrite)) { updatePart.push(...overwrite.map((column) => `${this.escape(column)} = EXCLUDED.${this.escape(column)}`)); } else if (columns) { updatePart.push(...columns.map((column) => `${this.escape(column)} = :${column}`)); } if (updatePart.length > 0) { query += ` ${conflictTarget} DO UPDATE SET `; updatePart.push(...this.expressionMap .mainAlias.metadata.columns.filter((column) => column.isUpdateDate && !overwrite?.includes(column.databaseName) && !((this.connection.driver.options .type === "oracle" && this.getValueSets().length > 1) || DriverUtils_1.DriverUtils.isSQLiteFamily(this.connection.driver) || this.connection.driver.options .type === "sap" || this.connection.driver.options .type === "spanner")) .map((column) => `${this.escape(column.databaseName)} = DEFAULT`)); query += updatePart.join(", "); } if (Array.isArray(overwrite) && skipUpdateIfNoValuesChanged) { this.expressionMap.onUpdate.overwriteCondition ??= []; const wheres = overwrite.map((column) => ({ type: "or", condition: `${tableOrAliasName}.${this.escape(column)} IS DISTINCT FROM EXCLUDED.${this.escape(column)}`, })); this.expressionMap.onUpdate.overwriteCondition.push({ type: "and", condition: wheres, }); } if (DriverUtils_1.DriverUtils.isPostgresFamily(this.connection.driver) && this.expressionMap.onUpdate.overwriteCondition && this.expressionMap.onUpdate.overwriteCondition.length > 0) { query += ` WHERE ${this.createUpsertConditionExpression()}`; } } } else if (this.connection.driver.supportedUpsertTypes.includes("on-duplicate-key-update")) { if (this.expressionMap.onUpdate) { const { overwrite, columns } = this.expressionMap.onUpdate; if (Array.isArray(overwrite)) { query += " ON DUPLICATE KEY UPDATE "; query += overwrite .map((column) => `${this.escape(column)} = VALUES(${this.escape(column)})`) .join(", "); query += " "; } else if (Array.isArray(columns)) { query += " ON DUPLICATE KEY UPDATE "; query += columns .map((column) => `${this.escape(column)} = :${column}`) .join(", "); query += " "; } } } else { if (this.expressionMap.onUpdate) { throw new error_1.TypeORMError(`onUpdate is not supported by the current database driver`); } } } // add RETURNING expression if (returningExpression && (DriverUtils_1.DriverUtils.isPostgresFamily(this.connection.driver) || this.connection.driver.options.type === "oracle" || this.connection.driver.options.type === "cockroachdb" || DriverUtils_1.DriverUtils.isMySQLFamily(this.connection.driver))) { query += ` RETURNING ${returningExpression}`; } if (returningExpression && this.connection.driver.options.type === "spanner") { query += ` THEN RETURN ${returningExpression}`; } // Inserting a specific value for an auto-increment primary key in mssql requires enabling IDENTITY_INSERT // IDENTITY_INSERT can only be enabled for tables where there is an IDENTITY column and only if there is a value to be inserted (i.e. supplying DEFAULT is prohibited if IDENTITY_INSERT is enabled) if (this.connection.driver.options.type === "mssql" && this.expressionMap.mainAlias.hasMetadata && this.expressionMap .mainAlias.metadata.columns.filter((column) => this.expressionMap.insertColumns.length > 0 ? this.expressionMap.insertColumns.indexOf(column.propertyPath) !== -1 : column.isInsert) .some((column) => this.isOverridingAutoIncrementBehavior(column))) { query = `SET IDENTITY_INSERT ${tableName} ON; ${query}; SET IDENTITY_INSERT ${tableName} OFF`; } return query; } /** * Gets list of columns where values must be inserted to. */ getInsertedColumns() { if (!this.expressionMap.mainAlias.hasMetadata) return []; return this.expressionMap.mainAlias.metadata.columns.filter((column) => { // if user specified list of columns he wants to insert to, then we filter only them if (this.expressionMap.insertColumns.length) return (this.expressionMap.insertColumns.indexOf(column.propertyPath) !== -1); // skip columns the user doesn't want included by default if (!column.isInsert) { return false; } // if user did not specified such list then return all columns except auto-increment one // for Oracle we return auto-increment column as well because Oracle does not support DEFAULT VALUES expression if (column.isGenerated && column.generationStrategy === "increment" && !(this.connection.driver.options.type === "spanner") && !(this.connection.driver.options.type === "oracle") && !DriverUtils_1.DriverUtils.isSQLiteFamily(this.connection.driver) && !DriverUtils_1.DriverUtils.isMySQLFamily(this.connection.driver) && !(this.connection.driver.options.type === "aurora-mysql") && !(this.connection.driver.options.type === "mssql" && this.isOverridingAutoIncrementBehavior(column))) return false; return true; }); } /** * Creates a columns string where values must be inserted to for INSERT INTO expression. */ createColumnNamesExpression() { const columns = this.getInsertedColumns(); if (columns.length > 0) return columns .map((column) => this.escape(column.databaseName)) .join(", "); // in the case if there are no insert columns specified and table without metadata used // we get columns from the inserted value map, in the case if only one inserted map is specified if (!this.expressionMap.mainAlias.hasMetadata && !this.expressionMap.insertColumns.length) { const valueSets = this.getValueSets(); if (valueSets.length === 1) return Object.keys(valueSets[0]) .map((columnName) => this.escape(columnName)) .join(", "); } // get a table name and all column database names return this.expressionMap.insertColumns .map((columnName) => this.escape(columnName)) .join(", "); } /** * Creates list of values needs to be inserted in the VALUES expression. */ createValuesExpression() { const valueSets = this.getValueSets(); const columns = this.getInsertedColumns(); // if column metadatas are given then apply all necessary operations with values if (columns.length > 0) { let expression = ""; valueSets.forEach((valueSet, valueSetIndex) => { columns.forEach((column, columnIndex) => { if (columnIndex === 0) { if (this.connection.driver.options.type === "oracle" && valueSets.length > 1) { expression += " SELECT "; } else if (this.connection.driver.options.type === "sap" && valueSets.length > 1) { expression += " SELECT "; } else { expression += "("; } } expression += this.createColumnValueExpression(valueSets, valueSetIndex, column); if (columnIndex === columns.length - 1) { if (valueSetIndex === valueSets.length - 1) { if (["oracle", "sap"].includes(this.connection.driver.options.type) && valueSets.length > 1) { expression += " FROM " + this.connection.driver.dummyTableName; } else { expression += ")"; } } else { if (["oracle", "sap"].includes(this.connection.driver.options.type) && valueSets.length > 1) { expression += " FROM " + this.connection.driver.dummyTableName + " UNION ALL "; } else { expression += "), "; } } } else { expression += ", "; } }); }); if (expression === "()") return ""; return expression; } else { // for tables without metadata // get values needs to be inserted let expression = ""; valueSets.forEach((valueSet, insertionIndex) => { const columns = Object.keys(valueSet); columns.forEach((columnName, columnIndex) => { if (columnIndex === 0) { expression += "("; } const value = valueSet[columnName]; // support for SQL expressions in queries if (typeof value === "function") { expression += value(); // if value for this column was not provided then insert default value } else if (value === undefined) { if ((this.connection.driver.options.type === "oracle" && valueSets.length > 1) || DriverUtils_1.DriverUtils.isSQLiteFamily(this.connection.driver) || this.connection.driver.options.type === "sap" || this.connection.driver.options.type === "spanner") { expression += "NULL"; } else { expression += "DEFAULT"; } } else if (value === null && this.connection.driver.options.type === "spanner") { // just any other regular value } else { expression += this.createParameter(value); } if (columnIndex === Object.keys(valueSet).length - 1) { if (insertionIndex === valueSets.length - 1) { expression += ")"; } else { expression += "), "; } } else { expression += ", "; } }); }); if (expression === "()") return ""; return expression; } } /** * Gets array of values need to be inserted into the target table. */ getValueSets() { if (Array.isArray(this.expressionMap.valuesSet)) return this.expressionMap.valuesSet; if (ObjectUtils_1.ObjectUtils.isObject(this.expressionMap.valuesSet)) return [this.expressionMap.valuesSet]; throw new InsertValuesMissingError_1.InsertValuesMissingError(); } /** * Checks if column is an auto-generated primary key, but the current insertion specifies a value for it. * * @param column */ isOverridingAutoIncrementBehavior(column) { return (column.isPrimary && column.isGenerated && column.generationStrategy === "increment" && this.getValueSets().some((valueSet) => column.getEntityValue(valueSet) !== undefined && column.getEntityValue(valueSet) !== null)); } /** * Creates MERGE express used to perform insert query. */ createMergeExpression() { if (!this.connection.driver.supportedUpsertTypes.includes("merge-into")) throw new error_1.TypeORMError(`Upsert type "merge-into" is not supported by current database driver`); if (this.expressionMap.onUpdate?.upsertType && this.expressionMap.onUpdate.upsertType !== "merge-into") { throw new error_1.TypeORMError(`Upsert type "${this.expressionMap.onUpdate.upsertType}" is not supported by current database driver`); } // const mainAlias = this.expressionMap.mainAlias! const tableName = this.getTableName(this.getMainTableName()); const tableAlias = this.escape(this.alias); const columns = this.getInsertedColumns(); const columnsExpression = this.createColumnNamesExpression(); let query = `MERGE INTO ${tableName} ${this.escape(this.alias)}`; const mergeSourceAlias = this.escape("mergeIntoSource"); const mergeSourceExpression = this.createMergeIntoSourceExpression(mergeSourceAlias); query += ` ${mergeSourceExpression}`; // build on condition if (this.expressionMap.onIgnore) { const primaryKey = columns.find((column) => column.isPrimary); if (primaryKey) { query += ` ON (${tableAlias}.${this.escape(primaryKey.databaseName)} = ${mergeSourceAlias}.${this.escape(primaryKey.databaseName)})`; } else { query += `ON (${this.expressionMap .mainAlias.metadata.uniques.map((unique) => { return `(${unique.columns .map((column) => { return `${tableAlias}.${this.escape(column.databaseName)} = ${mergeSourceAlias}.${this.escape(column.databaseName)}`; }) .join(" AND ")})`; }) .join(" OR ")})`; } } else if (this.expressionMap.onUpdate) { const { conflict, indexPredicate } = this.expressionMap.onUpdate; if (indexPredicate) { throw new error_1.TypeORMError(`indexPredicate option is not supported by upsert type "merge-into"`); } if (Array.isArray(conflict)) { query += ` ON (${conflict .map((column) => `${tableAlias}.${this.escape(column)} = ${mergeSourceAlias}.${this.escape(column)}`) .join(" AND ")})`; } else if (conflict) { query += ` ON (${tableAlias}.${this.escape(conflict)} = ${mergeSourceAlias}.${this.escape(conflict)})`; } else { query += `ON (${this.expressionMap .mainAlias.metadata.uniques.map((unique) => { return `(${unique.columns .map((column) => { return `${tableAlias}.${this.escape(column.databaseName)} = ${mergeSourceAlias}.${this.escape(column.databaseName)}`; }) .join(" AND ")})`; }) .join(" OR ")})`; } } if (this.expressionMap.onUpdate) { const { overwrite, columns, conflict, skipUpdateIfNoValuesChanged, } = this.expressionMap.onUpdate; let updateExpression = ""; if (Array.isArray(overwrite)) { updateExpression += (overwrite || columns) ?.filter((column) => !conflict?.includes(column)) .map((column) => `${tableAlias}.${this.escape(column)} = ${mergeSourceAlias}.${this.escape(column)}`) .join(", "); } if (Array.isArray(overwrite) && skipUpdateIfNoValuesChanged) { this.expressionMap.onUpdate.overwriteCondition ??= []; const wheres = overwrite.map((column) => ({ type: "or", condition: { operator: "notEqual", parameters: [ `${tableAlias}.${this.escape(column)}`, `${mergeSourceAlias}.${this.escape(column)}`, ], }, })); this.expressionMap.onUpdate.overwriteCondition.push({ type: "and", condition: wheres, }); } const mergeCondition = this.createUpsertConditionExpression(); if (updateExpression.trim()) { if ((this.connection.driver.options.type === "mssql" || this.connection.driver.options.type === "sap") && mergeCondition != "") { query += ` WHEN MATCHED AND ${mergeCondition} THEN UPDATE SET ${updateExpression}`; } else { query += ` WHEN MATCHED THEN UPDATE SET ${updateExpression}`; if (mergeCondition != "") { query += ` WHERE ${mergeCondition}`; } } } } const valuesExpression = this.createMergeIntoInsertValuesExpression(mergeSourceAlias); const returningExpression = this.connection.driver.options.type === "mssql" ? this.createReturningExpression("insert") : null; query += " WHEN NOT MATCHED THEN INSERT"; // add columns expression if (columnsExpression) { query += `(${columnsExpression})`; } // add VALUES expression if (valuesExpression) { query += ` VALUES ${valuesExpression}`; } // add OUTPUT expression if (returningExpression && this.connection.driver.options.type === "mssql") { query += ` OUTPUT ${returningExpression}`; } if (this.connection.driver.options.type === "mssql") { query += `;`; } return query; } /** * Creates list of values needs to be inserted in the VALUES expression. */ createMergeIntoSourceExpression(mergeSourceAlias) { const valueSets = this.getValueSets(); const columns = this.getInsertedColumns(); let expression = "USING ("; // if column metadatas are given then apply all necessary operations with values if (columns.length > 0) { if (this.connection.driver.options.type === "mssql") { expression += "VALUES "; } valueSets.forEach((valueSet, valueSetIndex) => { columns.forEach((column, columnIndex) => { if (columnIndex === 0) { if (this.connection.driver.options.type === "mssql") { expression += "("; } else { expression += "SELECT "; } } const value = column.getEntityValue(valueSet); if (value === undefined && !(column.isGenerated && column.generationStrategy === "uuid" && !this.connection.driver.isUUIDGenerationSupported())) { if (column.default !== undefined && column.default !== null) { // try to use default defined in the column expression += this.connection.driver.normalizeDefault(column); } else { expression += "NULL"; // otherwise simply use NULL and pray if column is nullable } } else if (value === null) { expression += "NULL"; } else { expression += this.createColumnValueExpression(valueSets, valueSetIndex, column); } if (this.connection.driver.options.type !== "mssql") expression += ` AS ${this.escape(column.databaseName)}`; if (columnIndex === columns.length - 1) { if (valueSetIndex === valueSets.length - 1) { if (["oracle", "sap"].includes(this.connection.driver.options.type)) { expression += " FROM " + this.connection.driver.dummyTableName; } else if (this.connection.driver.options.type === "mssql") { expression += ")"; } } else { if (["oracle", "sap"].includes(this.connection.driver.options.type) && valueSets.length > 1) { expression += " FROM " + this.connection.driver.dummyTableName + " UNION ALL "; } else if (this.connection.driver.options.type === "mssql") { expression += "), "; } else { expression += " UNION ALL "; } } } else { expression += ", "; } }); }); } else { // for tables without metadata throw new error_1.TypeORMError('Upsert type "merge-into" is not supported without metadata tables'); } expression += `) ${mergeSourceAlias}`; if (this.connection.driver.options.type === "mssql") expression += ` (${columns .map((column) => this.escape(column.databaseName)) .join(", ")})`; return expression; } /** * Creates list of values needs to be inserted in the VALUES expression. */ createMergeIntoInsertValuesExpression(mergeSourceAlias) { const columns = this.getInsertedColumns(); let expression = ""; // if column metadatas are given then apply all necessary operations with values if (columns.length > 0) { columns.forEach((column, columnIndex) => { if (columnIndex === 0) { expression += "("; } if ((column.isGenerated && column.generationStrategy === "uuid" && this.connection.driver.isUUIDGenerationSupported()) || (column.isGenerated && column.generationStrategy !== "uuid")) { expression += `DEFAULT`; } else { expression += `${mergeSourceAlias}.${this.escape(column.databaseName)}`; } if (columnIndex === columns.length - 1) { expression += ")"; } else { expression += ", "; } }); } else { // for tables without metadata throw new error_1.TypeORMError('Upsert type "merge-into" is not supported without metadata tables'); } if (expression === "()") return ""; return expression; } /** * Create upsert search condition expression. */ createUpsertConditionExpression() { if (!this.expressionMap.onUpdate.overwriteCondition) return ""; const conditionsArray = []; const whereExpression = this.createWhereClausesExpression(this.expressionMap.onUpdate.overwriteCondition); if (whereExpression.length > 0 && whereExpression !== "1=1") { conditionsArray.push(whereExpression); } if (this.expressionMap.mainAlias.hasMetadata) { const metadata = this.expressionMap.mainAlias.metadata; // Adds the global condition of "non-deleted" for the entity with delete date columns in select query. if (this.expressionMap.queryType === "select" && !this.expressionMap.withDeleted && metadata.deleteDateColumn) { const column = this.expressionMap.aliasNamePrefixingEnabled ? this.expressionMap.mainAlias.name + "." + metadata.deleteDateColumn.propertyName : metadata.deleteDateColumn.propertyName; const condition = `${column} IS NULL`; conditionsArray.push(condition); } if (metadata.discriminatorColumn && metadata.parentEntityMetadata) { const column = this.expressionMap.aliasNamePrefixingEnabled ? this.expressionMap.mainAlias.name + "." + metadata.discriminatorColumn.databaseName : metadata.discriminatorColumn.databaseName; const condition = `${column} IN (:...discriminatorColumnValues)`; conditionsArray.push(condition); } } if (this.expressionMap.extraAppendedAndWhereCondition) { const condition = this.expressionMap.extraAppendedAndWhereCondition; conditionsArray.push(condition); } let condition = ""; if (!conditionsArray.length) { condition += ""; } else if (conditionsArray.length === 1) { condition += `${conditionsArray[0]}`; } else { condition += `( ${conditionsArray.join(" ) AND ( ")} )`; } return condition; } createColumnValueExpression(valueSets, valueSetIndex, column) { const valueSet = valueSets[valueSetIndex]; let expression = ""; // extract real value from the entity let value = column.getEntityValue(valueSet); // if column is relational and value is an object then get real referenced column value from this object // for example column value is { question: { id: 1 } }, value will be equal to { id: 1 } // and we extract "1" from this object /*if (column.referencedColumn && value instanceof Object && !(typeof value === "function")) { // todo: check if we still need it since getEntityValue already has similar code value = column.referencedColumn.getEntityValue(value); }*/ if (!(typeof value === "function")) { // make sure our value is normalized by a driver value = this.connection.driver.preparePersistentValue(value, column); } // newly inserted entities always have a version equal to 1 (first version) // also, user-specified version must be empty if (column.isVersion && value === undefined) { expression += "1"; // } else if (column.isNestedSetLeft) { // const tableName = this.connection.driver.escape(column.entityMetadata.tablePath); // const rightColumnName = this.connection.driver.escape(column.entityMetadata.nestedSetRightColumn!.databaseName); // const subQuery = `(SELECT c.max + 1 FROM (SELECT MAX(${rightColumnName}) as max from ${tableName}) c)`; // expression += subQuery; // // } else if (column.isNestedSetRight) { // const tableName = this.connection.driver.escape(column.entityMetadata.tablePath); // const rightColumnName = this.connection.driver.escape(column.entityMetadata.nestedSetRightColumn!.databaseName); // const subQuery = `(SELECT c.max + 2 FROM (SELECT MAX(${rightColumnName}) as max from ${tableName}) c)`; // expression += subQuery; } else if (column.isDiscriminator) { expression += this.createParameter(this.expressionMap.mainAlias.metadata.discriminatorValue); // return "1"; // for create and update dates we insert current date // no, we don't do it because this constant is already in "default" value of the column // with extended timestamp functionality, like CURRENT_TIMESTAMP(6) for example // } else if (column.isCreateDate || column.isUpdateDate) { // return "CURRENT_TIMESTAMP"; // if column is generated uuid and database does not support its generation and custom generated value was not provided by a user - we generate a new uuid value for insertion } else if (column.isGenerated && column.generationStrategy === "uuid" && !this.connection.driver.isUUIDGenerationSupported() && value === undefined) { value = (0, uuid_1.v4)(); expression += this.createParameter(value); if (!(valueSetIndex in this.expressionMap.locallyGenerated)) { this.expressionMap.locallyGenerated[valueSetIndex] = {}; } column.setEntityValue(this.expressionMap.locallyGenerated[valueSetIndex], value); // if value for this column was not provided then insert default value } else if (value === undefined) { if ((this.connection.driver.options.type === "oracle" && valueSets.length > 1) || DriverUtils_1.DriverUtils.isSQLiteFamily(this.connection.driver) || this.connection.driver.options.type === "sap" || this.connection.driver.options.type === "spanner") { // unfortunately sqlite does not support DEFAULT expression in INSERT queries if (column.default !== undefined && column.default !== null) { // try to use default defined in the column expression += this.connection.driver.normalizeDefault(column); } else if (this.connection.driver.options.type === "spanner" && column.isGenerated && column.generationStrategy === "uuid") { expression += "GENERATE_UUID()"; // Produces a random universally unique identifier (UUID) as a STRING value. } else { expression += "NULL"; // otherwise simply use NULL and pray if column is nullable } } else { expression += "DEFAULT"; } } else if (value === null && (this.connection.driver.options.type === "spanner" || this.connection.driver.options.type === "oracle")) { expression += "NULL"; // support for SQL expressions in queries } else if (typeof value === "function") { expression += value(); // just any other regular value } else { if (this.connection.driver.options.type === "mssql") value = this.connection.driver.parametrizeValue(column, value); // we need to store array values in a special class to make sure parameter replacement will work correctly // if (value instanceof Array) /