diff --git a/package-lock.json b/package-lock.json index 5f1fad9d71..b6dc412d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1816,6 +1816,15 @@ "assert-plus": "^1.0.0" } }, + "data-api-client": { + "version": "1.0.0-beta", + "resolved": "https://registry.npmjs.org/data-api-client/-/data-api-client-1.0.0-beta.tgz", + "integrity": "sha512-sBC6pGooj59FhKhND7aj24a+pI4qFd0K08WtF6X7ZtthMy5x5ezWC6VDuMUfwMrvA0qGXttFdT6/2U1JTgSN2g==", + "dev": true, + "requires": { + "sqlstring": "^2.3.1" + } + }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -8398,6 +8407,15 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typeorm-aurora-data-api-driver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/typeorm-aurora-data-api-driver/-/typeorm-aurora-data-api-driver-1.1.1.tgz", + "integrity": "sha512-KqqMiwf/YrT0/YIPL0D97zEAt2TtRyxZGVo1UJusnO3o+3FoLbzFLp3x0Jg3KapOq8EyzYGeLRDsWVUSJQ6MkQ==", + "dev": true, + "requires": { + "data-api-client": "^1.0.0-beta" + } + }, "typescript": { "version": "3.3.3333", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3333.tgz", diff --git a/package.json b/package.json index d249998964..902c0d091d 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,8 @@ "sqlite3": "^4.0.9", "ts-node": "^8.0.2", "tslint": "^5.13.1", - "typescript": "^3.3.3333" + "typescript": "^3.3.3333", + "typeorm-aurora-data-api-driver": "^1.1.1" }, "dependencies": { "app-root-path": "^2.0.1", diff --git a/src/connection/ConnectionOptions.ts b/src/connection/ConnectionOptions.ts index 39bfbc7945..11317c01da 100644 --- a/src/connection/ConnectionOptions.ts +++ b/src/connection/ConnectionOptions.ts @@ -10,6 +10,8 @@ import {SqljsConnectionOptions} from "../driver/sqljs/SqljsConnectionOptions"; import {ReactNativeConnectionOptions} from "../driver/react-native/ReactNativeConnectionOptions"; import {NativescriptConnectionOptions} from "../driver/nativescript/NativescriptConnectionOptions"; import {ExpoConnectionOptions} from "../driver/expo/ExpoConnectionOptions"; +import {AuroraDataApiConnectionOptions} from "../driver/aurora-data-api/AuroraDataApiConnectionOptions"; + /** * ConnectionOptions is an interface with settings and options for specific connection. @@ -28,4 +30,5 @@ export type ConnectionOptions = ReactNativeConnectionOptions| SqljsConnectionOptions| MongoConnectionOptions| + AuroraDataApiConnectionOptions| ExpoConnectionOptions; diff --git a/src/driver/DriverFactory.ts b/src/driver/DriverFactory.ts index 0580adedc8..332d555c47 100644 --- a/src/driver/DriverFactory.ts +++ b/src/driver/DriverFactory.ts @@ -11,6 +11,7 @@ import {SqljsDriver} from "./sqljs/SqljsDriver"; import {MysqlDriver} from "./mysql/MysqlDriver"; import {PostgresDriver} from "./postgres/PostgresDriver"; import {ExpoDriver} from "./expo/ExpoDriver"; +import {AuroraDataApiDriver} from "./aurora-data-api/AuroraDataApiDriver"; import {Driver} from "./Driver"; import {Connection} from "../connection/Connection"; @@ -51,6 +52,8 @@ export class DriverFactory { return new MongoDriver(connection); case "expo": return new ExpoDriver(connection); + case "aurora-data-api": + return new AuroraDataApiDriver(connection); default: throw new MissingDriverError(type); } diff --git a/src/driver/aurora-data-api/AuroraDataApiConnection.ts b/src/driver/aurora-data-api/AuroraDataApiConnection.ts new file mode 100644 index 0000000000..386248809f --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiConnection.ts @@ -0,0 +1,20 @@ +import {AuroraDataApiQueryRunner} from "./AuroraDataApiQueryRunner"; +import {Connection} from "../../connection/Connection"; +import {ConnectionOptions, QueryRunner} from "../.."; + +/** + * Organizes communication with MySQL DBMS. + */ +export class AuroraDataApiConnection extends Connection { + queryRunnter: AuroraDataApiQueryRunner; + + constructor(options: ConnectionOptions, queryRunner: AuroraDataApiQueryRunner) { + super(options); + this.queryRunnter = queryRunner; + } + + public createQueryRunner(mode: "master" | "slave" = "master"): QueryRunner { + return this.queryRunnter; + } + +} diff --git a/src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts b/src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts new file mode 100644 index 0000000000..dc8be9db29 --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiConnectionCredentialsOptions.ts @@ -0,0 +1,43 @@ +/** + * MySQL specific connection credential options. + * + * @see https://github.com/mysqljs/mysql#connection-options + */ +export interface AuroraDataApiConnectionCredentialsOptions { + + /** + * Connection url where perform connection to. + */ + readonly url?: string; + + /** + * Database host. + */ + readonly host?: string; + + /** + * Database host port. + */ + readonly port?: number; + + /** + * Database username. + */ + readonly username?: string; + + /** + * Database password. + */ + readonly password?: string; + + /** + * Database name to connect to. + */ + readonly database?: string; + + /** + * Object with ssl parameters or a string containing name of ssl profile. + */ + readonly ssl?: any; + +} diff --git a/src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts b/src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts new file mode 100644 index 0000000000..c79c850ff2 --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiConnectionOptions.ts @@ -0,0 +1,23 @@ +import {BaseConnectionOptions} from "../../connection/BaseConnectionOptions"; +import {AuroraDataApiConnectionCredentialsOptions} from "./AuroraDataApiConnectionCredentialsOptions"; + +/** + * MySQL specific connection options. + * + * @see https://github.com/mysqljs/mysql#connection-options + */ +export interface AuroraDataApiConnectionOptions extends BaseConnectionOptions, AuroraDataApiConnectionCredentialsOptions { + + /** + * Database type. + */ + readonly type: "aurora-data-api"; + + readonly region: string; + + readonly secretArn: string; + + readonly resourceArn: string; + + readonly database: string; +} diff --git a/src/driver/aurora-data-api/AuroraDataApiDriver.ts b/src/driver/aurora-data-api/AuroraDataApiDriver.ts new file mode 100644 index 0000000000..e2248a324c --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiDriver.ts @@ -0,0 +1,828 @@ +import {Driver} from "../Driver"; +import {DriverUtils} from "../DriverUtils"; +import {AuroraDataApiQueryRunner} from "./AuroraDataApiQueryRunner"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; +import {ColumnMetadata} from "../../metadata/ColumnMetadata"; +import {DateUtils} from "../../util/DateUtils"; +import {PlatformTools} from "../../platform/PlatformTools"; +import {Connection} from "../../connection/Connection"; +import {RdbmsSchemaBuilder} from "../../schema-builder/RdbmsSchemaBuilder"; +import {AuroraDataApiConnectionOptions} from "./AuroraDataApiConnectionOptions"; +import {MappedColumnTypes} from "../types/MappedColumnTypes"; +import {ColumnType} from "../types/ColumnTypes"; +import {DataTypeDefaults} from "../types/DataTypeDefaults"; +import {TableColumn} from "../../schema-builder/table/TableColumn"; +import {AuroraDataApiConnectionCredentialsOptions} from "./AuroraDataApiConnectionCredentialsOptions"; +import {EntityMetadata} from "../../metadata/EntityMetadata"; +import {OrmUtils} from "../../util/OrmUtils"; +import {ApplyValueTransformers} from "../../util/ApplyValueTransformers"; + +/** + * Organizes communication with MySQL DBMS. + */ +export class AuroraDataApiDriver implements Driver { + + // ------------------------------------------------------------------------- + // Public Properties + // ------------------------------------------------------------------------- + + connection: Connection; + /** + * Aurora Data API underlying library. + */ + DataApiDriver: any; + + client: any; + + /** + * Connection pool. + * Used in non-replication mode. + */ + pool: any; + + /** + * Pool cluster used in replication mode. + */ + poolCluster: any; + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Connection options. + */ + options: AuroraDataApiConnectionOptions; + + /** + * Master database used to perform all write queries. + */ + database?: string; + + /** + * Indicates if replication is enabled. + */ + isReplicated: boolean = false; + + /** + * Indicates if tree tables are supported by this driver. + */ + treeSupport = true; + + /** + * Gets list of supported column data types by a driver. + * + * @see https://www.tutorialspoint.com/mysql/mysql-data-types.htm + * @see https://dev.mysql.com/doc/refman/8.0/en/data-types.html + */ + supportedDataTypes: ColumnType[] = [ + // numeric types + "bit", + "int", + "integer", // synonym for int + "tinyint", + "smallint", + "mediumint", + "bigint", + "float", + "double", + "double precision", // synonym for double + "real", // synonym for double + "decimal", + "dec", // synonym for decimal + "numeric", // synonym for decimal + "fixed", // synonym for decimal + "bool", // synonym for tinyint + "boolean", // synonym for tinyint + // date and time types + "date", + "datetime", + "timestamp", + "time", + "year", + // string types + "char", + "nchar", // synonym for national char + "national char", + "varchar", + "nvarchar", // synonym for national varchar + "national varchar", + "blob", + "text", + "tinyblob", + "tinytext", + "mediumblob", + "mediumtext", + "longblob", + "longtext", + "enum", + "binary", + "varbinary", + // json data type + "json", + // spatial data types + "geometry", + "point", + "linestring", + "polygon", + "multipoint", + "multilinestring", + "multipolygon", + "geometrycollection" + ]; + + /** + * Gets list of spatial column data types. + */ + spatialTypes: ColumnType[] = [ + "geometry", + "point", + "linestring", + "polygon", + "multipoint", + "multilinestring", + "multipolygon", + "geometrycollection" + ]; + + /** + * Gets list of column data types that support length by a driver. + */ + withLengthColumnTypes: ColumnType[] = [ + "char", + "varchar", + "nvarchar", + "binary", + "varbinary" + ]; + + /** + * Gets list of column data types that support length by a driver. + */ + withWidthColumnTypes: ColumnType[] = [ + "bit", + "tinyint", + "smallint", + "mediumint", + "int", + "integer", + "bigint" + ]; + + /** + * Gets list of column data types that support precision by a driver. + */ + withPrecisionColumnTypes: ColumnType[] = [ + "decimal", + "dec", + "numeric", + "fixed", + "float", + "double", + "double precision", + "real", + "time", + "datetime", + "timestamp" + ]; + + /** + * Gets list of column data types that supports scale by a driver. + */ + withScaleColumnTypes: ColumnType[] = [ + "decimal", + "dec", + "numeric", + "fixed", + "float", + "double", + "double precision", + "real" + ]; + + /** + * Gets list of column data types that supports UNSIGNED and ZEROFILL attributes. + */ + unsignedAndZerofillTypes: ColumnType[] = [ + "int", + "integer", + "smallint", + "tinyint", + "mediumint", + "bigint", + "decimal", + "dec", + "numeric", + "fixed", + "float", + "double", + "double precision", + "real" + ]; + + /** + * ORM has special columns and we need to know what database column types should be for those columns. + * Column types are driver dependant. + */ + mappedDataTypes: MappedColumnTypes = { + createDate: "datetime", + createDatePrecision: 6, + createDateDefault: "CURRENT_TIMESTAMP(6)", + updateDate: "datetime", + updateDatePrecision: 6, + updateDateDefault: "CURRENT_TIMESTAMP(6)", + version: "int", + treeLevel: "int", + migrationId: "int", + migrationName: "varchar", + migrationTimestamp: "bigint", + cacheId: "int", + cacheIdentifier: "varchar", + cacheTime: "bigint", + cacheDuration: "int", + cacheQuery: "text", + cacheResult: "text", + metadataType: "varchar", + metadataDatabase: "varchar", + metadataSchema: "varchar", + metadataTable: "varchar", + metadataName: "varchar", + metadataValue: "text", + }; + + /** + * Default values of length, precision and scale depends on column data type. + * Used in the cases when length/precision/scale is not specified by user. + */ + dataTypeDefaults: DataTypeDefaults = { + "varchar": { length: 255 }, + "nvarchar": { length: 255 }, + "national varchar": { length: 255 }, + "char": { length: 1 }, + "binary": { length: 1 }, + "varbinary": { length: 255 }, + "decimal": { precision: 10, scale: 0 }, + "dec": { precision: 10, scale: 0 }, + "numeric": { precision: 10, scale: 0 }, + "fixed": { precision: 10, scale: 0 }, + "float": { precision: 12 }, + "double": { precision: 22 }, + "time": { precision: 0 }, + "datetime": { precision: 0 }, + "timestamp": { precision: 0 }, + "bit": { width: 1 }, + "int": { width: 11 }, + "integer": { width: 11 }, + "tinyint": { width: 4 }, + "smallint": { width: 6 }, + "mediumint": { width: 9 }, + "bigint": { width: 20 } + }; + + + /** + * Max length allowed by MySQL for aliases. + * @see https://dev.mysql.com/doc/refman/5.5/en/identifiers.html + */ + maxAliasLength = 63; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(connection: Connection) { + this.connection = connection; + this.options = connection.options as AuroraDataApiConnectionOptions; + + // load mysql package + this.loadDependencies(); + + this.client = new this.DataApiDriver( + this.options.region, + this.options.secretArn, + this.options.resourceArn, + this.options.database, + (query: string, parameters?: any[]) => this.connection.logger.logQuery(query, parameters), + ); + + // validate options to make sure everything is set + // todo: revisit validation with replication in mind + // if (!(this.options.host || (this.options.extra && this.options.extra.socketPath)) && !this.options.socketPath) + // throw new DriverOptionNotSetError("socketPath and host"); + // if (!this.options.username) + // throw new DriverOptionNotSetError("username"); + // if (!this.options.database) + // throw new DriverOptionNotSetError("database"); + // todo: check what is going on when connection is setup without database and how to connect to a database then? + // todo: provide options to auto-create a database if it does not exist yet + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Performs connection to the database. + */ + async connect(): Promise { + } + + /** + * Makes any action after connection (e.g. create extensions in Postgres driver). + */ + afterConnect(): Promise { + return Promise.resolve(); + } + + /** + * Closes connection with the database. + */ + async disconnect(): Promise { + } + + /** + * Creates a schema builder used to build and sync a schema. + */ + createSchemaBuilder() { + return new RdbmsSchemaBuilder(this.connection); + } + + /** + * Creates a query runner used to execute database queries. + */ + createQueryRunner(mode: "master"|"slave" = "master") { + return new AuroraDataApiQueryRunner(this); + } + + /** + * Replaces parameters in the given sql with special escaping character + * and an array of parameter names to be passed to a query. + */ + escapeQueryWithParameters(sql: string, parameters: ObjectLiteral, nativeParameters: ObjectLiteral): [string, any[]] { + const escapedParameters: any[] = Object.keys(nativeParameters).map(key => nativeParameters[key]); + if (!parameters || !Object.keys(parameters).length) + return [sql, escapedParameters]; + + const keys = Object.keys(parameters).map(parameter => "(:(\\.\\.\\.)?" + parameter + "\\b)").join("|"); + sql = sql.replace(new RegExp(keys, "g"), (key: string) => { + let value: any; + if (key.substr(0, 4) === ":...") { + value = parameters[key.substr(4)]; + } else { + value = parameters[key.substr(1)]; + } + + if (value instanceof Function) { + return value(); + + } else { + escapedParameters.push(value); + return "?"; + } + }); // todo: make replace only in value statements, otherwise problems + return [sql, escapedParameters]; + } + + /** + * Escapes a column name. + */ + escape(columnName: string): string { + return "`" + columnName + "`"; + } + + /** + * Build full table name with database name, schema name and table name. + * E.g. "myDB"."mySchema"."myTable" + */ + buildTableName(tableName: string, schema?: string, database?: string): string { + return database ? `${database}.${tableName}` : tableName; + } + + /** + * Prepares given value to a value to be persisted, based on its column type and metadata. + */ + preparePersistentValue(value: any, columnMetadata: ColumnMetadata): any { + if (columnMetadata.transformer) + value = ApplyValueTransformers.transformTo(columnMetadata.transformer, value); + + if (value === null || value === undefined) + return value; + + if (columnMetadata.type === Boolean) { + return value === true ? 1 : 0; + + } else if (columnMetadata.type === "date") { + return DateUtils.mixedDateToDateString(value); + + } else if (columnMetadata.type === "time") { + return DateUtils.mixedDateToTimeString(value); + + } else if (columnMetadata.type === "json") { + return JSON.stringify(value); + + } else if (columnMetadata.type === "timestamp" || columnMetadata.type === "datetime" || columnMetadata.type === Date) { + return DateUtils.mixedDateToDate(value); + + } else if (columnMetadata.type === "simple-array") { + return DateUtils.simpleArrayToString(value); + + } else if (columnMetadata.type === "simple-json") { + return DateUtils.simpleJsonToString(value); + + } else if (columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") { + return "" + value; + } + + return value; + } + + /** + * Prepares given value to a value to be persisted, based on its column type or metadata. + */ + prepareHydratedValue(value: any, columnMetadata: ColumnMetadata): any { + if (value === null || value === undefined) + return columnMetadata.transformer ? ApplyValueTransformers.transformFrom(columnMetadata.transformer, value) : value; + + if (columnMetadata.type === Boolean || columnMetadata.type === "bool" || columnMetadata.type === "boolean") { + value = value ? true : false; + + } else if (columnMetadata.type === "datetime" || columnMetadata.type === Date) { + value = DateUtils.normalizeHydratedDate(value); + + } else if (columnMetadata.type === "date") { + value = DateUtils.mixedDateToDateString(value); + + } else if (columnMetadata.type === "json") { + value = typeof value === "string" ? JSON.parse(value) : value; + + } else if (columnMetadata.type === "time") { + value = DateUtils.mixedTimeToString(value); + + } else if (columnMetadata.type === "simple-array") { + value = DateUtils.stringToSimpleArray(value); + + } else if (columnMetadata.type === "simple-json") { + value = DateUtils.stringToSimpleJson(value); + + } else if ((columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") + && columnMetadata.enum + && !isNaN(value) + && columnMetadata.enum.indexOf(parseInt(value)) >= 0) { + // convert to number if that exists in possible enum options + value = parseInt(value); + } + + if (columnMetadata.transformer) + value = ApplyValueTransformers.transformFrom(columnMetadata.transformer, value); + + return value; + } + + /** + * Creates a database type from a given column metadata. + */ + normalizeType(column: { type: ColumnType, length?: number|string, precision?: number|null, scale?: number }): string { + if (column.type === Number || column.type === "integer") { + return "int"; + + } else if (column.type === String) { + return "varchar"; + + } else if (column.type === Date) { + return "datetime"; + + } else if ((column.type as any) === Buffer) { + return "blob"; + + } else if (column.type === Boolean) { + return "tinyint"; + + } else if (column.type === "uuid") { + return "varchar"; + + } else if (column.type === "simple-array" || column.type === "simple-json") { + return "text"; + + } else if (column.type === "simple-enum") { + return "enum"; + + } else if (column.type === "double precision" || column.type === "real") { + return "double"; + + } else if (column.type === "dec" || column.type === "numeric" || column.type === "fixed") { + return "decimal"; + + } else if (column.type === "bool" || column.type === "boolean") { + return "tinyint"; + + } else if (column.type === "nvarchar" || column.type === "national varchar") { + return "varchar"; + + } else if (column.type === "nchar" || column.type === "national char") { + return "char"; + + } else { + return column.type as string || ""; + } + } + + /** + * Normalizes "default" value of the column. + */ + normalizeDefault(columnMetadata: ColumnMetadata): string { + const defaultValue = columnMetadata.default; + + if ((columnMetadata.type === "enum" || columnMetadata.type === "simple-enum") && defaultValue !== undefined) { + return `'${defaultValue}'`; + } + + if (typeof defaultValue === "number") { + return "" + defaultValue; + + } else if (typeof defaultValue === "boolean") { + return defaultValue === true ? "1" : "0"; + + } else if (typeof defaultValue === "function") { + return defaultValue(); + + } else if (typeof defaultValue === "string") { + return `'${defaultValue}'`; + + } else if (defaultValue === null) { + return `null`; + + } else { + return defaultValue; + } + } + + /** + * Normalizes "isUnique" value of the column. + */ + normalizeIsUnique(column: ColumnMetadata): boolean { + return column.entityMetadata.indices.some(idx => idx.isUnique && idx.columns.length === 1 && idx.columns[0] === column); + } + + /** + * Returns default column lengths, which is required on column creation. + */ + getColumnLength(column: ColumnMetadata|TableColumn): string { + if (column.length) + return column.length.toString(); + + /** + * fix https://github.com/typeorm/typeorm/issues/1139 + */ + if (column.generationStrategy === "uuid") + return "36"; + + switch (column.type) { + case String: + case "varchar": + case "nvarchar": + case "national varchar": + return "255"; + case "varbinary": + return "255"; + default: + return ""; + } + } + + /** + * Creates column type definition including length, precision and scale + */ + createFullType(column: TableColumn): string { + let type = column.type; + + // used 'getColumnLength()' method, because MySQL requires column length for `varchar`, `nvarchar` and `varbinary` data types + if (this.getColumnLength(column)) { + type += `(${this.getColumnLength(column)})`; + + } else if (column.width) { + type += `(${column.width})`; + + } else if (column.precision !== null && column.precision !== undefined && column.scale !== null && column.scale !== undefined) { + type += `(${column.precision},${column.scale})`; + + } else if (column.precision !== null && column.precision !== undefined) { + type += `(${column.precision})`; + } + + if (column.isArray) + type += " array"; + + return type; + } + + /** + * Obtains a new database connection to a master server. + * Used for replication. + * If replication is not setup then returns default connection's database connection. + */ + obtainMasterConnection(): Promise { + return new Promise((ok, fail) => { + if (this.poolCluster) { + this.poolCluster.getConnection("MASTER", (err: any, dbConnection: any) => { + err ? fail(err) : ok(this.prepareDbConnection(dbConnection)); + }); + + } else if (this.pool) { + this.pool.getConnection((err: any, dbConnection: any) => { + err ? fail(err) : ok(this.prepareDbConnection(dbConnection)); + }); + } else { + fail(new Error(`Connection is not established with mysql database`)); + } + }); + } + + /** + * Obtains a new database connection to a slave server. + * Used for replication. + * If replication is not setup then returns master (default) connection's database connection. + */ + obtainSlaveConnection(): Promise { + if (!this.poolCluster) + return this.obtainMasterConnection(); + + return new Promise((ok, fail) => { + this.poolCluster.getConnection("SLAVE*", (err: any, dbConnection: any) => { + err ? fail(err) : ok(dbConnection); + }); + }); + } + + /** + * Creates generated map of values generated or returned by database after INSERT query. + */ + createGeneratedMap(metadata: EntityMetadata, insertResult: any) { + const generatedMap = metadata.generatedColumns.reduce((map, generatedColumn) => { + let value: any; + if (generatedColumn.generationStrategy === "increment" && insertResult.insertId) { + value = insertResult.insertId; + // } else if (generatedColumn.generationStrategy === "uuid") { + // console.log("getting db value:", generatedColumn.databaseName); + // value = generatedColumn.getEntityValue(uuidMap); + } + + return OrmUtils.mergeDeep(map, generatedColumn.createValueMap(value)); + }, {} as ObjectLiteral); + + return Object.keys(generatedMap).length > 0 ? generatedMap : undefined; + } + + /** + * Differentiate columns of this table and columns from the given column metadatas columns + * and returns only changed. + */ + findChangedColumns(tableColumns: TableColumn[], columnMetadatas: ColumnMetadata[]): ColumnMetadata[] { + return columnMetadatas.filter(columnMetadata => { + const tableColumn = tableColumns.find(c => c.name === columnMetadata.databaseName); + if (!tableColumn) + return false; // we don't need new columns, we only need exist and changed + + // console.log("table:", columnMetadata.entityMetadata.tableName); + // console.log("name:", tableColumn.name, columnMetadata.databaseName); + // console.log("type:", tableColumn.type, this.normalizeType(columnMetadata)); + // console.log("length:", tableColumn.length, columnMetadata.length); + // console.log("width:", tableColumn.width, columnMetadata.width); + // console.log("precision:", tableColumn.precision, columnMetadata.precision); + // console.log("scale:", tableColumn.scale, columnMetadata.scale); + // console.log("zerofill:", tableColumn.zerofill, columnMetadata.zerofill); + // console.log("unsigned:", tableColumn.unsigned, columnMetadata.unsigned); + // console.log("asExpression:", tableColumn.asExpression, columnMetadata.asExpression); + // console.log("generatedType:", tableColumn.generatedType, columnMetadata.generatedType); + // console.log("comment:", tableColumn.comment, columnMetadata.comment); + // console.log("default:", tableColumn.default, columnMetadata.default); + // console.log("enum:", tableColumn.enum, columnMetadata.enum); + // console.log("default changed:", !this.compareDefaultValues(this.normalizeDefault(columnMetadata), tableColumn.default)); + // console.log("onUpdate:", tableColumn.onUpdate, columnMetadata.onUpdate); + // console.log("isPrimary:", tableColumn.isPrimary, columnMetadata.isPrimary); + // console.log("isNullable:", tableColumn.isNullable, columnMetadata.isNullable); + // console.log("isUnique:", tableColumn.isUnique, this.normalizeIsUnique(columnMetadata)); + // console.log("isGenerated:", tableColumn.isGenerated, columnMetadata.isGenerated); + // console.log((columnMetadata.generationStrategy !== "uuid" && tableColumn.isGenerated !== columnMetadata.isGenerated)); + // console.log("=========================================="); + + let columnMetadataLength = columnMetadata.length; + if (!columnMetadataLength && columnMetadata.generationStrategy === "uuid") { // fixing #3374 + columnMetadataLength = this.getColumnLength(columnMetadata); + } + + return tableColumn.name !== columnMetadata.databaseName + || tableColumn.type !== this.normalizeType(columnMetadata) + || tableColumn.length !== columnMetadataLength + || tableColumn.width !== columnMetadata.width + || tableColumn.precision !== columnMetadata.precision + || tableColumn.scale !== columnMetadata.scale + || tableColumn.zerofill !== columnMetadata.zerofill + || tableColumn.unsigned !== columnMetadata.unsigned + || tableColumn.asExpression !== columnMetadata.asExpression + || tableColumn.generatedType !== columnMetadata.generatedType + // || tableColumn.comment !== columnMetadata.comment // todo + || !this.compareDefaultValues(this.normalizeDefault(columnMetadata), tableColumn.default) + || (tableColumn.enum && columnMetadata.enum && !OrmUtils.isArraysEqual(tableColumn.enum, columnMetadata.enum.map(val => val + ""))) + || tableColumn.onUpdate !== columnMetadata.onUpdate + || tableColumn.isPrimary !== columnMetadata.isPrimary + || tableColumn.isNullable !== columnMetadata.isNullable + || tableColumn.isUnique !== this.normalizeIsUnique(columnMetadata) + || (columnMetadata.generationStrategy !== "uuid" && tableColumn.isGenerated !== columnMetadata.isGenerated); + }); + } + + /** + * Returns true if driver supports RETURNING / OUTPUT statement. + */ + isReturningSqlSupported(): boolean { + return false; + } + + /** + * Returns true if driver supports uuid values generation on its own. + */ + isUUIDGenerationSupported(): boolean { + return false; + } + + /** + * Creates an escaped parameter. + */ + createParameter(parameterName: string, index: number): string { + return "?"; + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Loads all driver dependencies. + */ + protected loadDependencies(): void { + this.DataApiDriver = PlatformTools.load("typeorm-aurora-data-api-driver"); + } + + /** + * Creates a new connection pool for a given database credentials. + */ + protected createConnectionOptions(options: AuroraDataApiConnectionOptions, credentials: AuroraDataApiConnectionCredentialsOptions): Promise { + + credentials = Object.assign(credentials, DriverUtils.buildDriverOptions(credentials)); // todo: do it better way + + // build connection options for the driver + return Object.assign({}, { + resourceArn: options.resourceArn, + secretArn: options.secretArn, + database: options.database, + region: options.region, + type: options.type, + }, { + host: credentials.host, + user: credentials.username, + password: credentials.password, + database: credentials.database, + port: credentials.port, + ssl: options.ssl + }, + + options.extra || {}); + } + + /** + * Creates a new connection pool for a given database credentials. + */ + protected async createPool(connectionOptions: any): Promise { + return {}; + } + + /** + * Attaches all required base handlers to a database connection, such as the unhandled error handler. + */ + 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. + */ + if (connection.listeners("error").length === 0) { + connection.on("error", (error: any) => logger.log("warn", `MySQL connection raised an error. ${error}`)); + } + return connection; + } + + /** + * Checks if "DEFAULT" values in the column metadata and in the database are equal. + */ + protected compareDefaultValues(columnMetadataValue: string, databaseValue: string): boolean { + if (typeof columnMetadataValue === "string" && typeof databaseValue === "string") { + // we need to cut out "'" because in mysql we can understand returned value is a string or a function + // as result compare cannot understand if default is really changed or not + columnMetadataValue = columnMetadataValue.replace(/^'+|'+$/g, ""); + databaseValue = databaseValue.replace(/^'+|'+$/g, ""); + } + + return columnMetadataValue === databaseValue; + } + +} diff --git a/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts b/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts new file mode 100644 index 0000000000..2c38e16c22 --- /dev/null +++ b/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts @@ -0,0 +1,1614 @@ +import {QueryRunner} from "../../query-runner/QueryRunner"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; +import {TransactionAlreadyStartedError} from "../../error/TransactionAlreadyStartedError"; +import {TransactionNotStartedError} from "../../error/TransactionNotStartedError"; +import {TableColumn} from "../../schema-builder/table/TableColumn"; +import {Table} from "../../schema-builder/table/Table"; +import {TableForeignKey} from "../../schema-builder/table/TableForeignKey"; +import {TableIndex} from "../../schema-builder/table/TableIndex"; +import {QueryRunnerAlreadyReleasedError} from "../../error/QueryRunnerAlreadyReleasedError"; +import {View} from "../../schema-builder/view/View"; +import {Query} from "../Query"; +import {AuroraDataApiDriver} from "./AuroraDataApiDriver"; +import {ReadStream} from "../../platform/PlatformTools"; +import {OrmUtils} from "../../util/OrmUtils"; +import {TableIndexOptions} from "../../schema-builder/options/TableIndexOptions"; +import {TableUnique} from "../../schema-builder/table/TableUnique"; +import {BaseQueryRunner} from "../../query-runner/BaseQueryRunner"; +import {Broadcaster} from "../../subscriber/Broadcaster"; +import {ColumnType, PromiseUtils} from "../../index"; +import {TableCheck} from "../../schema-builder/table/TableCheck"; +import {IsolationLevel} from "../types/IsolationLevel"; +import {TableExclusion} from "../../schema-builder/table/TableExclusion"; + +/** + * Runs queries on a single mysql database connection. + */ +export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRunner { + + // ------------------------------------------------------------------------- + // Public Implemented Properties + // ------------------------------------------------------------------------- + + /** + * Database driver used by connection. + */ + + driver: AuroraDataApiDriver; + + // ------------------------------------------------------------------------- + // Protected Properties + // ------------------------------------------------------------------------- + + /** + * Promise used to obtain a database connection from a pool for a first time. + */ + protected databaseConnectionPromise: Promise; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(driver: AuroraDataApiDriver) { + super(); + this.driver = driver; + this.connection = driver.connection; + this.broadcaster = new Broadcaster(this); + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Creates/uses database connection from the connection pool to perform further operations. + * Returns obtained database connection. + */ + async connect(): Promise { + return {}; + } + + /** + * Releases used database connection. + * You cannot use query runner methods once its released. + */ + release(): Promise { + this.isReleased = true; + if (this.databaseConnection) + this.databaseConnection.release(); + return Promise.resolve(); + } + + /** + * Starts transaction on the current connection. + */ + async startTransaction(isolationLevel?: IsolationLevel): Promise { + if (this.isTransactionActive) + throw new TransactionAlreadyStartedError(); + + this.isTransactionActive = true; + await this.driver.client.startTransaction(); + } + + /** + * Commits transaction. + * Error will be thrown if transaction was not started. + */ + async commitTransaction(): Promise { + if (!this.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.driver.client.commitTransaction(); + this.isTransactionActive = false; + } + + /** + * Rollbacks transaction. + * Error will be thrown if transaction was not started. + */ + async rollbackTransaction(): Promise { + if (!this.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.driver.client.rollbackTransaction(); + this.isTransactionActive = false; + } + + /** + * Executes a raw SQL query. + */ + async query(query: string, parameters?: any[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const result = await this.driver.client.query(query, parameters); + + if (result.records) { + return result.records; + } + + return result; + } + + /** + * Returns raw data stream. + */ + stream(query: string, parameters?: any[], onEnd?: Function, onError?: Function): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + return new Promise(async (ok, fail) => { + try { + const databaseConnection = await this.connect(); + const stream = databaseConnection.query(query, parameters); + if (onEnd) stream.on("end", onEnd); + if (onError) stream.on("error", onError); + ok(stream); + + } catch (err) { + fail(err); + } + }); + } + + /** + * Returns all available database names including system databases. + */ + async getDatabases(): Promise { + return Promise.resolve([]); + } + + /** + * Returns all available schema names including system schemas. + * If database parameter specified, returns schemas of that database. + */ + async getSchemas(database?: string): Promise { + throw new Error(`MySql driver does not support table schemas`); + } + + /** + * Checks if database with the given name exist. + */ + async hasDatabase(database: string): Promise { + const result = await this.query(`SELECT * FROM \`INFORMATION_SCHEMA\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\` = '${database}'`); + return result.length ? true : false; + } + + /** + * Checks if schema with the given name exist. + */ + async hasSchema(schema: string): Promise { + throw new Error(`MySql driver does not support table schemas`); + } + + /** + * Checks if table with the given name exist in the database. + */ + async hasTable(tableOrName: Table|string): Promise { + const parsedTableName = this.parseTableName(tableOrName); + const sql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE \`TABLE_SCHEMA\` = '${parsedTableName.database}' AND \`TABLE_NAME\` = '${parsedTableName.tableName}'`; + const result = await this.query(sql); + return result.length ? true : false; + } + + /** + * Checks if column with the given name exist in the given table. + */ + async hasColumn(tableOrName: Table|string, column: TableColumn|string): Promise { + const parsedTableName = this.parseTableName(tableOrName); + const columnName = column instanceof TableColumn ? column.name : column; + const sql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE \`TABLE_SCHEMA\` = '${parsedTableName.database}' AND \`TABLE_NAME\` = '${parsedTableName.tableName}' AND \`COLUMN_NAME\` = '${columnName}'`; + const result = await this.query(sql); + return result.length ? true : false; + } + + /** + * Creates a new database. + */ + async createDatabase(database: string, ifNotExist?: boolean): Promise { + const up = ifNotExist ? `CREATE DATABASE IF NOT EXISTS \`${database}\`` : `CREATE DATABASE \`${database}\``; + const down = `DROP DATABASE \`${database}\``; + await this.executeQueries(new Query(up), new Query(down)); + } + + /** + * Drops database. + */ + async dropDatabase(database: string, ifExist?: boolean): Promise { + const up = ifExist ? `DROP DATABASE IF EXISTS \`${database}\`` : `DROP DATABASE \`${database}\``; + const down = `CREATE DATABASE \`${database}\``; + await this.executeQueries(new Query(up), new Query(down)); + } + + /** + * Creates a new table schema. + */ + async createSchema(schema: string, ifNotExist?: boolean): Promise { + throw new Error(`Schema create queries are not supported by MySql driver.`); + } + + /** + * Drops table schema. + */ + async dropSchema(schemaPath: string, ifExist?: boolean): Promise { + throw new Error(`Schema drop queries are not supported by MySql driver.`); + } + + /** + * Creates a new table. + */ + async createTable(table: Table, ifNotExist: boolean = false, createForeignKeys: boolean = true): Promise { + if (ifNotExist) { + const isTableExist = await this.hasTable(table); + if (isTableExist) return Promise.resolve(); + } + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + upQueries.push(this.createTableSql(table, createForeignKeys)); + downQueries.push(this.dropTableSql(table)); + + // we must first drop indices, than drop foreign keys, because drop queries runs in reversed order + // and foreign keys will be dropped first as indices. This order is very important, because we can't drop index + // if it related to the foreign key. + + // createTable does not need separate method to create indices, because it create indices in the same query with table creation. + table.indices.forEach(index => downQueries.push(this.dropIndexSql(table, index))); + + // if createForeignKeys is true, we must drop created foreign keys in down query. + // createTable does not need separate method to create foreign keys, because it create fk's in the same query with table creation. + if (createForeignKeys) + table.foreignKeys.forEach(foreignKey => downQueries.push(this.dropForeignKeySql(table, foreignKey))); + + return this.executeQueries(upQueries, downQueries); + } + + /** + * Drop the table. + */ + async dropTable(target: Table|string, ifExist?: boolean, dropForeignKeys: boolean = true): Promise { + // It needs because if table does not exist and dropForeignKeys or dropIndices is true, we don't need + // to perform drop queries for foreign keys and indices. + if (ifExist) { + const isTableExist = await this.hasTable(target); + if (!isTableExist) return Promise.resolve(); + } + + // if dropTable called with dropForeignKeys = true, we must create foreign keys in down query. + const createForeignKeys: boolean = dropForeignKeys; + const tableName = target instanceof Table ? target.name : target; + const table = await this.getCachedTable(tableName); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + if (dropForeignKeys) + table.foreignKeys.forEach(foreignKey => upQueries.push(this.dropForeignKeySql(table, foreignKey))); + + table.indices.forEach(index => upQueries.push(this.dropIndexSql(table, index))); + + upQueries.push(this.dropTableSql(table)); + downQueries.push(this.createTableSql(table, createForeignKeys)); + + await this.executeQueries(upQueries, downQueries); + } + + /** + * Creates a new view. + */ + async createView(view: View): Promise { + const upQueries: Query[] = []; + const downQueries: Query[] = []; + upQueries.push(this.createViewSql(view)); + upQueries.push(await this.insertViewDefinitionSql(view)); + downQueries.push(this.dropViewSql(view)); + downQueries.push(await this.deleteViewDefinitionSql(view)); + await this.executeQueries(upQueries, downQueries); + } + + /** + * Drops the view. + */ + async dropView(target: View|string): Promise { + const viewName = target instanceof View ? target.name : target; + const view = await this.getCachedView(viewName); + + const upQueries: Query[] = []; + const downQueries: Query[] = []; + upQueries.push(await this.deleteViewDefinitionSql(view)); + upQueries.push(this.dropViewSql(view)); + downQueries.push(await this.insertViewDefinitionSql(view)); + downQueries.push(this.createViewSql(view)); + await this.executeQueries(upQueries, downQueries); + } + + /** + * Renames a table. + */ + async renameTable(oldTableOrName: Table|string, newTableName: string): Promise { + const upQueries: Query[] = []; + const downQueries: Query[] = []; + const oldTable = oldTableOrName instanceof Table ? oldTableOrName : await this.getCachedTable(oldTableOrName); + const newTable = oldTable.clone(); + const dbName = oldTable.name.indexOf(".") === -1 ? undefined : oldTable.name.split(".")[0]; + newTable.name = dbName ? `${dbName}.${newTableName}` : newTableName; + + // rename table + upQueries.push(new Query(`RENAME TABLE ${this.escapePath(oldTable.name)} TO ${this.escapePath(newTable.name)}`)); + downQueries.push(new Query(`RENAME TABLE ${this.escapePath(newTable.name)} TO ${this.escapePath(oldTable.name)}`)); + + // rename index constraints + newTable.indices.forEach(index => { + // build new constraint name + const columnNames = index.columnNames.map(column => `\`${column}\``).join(", "); + const newIndexName = this.connection.namingStrategy.indexName(newTable, index.columnNames, index.where); + + // build queries + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable)} DROP INDEX \`${index.name}\`, ADD ${indexType}INDEX \`${newIndexName}\` (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(newTable)} DROP INDEX \`${newIndexName}\`, ADD ${indexType}INDEX \`${index.name}\` (${columnNames})`)); + + // replace constraint name + index.name = newIndexName; + }); + + // rename foreign key constraint + newTable.foreignKeys.forEach(foreignKey => { + // build new constraint name + const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(newTable, foreignKey.columnNames); + + // build queries + let up = `ALTER TABLE ${this.escapePath(newTable)} DROP FOREIGN KEY \`${foreignKey.name}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + up += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + up += ` ON UPDATE ${foreignKey.onUpdate}`; + + let down = `ALTER TABLE ${this.escapePath(newTable)} DROP FOREIGN KEY \`${newForeignKeyName}\`, ADD CONSTRAINT \`${foreignKey.name}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + down += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + down += ` ON UPDATE ${foreignKey.onUpdate}`; + + upQueries.push(new Query(up)); + downQueries.push(new Query(down)); + + // replace constraint name + foreignKey.name = newForeignKeyName; + }); + + await this.executeQueries(upQueries, downQueries); + + // rename old table and replace it in cached tabled; + oldTable.name = newTable.name; + this.replaceCachedTable(oldTable, newTable); + } + + /** + * Creates a new column from the column in the table. + */ + async addColumn(tableOrName: Table|string, column: TableColumn): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const clonedTable = table.clone(); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + const skipColumnLevelPrimary = clonedTable.primaryColumns.length > 0; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(column, skipColumnLevelPrimary, false)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN \`${column.name}\``)); + + // create or update primary key constraint + if (column.isPrimary && skipColumnLevelPrimary) { + // if we already have generated column, we must temporary drop AUTO_INCREMENT property. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${column.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(column, true)}`)); + } + + const primaryColumns = clonedTable.primaryColumns; + let columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + + primaryColumns.push(column); + columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + + // if we previously dropped AUTO_INCREMENT property, we must bring it back + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(column, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${column.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + } + } + + // create column index + const columnIndex = clonedTable.indices.find(index => index.columnNames.length === 1 && index.columnNames[0] === column.name); + if (columnIndex) { + upQueries.push(this.createIndexSql(table, columnIndex)); + downQueries.push(this.dropIndexSql(table, columnIndex)); + + } else if (column.isUnique) { + const uniqueIndex = new TableIndex({ + name: this.connection.namingStrategy.indexName(table.name, [column.name]), + columnNames: [column.name], + isUnique: true + }); + clonedTable.indices.push(uniqueIndex); + clonedTable.uniques.push(new TableUnique({ + name: uniqueIndex.name, + columnNames: uniqueIndex.columnNames + })); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${uniqueIndex.name}\` (\`${column.name}\`)`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${uniqueIndex.name}\``)); + } + + await this.executeQueries(upQueries, downQueries); + + clonedTable.addColumn(column); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Creates a new columns from the column in the table. + */ + async addColumns(tableOrName: Table|string, columns: TableColumn[]): Promise { + await PromiseUtils.runInSequence(columns, column => this.addColumn(tableOrName, column)); + } + + /** + * Renames column in the given table. + */ + async renameColumn(tableOrName: Table|string, oldTableColumnOrName: TableColumn|string, newTableColumnOrName: TableColumn|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const oldColumn = oldTableColumnOrName instanceof TableColumn ? oldTableColumnOrName : table.columns.find(c => c.name === oldTableColumnOrName); + if (!oldColumn) + throw new Error(`Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`); + + let newColumn: TableColumn|undefined = undefined; + if (newTableColumnOrName instanceof TableColumn) { + newColumn = newTableColumnOrName; + } else { + newColumn = oldColumn.clone(); + newColumn.name = newTableColumnOrName; + } + + await this.changeColumn(table, oldColumn, newColumn); + } + + /** + * Changes a column in the table. + */ + async changeColumn(tableOrName: Table|string, oldColumnOrName: TableColumn|string, newColumn: TableColumn): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + let clonedTable = table.clone(); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + const oldColumn = oldColumnOrName instanceof TableColumn + ? oldColumnOrName + : table.columns.find(column => column.name === oldColumnOrName); + if (!oldColumn) + throw new Error(`Column "${oldColumnOrName}" was not found in the "${table.name}" table.`); + + if ((newColumn.isGenerated !== oldColumn.isGenerated && newColumn.generationStrategy !== "uuid") + || oldColumn.type !== newColumn.type + || oldColumn.length !== newColumn.length + || oldColumn.generatedType !== newColumn.generatedType) { + await this.dropColumn(table, oldColumn); + await this.addColumn(table, newColumn); + + // update cloned table + clonedTable = table.clone(); + + } else { + if (newColumn.name !== oldColumn.name) { + // We don't change any column properties, just rename it. + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${oldColumn.name}\` \`${newColumn.name}\` ${this.buildCreateColumnSql(oldColumn, true, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${newColumn.name}\` \`${oldColumn.name}\` ${this.buildCreateColumnSql(oldColumn, true, true)}`)); + + // rename index constraints + clonedTable.findColumnIndices(oldColumn).forEach(index => { + // build new constraint name + index.columnNames.splice(index.columnNames.indexOf(oldColumn.name), 1); + index.columnNames.push(newColumn.name); + const columnNames = index.columnNames.map(column => `\`${column}\``).join(", "); + const newIndexName = this.connection.namingStrategy.indexName(clonedTable, index.columnNames, index.where); + + // build queries + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${index.name}\`, ADD ${indexType}INDEX \`${newIndexName}\` (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${newIndexName}\`, ADD ${indexType}INDEX \`${index.name}\` (${columnNames})`)); + + // replace constraint name + index.name = newIndexName; + }); + + // rename foreign key constraints + clonedTable.findColumnForeignKeys(oldColumn).forEach(foreignKey => { + // build new constraint name + foreignKey.columnNames.splice(foreignKey.columnNames.indexOf(oldColumn.name), 1); + foreignKey.columnNames.push(newColumn.name); + const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); + const newForeignKeyName = this.connection.namingStrategy.foreignKeyName(clonedTable, foreignKey.columnNames); + + // build queries + let up = `ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${foreignKey.name}\`, ADD CONSTRAINT \`${newForeignKeyName}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + up += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + up += ` ON UPDATE ${foreignKey.onUpdate}`; + + let down = `ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${newForeignKeyName}\`, ADD CONSTRAINT \`${foreignKey.name}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + down += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + down += ` ON UPDATE ${foreignKey.onUpdate}`; + + upQueries.push(new Query(up)); + downQueries.push(new Query(down)); + + // replace constraint name + foreignKey.name = newForeignKeyName; + }); + + // rename old column in the Table object + const oldTableColumn = clonedTable.columns.find(column => column.name === oldColumn.name); + clonedTable.columns[clonedTable.columns.indexOf(oldTableColumn!)].name = newColumn.name; + oldColumn.name = newColumn.name; + } + + if (this.isColumnChanged(oldColumn, newColumn, true)) { + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${oldColumn.name}\` ${this.buildCreateColumnSql(newColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${newColumn.name}\` ${this.buildCreateColumnSql(oldColumn, true)}`)); + } + + if (newColumn.isPrimary !== oldColumn.isPrimary) { + // if table have generated column, we must drop AUTO_INCREMENT before changing primary constraints. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + } + + const primaryColumns = clonedTable.primaryColumns; + + // if primary column state changed, we must always drop existed constraint. + if (primaryColumns.length > 0) { + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + } + + if (newColumn.isPrimary === true) { + primaryColumns.push(newColumn); + // update column in table + const column = clonedTable.columns.find(column => column.name === newColumn.name); + column!.isPrimary = true; + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + + } else { + const primaryColumn = primaryColumns.find(c => c.name === newColumn.name); + primaryColumns.splice(primaryColumns.indexOf(primaryColumn!), 1); + // update column in table + const column = clonedTable.columns.find(column => column.name === newColumn.name); + column!.isPrimary = false; + + // if we have another primary keys, we must recreate constraint. + if (primaryColumns.length > 0) { + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + } + } + + // if we have generated column, and we dropped AUTO_INCREMENT property before, we must bring it back + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + } + } + + if (newColumn.isUnique !== oldColumn.isUnique) { + if (newColumn.isUnique === true) { + const uniqueIndex = new TableIndex({ + name: this.connection.namingStrategy.indexName(table.name, [newColumn.name]), + columnNames: [newColumn.name], + isUnique: true + }); + clonedTable.indices.push(uniqueIndex); + clonedTable.uniques.push(new TableUnique({ + name: uniqueIndex.name, + columnNames: uniqueIndex.columnNames + })); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${uniqueIndex.name}\` (\`${newColumn.name}\`)`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${uniqueIndex.name}\``)); + + } else { + const uniqueIndex = clonedTable.indices.find(index => { + return index.columnNames.length === 1 && index.isUnique === true && !!index.columnNames.find(columnName => columnName === newColumn.name); + }); + clonedTable.indices.splice(clonedTable.indices.indexOf(uniqueIndex!), 1); + + const tableUnique = clonedTable.uniques.find(unique => unique.name === uniqueIndex!.name); + clonedTable.uniques.splice(clonedTable.uniques.indexOf(tableUnique!), 1); + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${uniqueIndex!.name}\``)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${uniqueIndex!.name}\` (\`${newColumn.name}\`)`)); + } + } + } + + await this.executeQueries(upQueries, downQueries); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Changes a column in the table. + */ + async changeColumns(tableOrName: Table|string, changedColumns: { newColumn: TableColumn, oldColumn: TableColumn }[]): Promise { + await PromiseUtils.runInSequence(changedColumns, changedColumn => this.changeColumn(tableOrName, changedColumn.oldColumn, changedColumn.newColumn)); + } + + /** + * Drops column in the table. + */ + async dropColumn(tableOrName: Table|string, columnOrName: TableColumn|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const column = columnOrName instanceof TableColumn ? columnOrName : table.findColumnByName(columnOrName); + if (!column) + throw new Error(`Column "${columnOrName}" was not found in table "${table.name}"`); + + const clonedTable = table.clone(); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + // drop primary key constraint + if (column.isPrimary) { + // if table have generated column, we must drop AUTO_INCREMENT before changing primary constraints. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + } + + // dropping primary key constraint + const columnNames = clonedTable.primaryColumns.map(primaryColumn => `\`${primaryColumn.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} ADD PRIMARY KEY (${columnNames})`)); + + // update column in table + const tableColumn = clonedTable.findColumnByName(column.name); + tableColumn!.isPrimary = false; + + // if primary key have multiple columns, we must recreate it without dropped column + if (clonedTable.primaryColumns.length > 0) { + const columnNames = clonedTable.primaryColumns.map(primaryColumn => `\`${primaryColumn.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} ADD PRIMARY KEY (${columnNames})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(clonedTable)} DROP PRIMARY KEY`)); + } + + // if we have generated column, and we dropped AUTO_INCREMENT property before, and this column is not current dropping column, we must bring it back + if (generatedColumn && generatedColumn.name !== column.name) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + } + } + + // drop column index + const columnIndex = clonedTable.indices.find(index => index.columnNames.length === 1 && index.columnNames[0] === column.name); + if (columnIndex) { + clonedTable.indices.splice(clonedTable.indices.indexOf(columnIndex), 1); + upQueries.push(this.dropIndexSql(table, columnIndex)); + downQueries.push(this.createIndexSql(table, columnIndex)); + + } else if (column.isUnique) { + // we splice constraints both from table uniques and indices. + const uniqueName = this.connection.namingStrategy.uniqueConstraintName(table.name, [column.name]); + const foundUnique = clonedTable.uniques.find(unique => unique.name === uniqueName); + if (foundUnique) + clonedTable.uniques.splice(clonedTable.uniques.indexOf(foundUnique), 1); + + const indexName = this.connection.namingStrategy.indexName(table.name, [column.name]); + const foundIndex = clonedTable.indices.find(index => index.name === indexName); + if (foundIndex) + clonedTable.indices.splice(clonedTable.indices.indexOf(foundIndex), 1); + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP INDEX \`${indexName}\``)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD UNIQUE INDEX \`${indexName}\` (\`${column.name}\`)`)); + } + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN \`${column.name}\``)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(column, true)}`)); + + await this.executeQueries(upQueries, downQueries); + + clonedTable.removeColumn(column); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Drops the columns in the table. + */ + async dropColumns(tableOrName: Table|string, columns: TableColumn[]): Promise { + await PromiseUtils.runInSequence(columns, column => this.dropColumn(tableOrName, column)); + } + + /** + * Creates a new primary key. + */ + async createPrimaryKey(tableOrName: Table|string, columnNames: string[]): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const clonedTable = table.clone(); + + const up = this.createPrimaryKeySql(table, columnNames); + const down = this.dropPrimaryKeySql(table); + + await this.executeQueries(up, down); + clonedTable.columns.forEach(column => { + if (columnNames.find(columnName => columnName === column.name)) + column.isPrimary = true; + }); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Updates composite primary keys. + */ + async updatePrimaryKeys(tableOrName: Table|string, columns: TableColumn[]): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const clonedTable = table.clone(); + const columnNames = columns.map(column => column.name); + const upQueries: Query[] = []; + const downQueries: Query[] = []; + + // if table have generated column, we must drop AUTO_INCREMENT before changing primary constraints. + const generatedColumn = clonedTable.columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (generatedColumn) { + const nonGeneratedColumn = generatedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${generatedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(generatedColumn, true)}`)); + } + + // if table already have primary columns, we must drop them. + const primaryColumns = clonedTable.primaryColumns; + if (primaryColumns.length > 0) { + const columnNames = primaryColumns.map(column => `\`${column.name}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNames})`)); + } + + // update columns in table. + clonedTable.columns + .filter(column => columnNames.indexOf(column.name) !== -1) + .forEach(column => column.isPrimary = true); + + const columnNamesString = columnNames.map(columnName => `\`${columnName}\``).join(", "); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNamesString})`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`)); + + // if we already have generated column or column is changed to generated, and we dropped AUTO_INCREMENT property before, we must bring it back + const newOrExistGeneratedColumn = generatedColumn ? generatedColumn : columns.find(column => column.isGenerated && column.generationStrategy === "increment"); + if (newOrExistGeneratedColumn) { + const nonGeneratedColumn = newOrExistGeneratedColumn.clone(); + nonGeneratedColumn.isGenerated = false; + nonGeneratedColumn.generationStrategy = undefined; + + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${nonGeneratedColumn.name}\` ${this.buildCreateColumnSql(newOrExistGeneratedColumn, true)}`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} CHANGE \`${newOrExistGeneratedColumn.name}\` ${this.buildCreateColumnSql(nonGeneratedColumn, true)}`)); + + // if column changed to generated, we must update it in table + const changedGeneratedColumn = clonedTable.columns.find(column => column.name === newOrExistGeneratedColumn.name); + changedGeneratedColumn!.isGenerated = true; + changedGeneratedColumn!.generationStrategy = "increment"; + } + + await this.executeQueries(upQueries, downQueries); + this.replaceCachedTable(table, clonedTable); + } + + /** + * Drops a primary key. + */ + async dropPrimaryKey(tableOrName: Table|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const up = this.dropPrimaryKeySql(table); + const down = this.createPrimaryKeySql(table, table.primaryColumns.map(column => column.name)); + await this.executeQueries(up, down); + table.primaryColumns.forEach(column => { + column.isPrimary = false; + }); + } + + /** + * Creates a new unique constraint. + */ + async createUniqueConstraint(tableOrName: Table|string, uniqueConstraint: TableUnique): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Creates a new unique constraints. + */ + async createUniqueConstraints(tableOrName: Table|string, uniqueConstraints: TableUnique[]): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Drops an unique constraint. + */ + async dropUniqueConstraint(tableOrName: Table|string, uniqueOrName: TableUnique|string): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Drops an unique constraints. + */ + async dropUniqueConstraints(tableOrName: Table|string, uniqueConstraints: TableUnique[]): Promise { + throw new Error(`MySql does not support unique constraints. Use unique index instead.`); + } + + /** + * Creates a new check constraint. + */ + async createCheckConstraint(tableOrName: Table|string, checkConstraint: TableCheck): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Creates a new check constraints. + */ + async createCheckConstraints(tableOrName: Table|string, checkConstraints: TableCheck[]): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Drops check constraint. + */ + async dropCheckConstraint(tableOrName: Table|string, checkOrName: TableCheck|string): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Drops check constraints. + */ + async dropCheckConstraints(tableOrName: Table|string, checkConstraints: TableCheck[]): Promise { + throw new Error(`MySql does not support check constraints.`); + } + + /** + * Creates a new exclusion constraint. + */ + async createExclusionConstraint(tableOrName: Table|string, exclusionConstraint: TableExclusion): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Creates a new exclusion constraints. + */ + async createExclusionConstraints(tableOrName: Table|string, exclusionConstraints: TableExclusion[]): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Drops exclusion constraint. + */ + async dropExclusionConstraint(tableOrName: Table|string, exclusionOrName: TableExclusion|string): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Drops exclusion constraints. + */ + async dropExclusionConstraints(tableOrName: Table|string, exclusionConstraints: TableExclusion[]): Promise { + throw new Error(`MySql does not support exclusion constraints.`); + } + + /** + * Creates a new foreign key. + */ + async createForeignKey(tableOrName: Table|string, foreignKey: TableForeignKey): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + + // new FK may be passed without name. In this case we generate FK name manually. + if (!foreignKey.name) + foreignKey.name = this.connection.namingStrategy.foreignKeyName(table.name, foreignKey.columnNames); + + const up = this.createForeignKeySql(table, foreignKey); + const down = this.dropForeignKeySql(table, foreignKey); + await this.executeQueries(up, down); + table.addForeignKey(foreignKey); + } + + /** + * Creates a new foreign keys. + */ + async createForeignKeys(tableOrName: Table|string, foreignKeys: TableForeignKey[]): Promise { + const promises = foreignKeys.map(foreignKey => this.createForeignKey(tableOrName, foreignKey)); + await Promise.all(promises); + } + + /** + * Drops a foreign key. + */ + async dropForeignKey(tableOrName: Table|string, foreignKeyOrName: TableForeignKey|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const foreignKey = foreignKeyOrName instanceof TableForeignKey ? foreignKeyOrName : table.foreignKeys.find(fk => fk.name === foreignKeyOrName); + if (!foreignKey) + throw new Error(`Supplied foreign key was not found in table ${table.name}`); + + const up = this.dropForeignKeySql(table, foreignKey); + const down = this.createForeignKeySql(table, foreignKey); + await this.executeQueries(up, down); + table.removeForeignKey(foreignKey); + } + + /** + * Drops a foreign keys from the table. + */ + async dropForeignKeys(tableOrName: Table|string, foreignKeys: TableForeignKey[]): Promise { + const promises = foreignKeys.map(foreignKey => this.dropForeignKey(tableOrName, foreignKey)); + await Promise.all(promises); + } + + /** + * Creates a new index. + */ + async createIndex(tableOrName: Table|string, index: TableIndex): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + + // new index may be passed without name. In this case we generate index name manually. + if (!index.name) + index.name = this.connection.namingStrategy.indexName(table.name, index.columnNames, index.where); + + const up = this.createIndexSql(table, index); + const down = this.dropIndexSql(table, index); + await this.executeQueries(up, down); + table.addIndex(index, true); + } + + /** + * Creates a new indices + */ + async createIndices(tableOrName: Table|string, indices: TableIndex[]): Promise { + const promises = indices.map(index => this.createIndex(tableOrName, index)); + await Promise.all(promises); + } + + /** + * Drops an index. + */ + async dropIndex(tableOrName: Table|string, indexOrName: TableIndex|string): Promise { + const table = tableOrName instanceof Table ? tableOrName : await this.getCachedTable(tableOrName); + const index = indexOrName instanceof TableIndex ? indexOrName : table.indices.find(i => i.name === indexOrName); + if (!index) + throw new Error(`Supplied index was not found in table ${table.name}`); + + const up = this.dropIndexSql(table, index); + const down = this.createIndexSql(table, index); + await this.executeQueries(up, down); + table.removeIndex(index, true); + } + + /** + * Drops an indices from the table. + */ + async dropIndices(tableOrName: Table|string, indices: TableIndex[]): Promise { + const promises = indices.map(index => this.dropIndex(tableOrName, index)); + await Promise.all(promises); + } + + /** + * Clears all table contents. + * Note: this operation uses SQL's TRUNCATE query which cannot be reverted in transactions. + */ + async clearTable(tableOrName: Table|string): Promise { + await this.query(`TRUNCATE TABLE ${this.escapePath(tableOrName)}`); + } + + /** + * Removes all tables from the currently connected database. + * Be careful using this method and avoid using it in production or migrations + * (because it can clear all your database). + */ + async clearDatabase(database?: string): Promise { + const dbName = database ? database : this.driver.database; + if (dbName) { + const isDatabaseExist = await this.hasDatabase(dbName); + if (!isDatabaseExist) + return Promise.resolve(); + } else { + throw new Error(`Can not clear database. No database is specified`); + } + + await this.startTransaction(); + try { + + const selectViewDropsQuery = `SELECT concat('DROP VIEW IF EXISTS \`', table_schema, '\`.\`', table_name, '\`') AS \`query\` FROM \`INFORMATION_SCHEMA\`.\`VIEWS\` WHERE \`TABLE_SCHEMA\` = '${dbName}'`; + const dropViewQueries: ObjectLiteral[] = await this.query(selectViewDropsQuery); + await Promise.all(dropViewQueries.map(q => this.query(q["query"]))); + + const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`; + const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS \`', table_schema, '\`.\`', table_name, '\`') AS \`query\` FROM \`INFORMATION_SCHEMA\`.\`TABLES\` WHERE \`TABLE_SCHEMA\` = '${dbName}'`; + const enableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 1;`; + + await this.query(disableForeignKeysCheckQuery); + const dropQueries: ObjectLiteral[] = await this.query(dropTablesQuery); + await Promise.all(dropQueries.map(query => this.query(query["query"]))); + await this.query(enableForeignKeysCheckQuery); + + await this.commitTransaction(); + + } catch (error) { + try { // we throw original error even if rollback thrown an error + await this.rollbackTransaction(); + } catch (rollbackError) { } + throw error; + } + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Returns current database. + */ + protected async getCurrentDatabase(): Promise { + const currentDBQuery = await this.query(`SELECT DATABASE() AS \`db_name\``); + return currentDBQuery[0]["db_name"]; + } + + protected async loadViews(viewNames: string[]): Promise { + const hasTable = await this.hasTable(this.getTypeormMetadataTableName()); + if (!hasTable) + return Promise.resolve([]); + + const currentDatabase = await this.getCurrentDatabase(); + const viewsCondition = viewNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`t\`.\`schema\` = '${database}' AND \`t\`.\`name\` = '${name}')`; + }).join(" OR "); + + const query = `SELECT \`t\`.*, \`v\`.\`check_option\` FROM ${this.escapePath(this.getTypeormMetadataTableName())} \`t\` ` + + `INNER JOIN \`information_schema\`.\`views\` \`v\` ON \`v\`.\`table_schema\` = \`t\`.\`schema\` AND \`v\`.\`table_name\` = \`t\`.\`name\` WHERE \`t\`.\`type\` = 'VIEW' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; + const dbViews = await this.query(query); + return dbViews.map((dbView: any) => { + const view = new View(); + const db = dbView["schema"] === currentDatabase ? undefined : dbView["schema"]; + view.name = this.driver.buildTableName(dbView["name"], undefined, db); + view.expression = dbView["value"]; + return view; + }); + } + + /** + * Loads all tables (with given names) from the database and creates a Table from them. + */ + protected async loadTables(tableNames: string[]): Promise { + + // if no tables given then no need to proceed + if (!tableNames || !tableNames.length) + return []; + + const currentDatabase = await this.getCurrentDatabase(); + const tablesCondition = tableNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`TABLE_SCHEMA\` = '${database}' AND \`TABLE_NAME\` = '${name}')`; + }).join(" OR "); + const tablesSql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`TABLES\` WHERE ` + tablesCondition; + + const columnsSql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`COLUMNS\` WHERE ` + tablesCondition; + + const primaryKeySql = `SELECT * FROM \`INFORMATION_SCHEMA\`.\`KEY_COLUMN_USAGE\` WHERE \`CONSTRAINT_NAME\` = 'PRIMARY' AND (${tablesCondition})`; + + const collationsSql = `SELECT \`SCHEMA_NAME\`, \`DEFAULT_CHARACTER_SET_NAME\` as \`CHARSET\`, \`DEFAULT_COLLATION_NAME\` AS \`COLLATION\` FROM \`INFORMATION_SCHEMA\`.\`SCHEMATA\``; + + const indicesCondition = tableNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`s\`.\`TABLE_SCHEMA\` = '${database}' AND \`s\`.\`TABLE_NAME\` = '${name}')`; + }).join(" OR "); + const indicesSql = `SELECT \`s\`.* FROM \`INFORMATION_SCHEMA\`.\`STATISTICS\` \`s\` ` + + `LEFT JOIN \`INFORMATION_SCHEMA\`.\`REFERENTIAL_CONSTRAINTS\` \`rc\` ON \`s\`.\`INDEX_NAME\` = \`rc\`.\`CONSTRAINT_NAME\` ` + + `WHERE (${indicesCondition}) AND \`s\`.\`INDEX_NAME\` != 'PRIMARY' AND \`rc\`.\`CONSTRAINT_NAME\` IS NULL`; + + const foreignKeysCondition = tableNames.map(tableName => { + let [database, name] = tableName.split("."); + if (!name) { + name = database; + database = this.driver.database || currentDatabase; + } + return `(\`kcu\`.\`TABLE_SCHEMA\` = '${database}' AND \`kcu\`.\`TABLE_NAME\` = '${name}')`; + }).join(" OR "); + const foreignKeysSql = `SELECT \`kcu\`.\`TABLE_SCHEMA\`, \`kcu\`.\`TABLE_NAME\`, \`kcu\`.\`CONSTRAINT_NAME\`, \`kcu\`.\`COLUMN_NAME\`, \`kcu\`.\`REFERENCED_TABLE_SCHEMA\`, ` + + `\`kcu\`.\`REFERENCED_TABLE_NAME\`, \`kcu\`.\`REFERENCED_COLUMN_NAME\`, \`rc\`.\`DELETE_RULE\` \`ON_DELETE\`, \`rc\`.\`UPDATE_RULE\` \`ON_UPDATE\` ` + + `FROM \`INFORMATION_SCHEMA\`.\`KEY_COLUMN_USAGE\` \`kcu\` ` + + `INNER JOIN \`INFORMATION_SCHEMA\`.\`REFERENTIAL_CONSTRAINTS\` \`rc\` ON \`rc\`.\`constraint_name\` = \`kcu\`.\`constraint_name\` ` + + `WHERE ` + foreignKeysCondition; + const [dbTables, dbColumns, dbPrimaryKeys, dbCollations, dbIndices, dbForeignKeys]: ObjectLiteral[][] = await Promise.all([ + this.query(tablesSql), + this.query(columnsSql), + this.query(primaryKeySql), + this.query(collationsSql), + this.query(indicesSql), + this.query(foreignKeysSql) + ]); + + // if tables were not found in the db, no need to proceed + if (!dbTables.length) + return []; + + + // create tables for loaded tables + return Promise.all(dbTables.map(async dbTable => { + const table = new Table(); + + const dbCollation = dbCollations.find(coll => coll["SCHEMA_NAME"] === dbTable["TABLE_SCHEMA"])!; + const defaultCollation = dbCollation["COLLATION"]; + const defaultCharset = dbCollation["CHARSET"]; + + // We do not need to join database name, when database is by default. + // In this case we need local variable `tableFullName` for below comparision. + const db = dbTable["TABLE_SCHEMA"] === currentDatabase ? undefined : dbTable["TABLE_SCHEMA"]; + table.name = this.driver.buildTableName(dbTable["TABLE_NAME"], undefined, db); + const tableFullName = this.driver.buildTableName(dbTable["TABLE_NAME"], undefined, dbTable["TABLE_SCHEMA"]); + + // create columns from the loaded columns + table.columns = dbColumns + .filter(dbColumn => this.driver.buildTableName(dbColumn["TABLE_NAME"], undefined, dbColumn["TABLE_SCHEMA"]) === tableFullName) + .map(dbColumn => { + + const columnUniqueIndex = dbIndices.find(dbIndex => { + return this.driver.buildTableName(dbIndex["TABLE_NAME"], undefined, dbIndex["TABLE_SCHEMA"]) === tableFullName + && dbIndex["COLUMN_NAME"] === dbColumn["COLUMN_NAME"] && dbIndex["NON_UNIQUE"] === "0"; + }); + + const tableMetadata = this.connection.entityMetadatas.find(metadata => metadata.tablePath === table.name); + const hasIgnoredIndex = columnUniqueIndex && tableMetadata && tableMetadata.indices + .some(index => index.name === columnUniqueIndex["INDEX_NAME"] && index.synchronize === false); + + const isConstraintComposite = columnUniqueIndex + ? !!dbIndices.find(dbIndex => dbIndex["INDEX_NAME"] === columnUniqueIndex["INDEX_NAME"] && dbIndex["COLUMN_NAME"] !== dbColumn["COLUMN_NAME"]) + : false; + + const tableColumn = new TableColumn(); + tableColumn.name = dbColumn["COLUMN_NAME"]; + tableColumn.type = dbColumn["DATA_TYPE"].toLowerCase(); + + if (this.driver.withWidthColumnTypes.indexOf(tableColumn.type as ColumnType) !== -1) { + const width = dbColumn["COLUMN_TYPE"].substring(dbColumn["COLUMN_TYPE"].indexOf("(") + 1, dbColumn["COLUMN_TYPE"].indexOf(")")); + tableColumn.width = width && !this.isDefaultColumnWidth(table, tableColumn, parseInt(width)) ? parseInt(width) : undefined; + } + + if (dbColumn["COLUMN_DEFAULT"] === null + || dbColumn["COLUMN_DEFAULT"] === undefined) { + tableColumn.default = undefined; + + } else { + tableColumn.default = dbColumn["COLUMN_DEFAULT"] === "CURRENT_TIMESTAMP" ? dbColumn["COLUMN_DEFAULT"] : `'${dbColumn["COLUMN_DEFAULT"]}'`; + } + + if (dbColumn["EXTRA"].indexOf("on update") !== -1) { + tableColumn.onUpdate = dbColumn["EXTRA"].substring(dbColumn["EXTRA"].indexOf("on update") + 10); + } + + if (dbColumn["GENERATION_EXPRESSION"]) { + tableColumn.asExpression = dbColumn["GENERATION_EXPRESSION"]; + tableColumn.generatedType = dbColumn["EXTRA"].indexOf("VIRTUAL") !== -1 ? "VIRTUAL" : "STORED"; + } + + tableColumn.isUnique = !!columnUniqueIndex && !hasIgnoredIndex && !isConstraintComposite; + tableColumn.isNullable = dbColumn["IS_NULLABLE"] === "YES"; + tableColumn.isPrimary = dbPrimaryKeys.some(dbPrimaryKey => { + return this.driver.buildTableName(dbPrimaryKey["TABLE_NAME"], undefined, dbPrimaryKey["TABLE_SCHEMA"]) === tableFullName && dbPrimaryKey["COLUMN_NAME"] === tableColumn.name; + }); + tableColumn.zerofill = dbColumn["COLUMN_TYPE"].indexOf("zerofill") !== -1; + tableColumn.unsigned = tableColumn.zerofill ? true : dbColumn["COLUMN_TYPE"].indexOf("unsigned") !== -1; + tableColumn.isGenerated = dbColumn["EXTRA"].indexOf("auto_increment") !== -1; + if (tableColumn.isGenerated) + tableColumn.generationStrategy = "increment"; + + tableColumn.comment = dbColumn["COLUMN_COMMENT"]; + if (dbColumn["CHARACTER_SET_NAME"]) + tableColumn.charset = dbColumn["CHARACTER_SET_NAME"] === defaultCharset ? undefined : dbColumn["CHARACTER_SET_NAME"]; + if (dbColumn["COLLATION_NAME"]) + tableColumn.collation = dbColumn["COLLATION_NAME"] === defaultCollation ? undefined : dbColumn["COLLATION_NAME"]; + + // check only columns that have length property + if (this.driver.withLengthColumnTypes.indexOf(tableColumn.type as ColumnType) !== -1 && dbColumn["CHARACTER_MAXIMUM_LENGTH"]) { + const length = dbColumn["CHARACTER_MAXIMUM_LENGTH"].toString(); + tableColumn.length = !this.isDefaultColumnLength(table, tableColumn, length) ? length : ""; + } + + if (tableColumn.type === "decimal" || tableColumn.type === "double" || tableColumn.type === "float") { + if (dbColumn["NUMERIC_PRECISION"] !== null && !this.isDefaultColumnPrecision(table, tableColumn, dbColumn["NUMERIC_PRECISION"])) + tableColumn.precision = parseInt(dbColumn["NUMERIC_PRECISION"]); + if (dbColumn["NUMERIC_SCALE"] !== null && !this.isDefaultColumnScale(table, tableColumn, dbColumn["NUMERIC_SCALE"])) + tableColumn.scale = parseInt(dbColumn["NUMERIC_SCALE"]); + } + + if (tableColumn.type === "enum" || tableColumn.type === "simple-enum") { + const colType = dbColumn["COLUMN_TYPE"]; + const items = colType.substring(colType.indexOf("(") + 1, colType.indexOf(")")).split(","); + tableColumn.enum = (items as string[]).map(item => { + return item.substring(1, item.length - 1); + }); + tableColumn.length = ""; + } + + if ((tableColumn.type === "datetime" || tableColumn.type === "time" || tableColumn.type === "timestamp") + && dbColumn["DATETIME_PRECISION"] !== null && dbColumn["DATETIME_PRECISION"] !== undefined + && !this.isDefaultColumnPrecision(table, tableColumn, parseInt(dbColumn["DATETIME_PRECISION"]))) { + tableColumn.precision = parseInt(dbColumn["DATETIME_PRECISION"]); + } + + return tableColumn; + }); + + // find foreign key constraints of table, group them by constraint name and build TableForeignKey. + const tableForeignKeyConstraints = OrmUtils.uniq(dbForeignKeys.filter(dbForeignKey => { + return this.driver.buildTableName(dbForeignKey["TABLE_NAME"], undefined, dbForeignKey["TABLE_SCHEMA"]) === tableFullName; + }), dbForeignKey => dbForeignKey["CONSTRAINT_NAME"]); + + table.foreignKeys = tableForeignKeyConstraints.map(dbForeignKey => { + const foreignKeys = dbForeignKeys.filter(dbFk => dbFk["CONSTRAINT_NAME"] === dbForeignKey["CONSTRAINT_NAME"]); + + // if referenced table located in currently used db, we don't need to concat db name to table name. + const database = dbForeignKey["REFERENCED_TABLE_SCHEMA"] === currentDatabase ? undefined : dbForeignKey["REFERENCED_TABLE_SCHEMA"]; + const referencedTableName = this.driver.buildTableName(dbForeignKey["REFERENCED_TABLE_NAME"], undefined, database); + + return new TableForeignKey({ + name: dbForeignKey["CONSTRAINT_NAME"], + columnNames: foreignKeys.map(dbFk => dbFk["COLUMN_NAME"]), + referencedTableName: referencedTableName, + referencedColumnNames: foreignKeys.map(dbFk => dbFk["REFERENCED_COLUMN_NAME"]), + onDelete: dbForeignKey["ON_DELETE"], + onUpdate: dbForeignKey["ON_UPDATE"] + }); + }); + + // find index constraints of table, group them by constraint name and build TableIndex. + const tableIndexConstraints = OrmUtils.uniq(dbIndices.filter(dbIndex => { + return this.driver.buildTableName(dbIndex["TABLE_NAME"], undefined, dbIndex["TABLE_SCHEMA"]) === tableFullName; + }), dbIndex => dbIndex["INDEX_NAME"]); + + table.indices = tableIndexConstraints.map(constraint => { + const indices = dbIndices.filter(index => { + return index["TABLE_SCHEMA"] === constraint["TABLE_SCHEMA"] + && index["TABLE_NAME"] === constraint["TABLE_NAME"] + && index["INDEX_NAME"] === constraint["INDEX_NAME"]; + }); + return new TableIndex({ + table: table, + name: constraint["INDEX_NAME"], + columnNames: indices.map(i => i["COLUMN_NAME"]), + isUnique: constraint["NON_UNIQUE"] === "0", + isSpatial: constraint["INDEX_TYPE"] === "SPATIAL", + isFulltext: constraint["INDEX_TYPE"] === "FULLTEXT" + }); + }); + + return table; + })); + } + + /** + * Builds create table sql + */ + protected createTableSql(table: Table, createForeignKeys?: boolean): Query { + const columnDefinitions = table.columns.map(column => this.buildCreateColumnSql(column, true)).join(", "); + let sql = `CREATE TABLE ${this.escapePath(table)} (${columnDefinitions}`; + + // we create unique indexes instead of unique constraints, because MySql does not have unique constraints. + // if we mark column as Unique, it means that we create UNIQUE INDEX. + table.columns + .filter(column => column.isUnique) + .forEach(column => { + const isUniqueIndexExist = table.indices.some(index => { + return index.columnNames.length === 1 && !!index.isUnique && index.columnNames.indexOf(column.name) !== -1; + }); + const isUniqueConstraintExist = table.uniques.some(unique => { + return unique.columnNames.length === 1 && unique.columnNames.indexOf(column.name) !== -1; + }); + if (!isUniqueIndexExist && !isUniqueConstraintExist) + table.indices.push(new TableIndex({ + name: this.connection.namingStrategy.uniqueConstraintName(table.name, [column.name]), + columnNames: [column.name], + isUnique: true + })); + }); + + // as MySql does not have unique constraints, we must create table indices from table uniques and mark them as unique. + if (table.uniques.length > 0) { + table.uniques.forEach(unique => { + const uniqueExist = table.indices.some(index => index.name === unique.name); + if (!uniqueExist) { + table.indices.push(new TableIndex({ + name: unique.name, + columnNames: unique.columnNames, + isUnique: true + })); + } + }); + } + + if (table.indices.length > 0) { + const indicesSql = table.indices.map(index => { + const columnNames = index.columnNames.map(columnName => `\`${columnName}\``).join(", "); + if (!index.name) + index.name = this.connection.namingStrategy.indexName(table.name, index.columnNames, index.where); + + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + + return `${indexType}INDEX \`${index.name}\` (${columnNames})`; + }).join(", "); + + sql += `, ${indicesSql}`; + } + + if (table.foreignKeys.length > 0 && createForeignKeys) { + const foreignKeysSql = table.foreignKeys.map(fk => { + const columnNames = fk.columnNames.map(columnName => `\`${columnName}\``).join(", "); + if (!fk.name) + fk.name = this.connection.namingStrategy.foreignKeyName(table.name, fk.columnNames); + const referencedColumnNames = fk.referencedColumnNames.map(columnName => `\`${columnName}\``).join(", "); + + let constraint = `CONSTRAINT \`${fk.name}\` FOREIGN KEY (${columnNames}) REFERENCES ${this.escapePath(fk.referencedTableName)} (${referencedColumnNames})`; + if (fk.onDelete) + constraint += ` ON DELETE ${fk.onDelete}`; + if (fk.onUpdate) + constraint += ` ON UPDATE ${fk.onUpdate}`; + + return constraint; + }).join(", "); + + sql += `, ${foreignKeysSql}`; + } + + if (table.primaryColumns.length > 0) { + const columnNames = table.primaryColumns.map(column => `\`${column.name}\``).join(", "); + sql += `, PRIMARY KEY (${columnNames})`; + } + + sql += `) ENGINE=${table.engine || "InnoDB"}`; + + return new Query(sql); + } + + /** + * Builds drop table sql + */ + protected dropTableSql(tableOrName: Table|string): Query { + return new Query(`DROP TABLE ${this.escapePath(tableOrName)}`); + } + + protected createViewSql(view: View): Query { + if (typeof view.expression === "string") { + return new Query(`CREATE VIEW ${this.escapePath(view)} AS ${view.expression}`); + } else { + return new Query(`CREATE VIEW ${this.escapePath(view)} AS ${view.expression(this.connection).getQuery()}`); + } + } + + protected async insertViewDefinitionSql(view: View): Promise { + const currentDatabase = await this.getCurrentDatabase(); + const expression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery(); + const [query, parameters] = this.connection.createQueryBuilder() + .insert() + .into(this.getTypeormMetadataTableName()) + .values({ type: "VIEW", schema: currentDatabase, name: view.name, value: expression }) + .getQueryAndParameters(); + + return new Query(query, parameters); + } + + /** + * Builds drop view sql. + */ + protected dropViewSql(viewOrPath: View|string): Query { + return new Query(`DROP VIEW ${this.escapePath(viewOrPath)}`); + } + + /** + * Builds remove view sql. + */ + protected async deleteViewDefinitionSql(viewOrPath: View|string): Promise { + const currentDatabase = await this.getCurrentDatabase(); + const viewName = viewOrPath instanceof View ? viewOrPath.name : viewOrPath; + const qb = this.connection.createQueryBuilder(); + const [query, parameters] = qb.delete() + .from(this.getTypeormMetadataTableName()) + .where(`${qb.escape("type")} = 'VIEW'`) + .andWhere(`${qb.escape("schema")} = :schema`, { schema: currentDatabase }) + .andWhere(`${qb.escape("name")} = :name`, { name: viewName }) + .getQueryAndParameters(); + + return new Query(query, parameters); + } + + /** + * Builds create index sql. + */ + protected createIndexSql(table: Table, index: TableIndex): Query { + const columns = index.columnNames.map(columnName => `\`${columnName}\``).join(", "); + let indexType = ""; + if (index.isUnique) + indexType += "UNIQUE "; + if (index.isSpatial) + indexType += "SPATIAL "; + if (index.isFulltext) + indexType += "FULLTEXT "; + return new Query(`CREATE ${indexType}INDEX \`${index.name}\` ON ${this.escapePath(table)} (${columns})`); + } + + /** + * Builds drop index sql. + */ + protected dropIndexSql(table: Table, indexOrName: TableIndex|string): Query { + let indexName = indexOrName instanceof TableIndex ? indexOrName.name : indexOrName; + return new Query(`DROP INDEX \`${indexName}\` ON ${this.escapePath(table)}`); + } + + /** + * Builds create primary key sql. + */ + protected createPrimaryKeySql(table: Table, columnNames: string[]): Query { + const columnNamesString = columnNames.map(columnName => `\`${columnName}\``).join(", "); + return new Query(`ALTER TABLE ${this.escapePath(table)} ADD PRIMARY KEY (${columnNamesString})`); + } + + /** + * Builds drop primary key sql. + */ + protected dropPrimaryKeySql(table: Table): Query { + return new Query(`ALTER TABLE ${this.escapePath(table)} DROP PRIMARY KEY`); + } + + /** + * Builds create foreign key sql. + */ + protected createForeignKeySql(table: Table, foreignKey: TableForeignKey): Query { + const columnNames = foreignKey.columnNames.map(column => `\`${column}\``).join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => `\`${column}\``).join(","); + let sql = `ALTER TABLE ${this.escapePath(table)} ADD CONSTRAINT \`${foreignKey.name}\` FOREIGN KEY (${columnNames}) ` + + `REFERENCES ${this.escapePath(foreignKey.referencedTableName)}(${referencedColumnNames})`; + if (foreignKey.onDelete) + sql += ` ON DELETE ${foreignKey.onDelete}`; + if (foreignKey.onUpdate) + sql += ` ON UPDATE ${foreignKey.onUpdate}`; + + return new Query(sql); + } + + /** + * Builds drop foreign key sql. + */ + protected dropForeignKeySql(table: Table, foreignKeyOrName: TableForeignKey|string): Query { + const foreignKeyName = foreignKeyOrName instanceof TableForeignKey ? foreignKeyOrName.name : foreignKeyOrName; + return new Query(`ALTER TABLE ${this.escapePath(table)} DROP FOREIGN KEY \`${foreignKeyName}\``); + } + + protected parseTableName(target: Table|string) { + const tableName = target instanceof Table ? target.name : target; + return { + database: tableName.indexOf(".") !== -1 ? tableName.split(".")[0] : this.driver.database, + tableName: tableName.indexOf(".") !== -1 ? tableName.split(".")[1] : tableName + }; + } + + /** + * Escapes given table or view path. + */ + protected escapePath(target: Table|View|string, disableEscape?: boolean): string { + const tableName = target instanceof Table || target instanceof View ? target.name : target; + return tableName.split(".").map(i => disableEscape ? i : `\`${i}\``).join("."); + } + + /** + * Builds a part of query to create/change a column. + */ + protected buildCreateColumnSql(column: TableColumn, skipPrimary: boolean, skipName: boolean = false) { + let c = ""; + if (skipName) { + c = this.connection.driver.createFullType(column); + } else { + c = `\`${column.name}\` ${this.connection.driver.createFullType(column)}`; + } + if (column.asExpression) + c += ` AS (${column.asExpression}) ${column.generatedType ? column.generatedType : "VIRTUAL"}`; + + // if you specify ZEROFILL for a numeric column, MySQL automatically adds the UNSIGNED attribute to that column. + if (column.zerofill) { + c += " ZEROFILL"; + } else if (column.unsigned) { + c += " UNSIGNED"; + } + if (column.enum) + c += ` (${column.enum.map(value => "'" + value + "'").join(", ")})`; + if (column.charset) + c += ` CHARACTER SET "${column.charset}"`; + if (column.collation) + c += ` COLLATE "${column.collation}"`; + if (!column.isNullable) + c += " NOT NULL"; + if (column.isNullable) + c += " NULL"; + if (column.isPrimary && !skipPrimary) + c += " PRIMARY KEY"; + if (column.isGenerated && column.generationStrategy === "increment") // don't use skipPrimary here since updates can update already exist primary without auto inc. + c += " AUTO_INCREMENT"; + if (column.comment) + c += ` COMMENT '${column.comment}'`; + if (column.default !== undefined && column.default !== null) + c += ` DEFAULT ${column.default}`; + if (column.onUpdate) + c += ` ON UPDATE ${column.onUpdate}`; + + return c; + } + +} diff --git a/src/driver/types/DatabaseType.ts b/src/driver/types/DatabaseType.ts index a8c9f0a99f..83754c1a99 100644 --- a/src/driver/types/DatabaseType.ts +++ b/src/driver/types/DatabaseType.ts @@ -14,4 +14,5 @@ export type DatabaseType = "oracle"| "mssql"| "mongodb"| + "aurora-data-api"| "expo";