Skip to content

Commit

Permalink
feat: allow {delete,insert}().returning() on MariaDB (#8673)
Browse files Browse the repository at this point in the history
* feat: allow `returning()` on MariaDB >= 10.5.0

Closes: #7235

* build: update docker mariadb version to 10.5.13

* fix: MySqlDriver behavior returning is supported

* feat: what kind of DML returning is supported

* test: imporve test #7235
  • Loading branch information
nix6839 committed Feb 21, 2022
1 parent 1f54c70 commit 7facbab
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 76 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Expand Up @@ -26,7 +26,7 @@ services:

# mariadb
mariadb:
image: "mariadb:10.4.8"
image: "mariadb:10.5.13"
container_name: "typeorm-mariadb"
ports:
- "3307:3306"
Expand Down
12 changes: 7 additions & 5 deletions src/driver/Driver.ts
Expand Up @@ -9,10 +9,12 @@ import {BaseConnectionOptions} from "../connection/BaseConnectionOptions";
import {TableColumn} from "../schema-builder/table/TableColumn";
import {EntityMetadata} from "../metadata/EntityMetadata";
import {ReplicationMode} from "./types/ReplicationMode";
import { Table } from "../schema-builder/table/Table";
import { View } from "../schema-builder/view/View";
import { TableForeignKey } from "../schema-builder/table/TableForeignKey";
import { UpsertType } from "./types/UpsertType";
import {Table} from "../schema-builder/table/Table";
import {View} from "../schema-builder/view/View";
import {TableForeignKey} from "../schema-builder/table/TableForeignKey";
import {UpsertType} from "./types/UpsertType";

export type ReturningType = "insert" | "update" | "delete";

/**
* Driver organizes TypeORM communication with specific database management system.
Expand Down Expand Up @@ -206,7 +208,7 @@ export interface Driver {
/**
* Returns true if driver supports RETURNING / OUTPUT statement.
*/
isReturningSqlSupported(): boolean;
isReturningSqlSupported(returningType: ReturningType): boolean;

/**
* Returns true if driver supports uuid values generation on its own.
Expand Down
61 changes: 50 additions & 11 deletions src/driver/mysql/MysqlDriver.ts
@@ -1,4 +1,4 @@
import {Driver} from "../Driver";
import {Driver, ReturningType} from "../Driver";
import {ConnectionIsNotSetError} from "../../error/ConnectionIsNotSetError";
import {DriverPackageNotInstalledError} from "../../error/DriverPackageNotInstalledError";
import {DriverUtils} from "../DriverUtils";
Expand All @@ -19,10 +19,11 @@ import {EntityMetadata} from "../../metadata/EntityMetadata";
import {OrmUtils} from "../../util/OrmUtils";
import {ApplyValueTransformers} from "../../util/ApplyValueTransformers";
import {ReplicationMode} from "../types/ReplicationMode";
import { TypeORMError } from "../../error";
import { Table } from "../../schema-builder/table/Table";
import { View } from "../../schema-builder/view/View";
import { TableForeignKey } from "../../schema-builder/table/TableForeignKey";
import {TypeORMError} from "../../error";
import {Table} from "../../schema-builder/table/Table";
import {View} from "../../schema-builder/view/View";
import {TableForeignKey} from "../../schema-builder/table/TableForeignKey";
import {VersionUtils} from "../../util/VersionUtils";

/**
* Organizes communication with MySQL DBMS.
Expand Down Expand Up @@ -304,6 +305,16 @@ export class MysqlDriver implements Driver {
*/
maxAliasLength = 63;


/**
* Supported returning types
*/
private readonly _isReturningSqlSupported: Record<ReturningType, boolean> = {
delete: false,
insert: false,
update: false,
};

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -360,6 +371,19 @@ export class MysqlDriver implements Driver {

await queryRunner.release();
}

if (this.options.type === "mariadb") {
const result = await this.createQueryRunner("master")
.query(`SELECT VERSION() AS \`version\``) as { version: string; }[];
const dbVersion = result[0].version;

if (VersionUtils.isGreaterOrEqual(dbVersion, "10.0.5")) {
this._isReturningSqlSupported.delete = true;
}
if (VersionUtils.isGreaterOrEqual(dbVersion, "10.5.0")) {
this._isReturningSqlSupported.insert = true;
}
}
}

/**
Expand Down Expand Up @@ -795,6 +819,21 @@ export class MysqlDriver implements Driver {
* Creates generated map of values generated or returned by database after INSERT query.
*/
createGeneratedMap(metadata: EntityMetadata, insertResult: any, entityIndex: number) {
if (!insertResult) {
return undefined;
}

if (insertResult.insertId === undefined) {
return Object.keys(insertResult).reduce((map, key) => {
const column = metadata.findColumnWithDatabaseName(key);
if (column) {
OrmUtils.mergeDeep(map, column.createValueMap(insertResult[key]));
// OrmUtils.mergeDeep(map, column.createValueMap(this.prepareHydratedValue(insertResult[key], column))); // TODO: probably should be like there, but fails on enums, fix later
}
return map;
}, {} as ObjectLiteral);
}

const generatedMap = metadata.generatedColumns.reduce((map, generatedColumn) => {
let value: any;
if (generatedColumn.generationStrategy === "increment" && insertResult.insertId) {
Expand Down Expand Up @@ -874,8 +913,8 @@ export class MysqlDriver implements Driver {
/**
* Returns true if driver supports RETURNING / OUTPUT statement.
*/
isReturningSqlSupported(): boolean {
return false;
isReturningSqlSupported(returningType: ReturningType): boolean {
return this._isReturningSqlSupported[returningType];
}

/**
Expand Down Expand Up @@ -961,8 +1000,8 @@ export class MysqlDriver implements Driver {
socketPath: credentials.socketPath
},
options.acquireTimeout === undefined
? {}
: { acquireTimeout: options.acquireTimeout },
? {}
: { acquireTimeout: options.acquireTimeout },
options.extra || {});
}

Expand Down Expand Up @@ -994,8 +1033,8 @@ export class MysqlDriver implements Driver {
private prepareDbConnection(connection: any): any {
const { logger } = this.connection;
/*
Attaching an error handler to connection errors is essential, as, otherwise, errors raised will go unhandled and
cause the hosting app to crash.
* Attaching an error handler to connection errors is essential, as, otherwise, errors raised will go unhandled and
* cause the hosting app to crash.
*/
if (connection.listeners("error").length === 0) {
connection.on("error", (error: any) => logger.log("warn", `MySQL connection raised an error. ${error}`));
Expand Down
2 changes: 1 addition & 1 deletion src/error/ReturningStatementNotSupportedError.ts
Expand Up @@ -7,7 +7,7 @@ import {TypeORMError} from "./TypeORMError";
export class ReturningStatementNotSupportedError extends TypeORMError {
constructor() {
super(
`OUTPUT or RETURNING clause only supported by Microsoft SQL Server or PostgreSQL databases.`
`OUTPUT or RETURNING clause only supported by Microsoft SQL Server or PostgreSQL or MariaDB databases.`
);
}
}
3 changes: 2 additions & 1 deletion src/persistence/SubjectExecutor.ts
Expand Up @@ -832,7 +832,8 @@ export class SubjectExecutor {
protected groupBulkSubjects(subjects: Subject[], type: "insert" | "delete"): [{ [key: string]: Subject[] }, string[]] {
const group: { [key: string]: Subject[] } = {};
const keys: string[] = [];
const groupingAllowed = type === "delete" || this.queryRunner.connection.driver.isReturningSqlSupported();
const groupingAllowed = type === "delete" ||
this.queryRunner.connection.driver.isReturningSqlSupported("insert");

subjects.forEach((subject, index) => {
const key = groupingAllowed || subject.metadata.isJunction ? subject.metadata.name : subject.metadata.name + "_" + index;
Expand Down
19 changes: 8 additions & 11 deletions src/query-builder/DeleteQueryBuilder.ts
@@ -1,11 +1,9 @@
import {CockroachDriver} from "../driver/cockroachdb/CockroachDriver";
import {QueryBuilder} from "./QueryBuilder";
import {ObjectLiteral} from "../common/ObjectLiteral";
import {EntityTarget} from "../common/EntityTarget";
import {Connection} from "../connection/Connection";
import {QueryRunner} from "../query-runner/QueryRunner";
import {SqlServerDriver} from "../driver/sqlserver/SqlServerDriver";
import {PostgresDriver} from "../driver/postgres/PostgresDriver";
import {WhereExpressionBuilder} from "./WhereExpressionBuilder";
import {Brackets} from "./Brackets";
import {DeleteResult} from "./result/DeleteResult";
Expand Down Expand Up @@ -212,8 +210,9 @@ export class DeleteQueryBuilder<Entity> extends QueryBuilder<Entity> implements
returning(returning: string|string[]): this {

// not all databases support returning/output cause
if (!this.connection.driver.isReturningSqlSupported())
if (!this.connection.driver.isReturningSqlSupported("delete")) {
throw new ReturningStatementNotSupportedError();
}

this.expressionMap.returning = returning;
return this;
Expand All @@ -229,17 +228,15 @@ export class DeleteQueryBuilder<Entity> extends QueryBuilder<Entity> implements
protected createDeleteExpression() {
const tableName = this.getTableName(this.getMainTableName());
const whereExpression = this.createWhereExpression();
const returningExpression = this.createReturningExpression();

if (returningExpression && (this.connection.driver instanceof PostgresDriver || this.connection.driver instanceof CockroachDriver)) {
return `DELETE FROM ${tableName}${whereExpression} RETURNING ${returningExpression}`;

} else if (returningExpression !== "" && this.connection.driver instanceof SqlServerDriver) {
return `DELETE FROM ${tableName} OUTPUT ${returningExpression}${whereExpression}`;
const returningExpression = this.createReturningExpression("delete");

} else {
if (returningExpression === "") {
return `DELETE FROM ${tableName}${whereExpression}`;
}
if (this.connection.driver instanceof SqlServerDriver) {
return `DELETE FROM ${tableName} OUTPUT ${returningExpression}${whereExpression}`;
}
return `DELETE FROM ${tableName}${whereExpression} RETURNING ${returningExpression}`;
}

}
28 changes: 19 additions & 9 deletions src/query-builder/InsertQueryBuilder.ts
Expand Up @@ -17,8 +17,8 @@ import {BroadcasterResult} from "../subscriber/BroadcasterResult";
import {EntitySchema} from "../entity-schema/EntitySchema";
import {OracleDriver} from "../driver/oracle/OracleDriver";
import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver";
import { TypeORMError } from "../error";
import { v4 as uuidv4 } from "uuid";
import {TypeORMError} from "../error";
import {v4 as uuidv4} from "uuid";

/**
* Allows to build complex sql queries in a fashion way and execute those queries.
Expand Down Expand Up @@ -244,8 +244,9 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
returning(returning: string|string[]): this {

// not all databases support returning/output cause
if (!this.connection.driver.isReturningSqlSupported())
if (!this.connection.driver.isReturningSqlSupported("insert")) {
throw new ReturningStatementNotSupportedError();
}

this.expressionMap.returning = returning;
return this;
Expand Down Expand Up @@ -316,12 +317,15 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
protected createInsertExpression() {
const tableName = this.getTableName(this.getMainTableName());
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 instanceof OracleDriver && this.getValueSets().length > 1) ? null : this.createReturningExpression(); // oracle doesnt support returning with multi-row insert
const returningExpression =
(this.connection.driver instanceof OracleDriver && 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.connection.driver instanceof MysqlDriver || this.connection.driver instanceof AuroraDataApiDriver) {
query += `${this.expressionMap.onIgnore ? " IGNORE " : ""}`;
query += `${this.expressionMap.onIgnore ? " IGNORE " : ""}`;
}

query += `INTO ${tableName}`;
Expand Down Expand Up @@ -400,7 +404,13 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
}

// add RETURNING expression
if (returningExpression && (this.connection.driver instanceof PostgresDriver || this.connection.driver instanceof OracleDriver || this.connection.driver instanceof CockroachDriver)) {
if (
returningExpression &&
(this.connection.driver instanceof PostgresDriver ||
this.connection.driver instanceof OracleDriver ||
this.connection.driver instanceof CockroachDriver ||
this.connection.driver instanceof MysqlDriver)
) {
query += ` RETURNING ${returningExpression}`;
}

Expand Down Expand Up @@ -504,7 +514,7 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {

if (!(value instanceof Function)) {
// make sure our value is normalized by a driver
value = this.connection.driver.preparePersistentValue(value, column);
value = this.connection.driver.preparePersistentValue(value, column);
}

// newly inserted entities always have a version equal to 1 (first version)
Expand Down Expand Up @@ -584,9 +594,9 @@ export class InsertQueryBuilder<Entity> extends QueryBuilder<Entity> {
}
} else if (this.connection.driver instanceof PostgresDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) {
if (column.srid != null) {
expression += `ST_SetSRID(ST_GeomFromGeoJSON(${paramName}), ${column.srid})::${column.type}`;
expression += `ST_SetSRID(ST_GeomFromGeoJSON(${paramName}), ${column.srid})::${column.type}`;
} else {
expression += `ST_GeomFromGeoJSON(${paramName})::${column.type}`;
expression += `ST_GeomFromGeoJSON(${paramName})::${column.type}`;
}
} else if (this.connection.driver instanceof SqlServerDriver && this.connection.driver.spatialTypes.indexOf(column.type) !== -1) {
expression += column.type + "::STGeomFromText(" + paramName + ", " + (column.srid || "0") + ")";
Expand Down
9 changes: 5 additions & 4 deletions src/query-builder/QueryBuilder.ts
Expand Up @@ -22,9 +22,10 @@ import {EntitySchema} from "../entity-schema/EntitySchema";
import {FindOperator} from "../find-options/FindOperator";
import {In} from "../find-options/operator/In";
import {EntityColumnNotFound} from "../error/EntityColumnNotFound";
import { TypeORMError } from "../error";
import { WhereClause, WhereClauseCondition } from "./WhereClause";
import {TypeORMError} from "../error";
import {WhereClause, WhereClauseCondition} from "./WhereClause";
import {NotBrackets} from "./NotBrackets";
import {ReturningType} from "../driver/Driver";

// todo: completely cover query builder with tests
// todo: entityOrProperty can be target name. implement proper behaviour if it is.
Expand Down Expand Up @@ -737,15 +738,15 @@ export abstract class QueryBuilder<Entity> {
/**
* Creates "RETURNING" / "OUTPUT" expression.
*/
protected createReturningExpression(): string {
protected createReturningExpression(returningType: ReturningType): string {
const columns = this.getReturningColumns();
const driver = this.connection.driver;

// also add columns we must auto-return to perform entity updation
// if user gave his own returning
if (typeof this.expressionMap.returning !== "string" &&
this.expressionMap.extraReturningColumns.length > 0 &&
driver.isReturningSqlSupported()) {
driver.isReturningSqlSupported(returningType)) {
columns.push(...this.expressionMap.extraReturningColumns.filter(column => {
return columns.indexOf(column) === -1;
}));
Expand Down
11 changes: 7 additions & 4 deletions src/query-builder/ReturningResultsEntityUpdator.ts
Expand Up @@ -5,7 +5,7 @@ import {ColumnMetadata} from "../metadata/ColumnMetadata";
import {UpdateResult} from "./result/UpdateResult";
import {InsertResult} from "./result/InsertResult";
import {OracleDriver} from "../driver/oracle/OracleDriver";
import { TypeORMError } from "../error";
import {TypeORMError} from "../error";

/**
* Updates entity with returning results in the entity insert and update operations.
Expand Down Expand Up @@ -33,7 +33,7 @@ export class ReturningResultsEntityUpdator {
await Promise.all(entities.map(async (entity, entityIndex) => {

// if database supports returning/output statement then we already should have updating values in the raw data returned by insert query
if (this.queryRunner.connection.driver.isReturningSqlSupported()) {
if (this.queryRunner.connection.driver.isReturningSqlSupported("update")) {
if (this.queryRunner.connection.driver instanceof OracleDriver && Array.isArray(updateResult.raw) && this.expressionMap.extraReturningColumns.length > 0) {
updateResult.raw = updateResult.raw.reduce((newRaw, rawItem, rawItemIndex) => {
newRaw[this.expressionMap.extraReturningColumns[rawItemIndex].databaseName] = rawItem[0];
Expand Down Expand Up @@ -116,7 +116,10 @@ export class ReturningResultsEntityUpdator {

// for postgres and mssql we use returning/output statement to get values of inserted default and generated values
// for other drivers we have to re-select this data from the database
if (this.queryRunner.connection.driver.isReturningSqlSupported() === false && insertionColumns.length > 0) {
if (
insertionColumns.length > 0 &&
!this.queryRunner.connection.driver.isReturningSqlSupported("insert")
) {
const entityIds = entities.map((entity) => {
const entityId = metadata.getEntityIdMap(entity)!;

Expand Down Expand Up @@ -173,7 +176,7 @@ export class ReturningResultsEntityUpdator {

// for databases which support returning statement we need to return extra columns like id
// for other databases we don't need to return id column since its returned by a driver already
const needToCheckGenerated = this.queryRunner.connection.driver.isReturningSqlSupported();
const needToCheckGenerated = this.queryRunner.connection.driver.isReturningSqlSupported("insert");

// filter out the columns of which we need database inserted values to update our entity
return this.expressionMap.mainAlias!.metadata.columns.filter(column => {
Expand Down

0 comments on commit 7facbab

Please sign in to comment.