From 7fc08acecba3f98786a9e5abe1e7044919dd546c Mon Sep 17 00:00:00 2001 From: Umed Khudoiberdiev Date: Mon, 29 Aug 2016 16:17:44 +0500 Subject: [PATCH] added mariadb driver --- config/parameters.json | 14 + package.json | 1 + src/connection/ConnectionManager.ts | 3 + src/driver/Driver.ts | 9 +- src/driver/DriverOptions.ts | 2 +- src/driver/QueryRunner.ts | 2 +- .../error/DriverPoolingNotSupportedError.ts | 14 + src/driver/mariadb/MariaDbDriver.ts | 332 +++++++++++ src/driver/mariadb/MariaDbQueryRunner.ts | 517 ++++++++++++++++++ src/driver/mysql/MysqlDriver.ts | 21 +- src/driver/mysql/MysqlQueryRunner.ts | 6 +- src/driver/postgres/PostgresDriver.ts | 19 +- src/driver/postgres/PostgresQueryRunner.ts | 15 +- src/metadata/RelationMetadata.ts | 27 +- src/metadata/TableMetadata.ts | 2 + src/metadata/types/ColumnTypes.ts | 13 + src/persistment/PersistOperationExecutor.ts | 8 +- src/query-builder/QueryBuilder.ts | 6 +- .../RawSqlResultsToEntityTransformer.ts | 8 +- .../decorators/relation-id/relation-id.ts | 1 + test/utils/test-utils.ts | 46 +- 21 files changed, 1025 insertions(+), 41 deletions(-) create mode 100644 src/driver/error/DriverPoolingNotSupportedError.ts create mode 100644 src/driver/mariadb/MariaDbDriver.ts create mode 100644 src/driver/mariadb/MariaDbQueryRunner.ts diff --git a/config/parameters.json b/config/parameters.json index 048aa4332f..925f09901b 100644 --- a/config/parameters.json +++ b/config/parameters.json @@ -14,6 +14,20 @@ "password": "admin", "database": "test2" }, + "mariadb": { + "host": "192.168.99.100", + "port": 3307, + "username": "root", + "password": "admin", + "database": "test" + }, + "mariadbSecondary": { + "host": "192.168.99.100", + "port": 3307, + "username": "root", + "password": "admin", + "database": "test2" + }, "postgres": { "host": "192.168.99.100", "port": 5432, diff --git a/package.json b/package.json index 0f12264bf4..8038749530 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "gulp-tslint": "^6.0.2", "gulp-typescript": "^2.13.6", "gulpclass": "0.1.1", + "mariasql": "^0.2.6", "mocha": "^3.0.1", "mysql": "^2.11.1", "pg": "^6.0.3", diff --git a/src/connection/ConnectionManager.ts b/src/connection/ConnectionManager.ts index e36b1e6e12..705c2ed987 100644 --- a/src/connection/ConnectionManager.ts +++ b/src/connection/ConnectionManager.ts @@ -8,6 +8,7 @@ import {MissingDriverError} from "./error/MissingDriverError"; import {PostgresDriver} from "../driver/postgres/PostgresDriver"; import {AlreadyHasActiveConnectionError} from "./error/AlreadyHasActiveConnectionError"; import {Logger} from "../logger/Logger"; +import {MariaDbDriver} from "../driver/mariadb/MariaDbDriver"; /** * Connection manager holds all connections made to the databases and providers helper management functions @@ -117,6 +118,8 @@ export class ConnectionManager { return new MysqlDriver(options, logger); case "postgres": return new PostgresDriver(options, logger); + case "mariadb": + return new MariaDbDriver(options, logger); default: throw new MissingDriverError(options.type); } diff --git a/src/driver/Driver.ts b/src/driver/Driver.ts index c02823fff0..47f1799d97 100644 --- a/src/driver/Driver.ts +++ b/src/driver/Driver.ts @@ -2,6 +2,7 @@ import {DriverOptions} from "./DriverOptions"; import {QueryRunner} from "./QueryRunner"; import {ColumnMetadata} from "../metadata/ColumnMetadata"; import {ObjectLiteral} from "../common/ObjectLiteral"; +import {ColumnType} from "../metadata/types/ColumnTypes"; /** * Driver organizes TypeORM communication with specific database management system. @@ -62,8 +63,14 @@ export interface Driver { preparePersistentValue(value: any, column: ColumnMetadata): any; /** - * Prepares given value to a value to be hydrated, based on its column type and metadata. + * Prepares given value to a value to be persisted, based on its column metadata. + */ + prepareHydratedValue(value: any, type: ColumnType): any; + + /** + * Prepares given value to a value to be persisted, based on its column type. */ prepareHydratedValue(value: any, column: ColumnMetadata): any; + } \ No newline at end of file diff --git a/src/driver/DriverOptions.ts b/src/driver/DriverOptions.ts index 9da6b4f6f4..c42027dd57 100644 --- a/src/driver/DriverOptions.ts +++ b/src/driver/DriverOptions.ts @@ -6,7 +6,7 @@ export interface DriverOptions { /** * Database type. Mysql and postgres are the only drivers supported at this moment. */ - readonly type: "mysql"|"postgres"; + readonly type: "mysql"|"postgres"|"mariadb"; /** * Url to where perform connection. diff --git a/src/driver/QueryRunner.ts b/src/driver/QueryRunner.ts index 7d46df871a..5cc5d6d696 100644 --- a/src/driver/QueryRunner.ts +++ b/src/driver/QueryRunner.ts @@ -55,7 +55,7 @@ export interface QueryRunner { /** * Inserts a new row into given table. */ - insert(tableName: string, valuesMap: Object, idColumnName?: string): Promise; + insert(tableName: string, valuesMap: Object, idColumn?: ColumnMetadata): Promise; /** * Performs a simple DELETE query by a given conditions in a given table. diff --git a/src/driver/error/DriverPoolingNotSupportedError.ts b/src/driver/error/DriverPoolingNotSupportedError.ts new file mode 100644 index 0000000000..9f57701d2f --- /dev/null +++ b/src/driver/error/DriverPoolingNotSupportedError.ts @@ -0,0 +1,14 @@ +/** + * Thrown if database driver does not support pooling. + * + * @internal + */ +export class DriverPoolingNotSupportedError extends Error { + name = "DriverPoolingNotSupportedError"; + + constructor(driverName: string) { + super(); + this.message = `Connection pooling is not supported by (${driverName}) driver.`; + } + +} \ No newline at end of file diff --git a/src/driver/mariadb/MariaDbDriver.ts b/src/driver/mariadb/MariaDbDriver.ts new file mode 100644 index 0000000000..ae1ec3f6b4 --- /dev/null +++ b/src/driver/mariadb/MariaDbDriver.ts @@ -0,0 +1,332 @@ +import {Driver} from "../Driver"; +import {ConnectionIsNotSetError} from "../error/ConnectionIsNotSetError"; +import {DriverOptions} from "../DriverOptions"; +import {DatabaseConnection} from "../DatabaseConnection"; +import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError"; +import {DriverPackageLoadError} from "../error/DriverPackageLoadError"; +import {DriverUtils} from "../DriverUtils"; +import {Logger} from "../../logger/Logger"; +import {QueryRunner} from "../QueryRunner"; +import {MariaDbQueryRunner} from "./MariaDbQueryRunner"; +import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes"; +import * as moment from "moment"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; +import {ColumnMetadata} from "../../metadata/ColumnMetadata"; +import {DriverPoolingNotSupportedError} from "../error/DriverPoolingNotSupportedError"; + +/** + * Organizes communication with MariaDB DBMS. + */ +export class MariaDbDriver implements Driver { + + // ------------------------------------------------------------------------- + // Public Properties + // ------------------------------------------------------------------------- + + /** + * Driver connection options. + */ + readonly options: DriverOptions; + + // ------------------------------------------------------------------------- + // Protected Properties + // ------------------------------------------------------------------------- + + /** + * MariaDB library. + */ + protected mariaSql: any; + + /** + * Connection to mariadb database. + */ + protected databaseConnection: DatabaseConnection|undefined; + + /** + * MariaDB pool. + */ + protected pool: any; + + /** + * Pool of database connections. + */ + protected databaseConnectionPool: DatabaseConnection[] = []; + + /** + * Logger used go log queries and errors. + */ + protected logger: Logger; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(options: DriverOptions, logger: Logger, mariadb?: any) { + + this.options = DriverUtils.buildDriverOptions(options); + this.logger = logger; + this.mariaSql = mariadb; + + // validate options to make sure everything is set + DriverUtils.validateDriverOptions(this.options); + + // if mariadb package instance was not set explicitly then try to load it + if (!mariadb) + this.loadDependencies(); + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Performs connection to the database. + * Based on pooling options, it can either create connection immediately, + * either create a pool and create connection when needed. + */ + connect(): Promise { + + // build connection options for the driver + const options = Object.assign({}, { + host: this.options.host, + user: this.options.username, + password: this.options.password, + db: this.options.database, + port: this.options.port + }, this.options.extra || {}); + + // pooling is not supported out of the box my mariasql driver, + // so we disable it by default, and if client try to enable it - we throw an error + if (/*this.options.usePool === undefined || */this.options.usePool === true) { + return Promise.reject(new DriverPoolingNotSupportedError("mariasql")); + // this.pool = this.mariadb.createPool(options); + // return Promise.resolve(); + + } else { + return new Promise((ok, fail) => { + const connection = new this.mariaSql(options); + this.databaseConnection = { + id: 1, + connection: connection, + isTransactionActive: false + }; + this.databaseConnection.connection.connect((err: any) => err ? fail(err) : ok()); + }); + } + } + + /** + * Closes connection with the database. + */ + disconnect(): Promise { + if (!this.databaseConnection && !this.pool) + throw new ConnectionIsNotSetError("mariadb"); + + return new Promise((ok, fail) => { + // const handler = (err: any) => err ? fail(err) : ok(); + + // if pooling is used, then disconnect from it + /*if (this.pool) { + this.pool.end(handler); + this.pool = undefined; + this.databaseConnectionPool = []; + }*/ // commented since pooling is not supported yet + + // if single connection is opened, then close it + if (this.databaseConnection) { + this.databaseConnection.connection.end(); + this.databaseConnection = undefined; + ok(); + } + }); + } + + /** + * Creates a query runner used for common queries. + */ + async createQueryRunner(): Promise { + if (!this.databaseConnection && !this.pool) + return Promise.reject(new ConnectionIsNotSetError("mariadb")); + + const databaseConnection = await this.retrieveDatabaseConnection(); + return new MariaDbQueryRunner(databaseConnection, this, this.logger); + } + + /** + * Access to the native implementation of the database. + */ + nativeInterface() { + return { + driver: this.mariaSql, + connection: this.databaseConnection ? this.databaseConnection.connection : undefined, + pool: this.pool + }; + } + + /** + * 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): [string, any[]] { + if (!parameters || !Object.keys(parameters).length) + return [sql, []]; + const escapedParameters: any[] = []; + const keys = Object.keys(parameters).map(parameter => "(:" + parameter + "\\b)").join("|"); + sql = sql.replace(new RegExp(keys, "g"), (key: string) => { + escapedParameters.push(parameters[key.substr(1)]); + return "?"; + }); // todo: make replace only in value statements, otherwise problems + return [sql, escapedParameters]; + } + + /** + * Escapes a column name. + */ + escapeColumnName(columnName: string): string { + return columnName; // "`" + columnName + "`"; + } + + /** + * Escapes an alias. + */ + escapeAliasName(aliasName: string): string { + return aliasName; // "`" + aliasName + "`"; + } + + /** + * Escapes a table name. + */ + escapeTableName(tableName: string): string { + return tableName; // "`" + tableName + "`"; + } + + /** + * Prepares given value to a value to be persisted, based on its column type and metadata. + */ + preparePersistentValue(value: any, column: ColumnMetadata): any { + switch (column.type) { + case ColumnTypes.BOOLEAN: + return value === true ? 1 : 0; + case ColumnTypes.DATE: + return moment(value).format("YYYY-MM-DD"); + case ColumnTypes.TIME: + return moment(value).format("HH:mm:ss"); + case ColumnTypes.DATETIME: + return moment(value).format("YYYY-MM-DD HH:mm:ss"); + case ColumnTypes.JSON: + return JSON.stringify(value); + case ColumnTypes.SIMPLE_ARRAY: + return (value as any[]) + .map(i => String(i)) + .join(","); + } + + return value; + } + + /** + * Prepares given value to a value to be persisted, based on its column metadata. + */ + prepareHydratedValue(value: any, type: ColumnType): any; + + /** + * Prepares given value to a value to be persisted, based on its column type. + */ + prepareHydratedValue(value: any, column: ColumnMetadata): any; + + /** + * Prepares given value to a value to be persisted, based on its column type or metadata. + */ + prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any { + const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType; + if (ColumnTypes.isNumeric(type) && value !== null && value !== undefined) + return parseInt(value); + + switch (type) { + case ColumnTypes.BOOLEAN: + return value ? true : false; + + case ColumnTypes.DATE: + if (value instanceof Date) + return value; + + return moment(value, "YYYY-MM-DD").toDate(); + + case ColumnTypes.TIME: + return moment(value, "HH:mm:ss").toDate(); + + case ColumnTypes.DATETIME: + if (value instanceof Date) + return value; + + return moment(value, "YYYY-MM-DD HH:mm:ss").toDate(); + + case ColumnTypes.JSON: + return JSON.parse(value); + + case ColumnTypes.SIMPLE_ARRAY: + return (value as string).split(","); + } + + return value; + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Retrieves a new database connection. + * If pooling is enabled then connection from the pool will be retrieved. + * Otherwise active connection will be returned. + */ + protected retrieveDatabaseConnection(): Promise { + + /*if (this.pool) { + return new Promise((ok, fail) => { + this.pool.getConnection((err: any, connection: any) => { + if (err) + return fail(err); + + let dbConnection = this.databaseConnectionPool.find(dbConnection => dbConnection.connection === connection); + if (!dbConnection) { + dbConnection = { + id: this.databaseConnectionPool.length, + connection: connection, + isTransactionActive: false + }; + dbConnection.releaseCallback = () => { + if (this.pool && dbConnection) { + connection.release(); + this.databaseConnectionPool.splice(this.databaseConnectionPool.indexOf(dbConnection), 1); + } + return Promise.resolve(); + }; + this.databaseConnectionPool.push(dbConnection); + } + ok(dbConnection); + }); + }); + }*/ // commented since pooling is not supported yet + + if (this.databaseConnection) + return Promise.resolve(this.databaseConnection); + + throw new ConnectionIsNotSetError("mariadb"); + } + + /** + * If driver dependency is not given explicitly, then try to load it via "require". + */ + protected loadDependencies(): void { + if (!require) + throw new DriverPackageLoadError(); + + try { + this.mariaSql = require("mariasql"); + } catch (e) { + throw new DriverPackageNotInstalledError("MariaDB", "mariasql"); + } + } + +} \ No newline at end of file diff --git a/src/driver/mariadb/MariaDbQueryRunner.ts b/src/driver/mariadb/MariaDbQueryRunner.ts new file mode 100644 index 0000000000..9b84b8c117 --- /dev/null +++ b/src/driver/mariadb/MariaDbQueryRunner.ts @@ -0,0 +1,517 @@ +import {QueryRunner} from "../QueryRunner"; +import {DatabaseConnection} from "../DatabaseConnection"; +import {ObjectLiteral} from "../../common/ObjectLiteral"; +import {TransactionAlreadyStartedError} from "../error/TransactionAlreadyStartedError"; +import {TransactionNotStartedError} from "../error/TransactionNotStartedError"; +import {Logger} from "../../logger/Logger"; +import {MariaDbDriver} from "./MariaDbDriver"; +import {DataTypeNotSupportedByDriverError} from "../error/DataTypeNotSupportedByDriverError"; +import {IndexMetadata} from "../../metadata/IndexMetadata"; +import {ForeignKeyMetadata} from "../../metadata/ForeignKeyMetadata"; +import {ColumnSchema} from "../../schema-builder/ColumnSchema"; +import {ColumnMetadata} from "../../metadata/ColumnMetadata"; +import {TableMetadata} from "../../metadata/TableMetadata"; +import {TableSchema} from "../../schema-builder/TableSchema"; +import {UniqueKeySchema} from "../../schema-builder/UniqueKeySchema"; +import {ForeignKeySchema} from "../../schema-builder/ForeignKeySchema"; +import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema"; +import {IndexSchema} from "../../schema-builder/IndexSchema"; +import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError"; +import {ColumnTypes} from "../../metadata/types/ColumnTypes"; + +/** + * Runs queries on a single MariaDB database connection. + */ +export class MariaDbQueryRunner implements QueryRunner { + + // ------------------------------------------------------------------------- + // Protected Properties + // ------------------------------------------------------------------------- + + /** + * Indicates if connection for this query runner is released. + * Once its released, query runner cannot run queries anymore. + */ + protected isReleased = false; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + constructor(protected databaseConnection: DatabaseConnection, + protected driver: MariaDbDriver, + protected logger: Logger) { + } + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + + /** + * Releases database connection. This is needed when using connection pooling. + * If connection is not from a pool, it should not be released. + * You cannot use this class's methods after its released. + */ + release(): Promise { + if (this.databaseConnection.releaseCallback) { + this.isReleased = true; + return this.databaseConnection.releaseCallback(); + } + + return Promise.resolve(); + } + + /** + * Removes all tables from the currently connected database. + */ + async clearDatabase(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const disableForeignKeysCheckQuery = `SET FOREIGN_KEY_CHECKS = 0;`; + const dropTablesQuery = `SELECT concat('DROP TABLE IF EXISTS ', table_name, ';') AS query FROM information_schema.tables WHERE table_schema = '${this.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); + } + + /** + * Starts transaction. + */ + async beginTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + if (this.databaseConnection.isTransactionActive) + throw new TransactionAlreadyStartedError(); + + await this.query("START TRANSACTION"); + this.databaseConnection.isTransactionActive = true; + } + + /** + * Commits transaction. + */ + async commitTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + if (!this.databaseConnection.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.query("COMMIT"); + this.databaseConnection.isTransactionActive = false; + } + + /** + * Rollbacks transaction. + */ + async rollbackTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + if (!this.databaseConnection.isTransactionActive) + throw new TransactionNotStartedError(); + + await this.query("ROLLBACK"); + this.databaseConnection.isTransactionActive = false; + } + + /** + * Checks if transaction is in progress. + */ + isTransactionActive(): boolean { + return this.databaseConnection.isTransactionActive; + } + + /** + * Executes a given SQL query. + */ + query(query: string, parameters?: any[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + if (parameters) { + parameters = parameters.map(parameter => { + if (typeof parameter === "boolean") { + return parameter === true ? "1" : "0"; + } + + return parameter; + }); + } + + // console.log(query); + // console.log(parameters); + this.logger.logQuery(query); + return new Promise((ok, fail) => { + this.databaseConnection.connection.query(query, parameters, (err: any, result: any) => { + if (err) { + this.logger.logFailedQuery(query); + this.logger.logQueryError(err); + return fail(err); + } + + ok(result); + }); + }); + } + + /** + * Insert a new row with given values into given table. + */ + async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const keys = Object.keys(keyValues); + const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", "); + const values = keys.map(key => "?").join(","); + const parameters = keys.map(key => keyValues[key]); + const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`; + const result = await this.query(sql, parameters); + if (result.info && idColumn) { + const id = result.info.insertId; + if (ColumnTypes.isNumeric(idColumn.type)) { + return +id; + } + return id; + } + } + + /** + * Updates rows that match given conditions in the given table. + */ + async update(tableName: string, valuesMap: ObjectLiteral, conditions: ObjectLiteral): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const updateValues = this.parametrize(valuesMap).join(", "); + const conditionString = this.parametrize(conditions).join(" AND "); + const sql = `UPDATE ${this.driver.escapeTableName(tableName)} SET ${updateValues} ${conditionString ? (" WHERE " + conditionString) : ""}`; + const conditionParams = Object.keys(conditions).map(key => conditions[key]); + const updateParams = Object.keys(valuesMap).map(key => valuesMap[key]); + const allParameters = updateParams.concat(conditionParams); + await this.query(sql, allParameters); + } + + /** + * Deletes from the given table by a given conditions. + */ + async delete(tableName: string, conditions: ObjectLiteral): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const conditionString = this.parametrize(conditions).join(" AND "); + const sql = `DELETE FROM ${this.driver.escapeTableName(tableName)} WHERE ${conditionString}`; + const parameters = Object.keys(conditions).map(key => conditions[key]); + await this.query(sql, parameters); + } + + /** + * Inserts rows into the closure table. + */ + async insertIntoClosureTable(tableName: string, newEntityId: any, parentId: any, hasLevel: boolean): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + let sql = ""; + if (hasLevel) { + sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant, level) ` + + `SELECT ancestor, ${newEntityId}, level + 1 FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` + + `UNION ALL SELECT ${newEntityId}, ${newEntityId}, 1`; + } else { + sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(ancestor, descendant) ` + + `SELECT ancestor, ${newEntityId} FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId} ` + + `UNION ALL SELECT ${newEntityId}, ${newEntityId}`; + } + await this.query(sql); + const results: ObjectLiteral[] = await this.query(`SELECT MAX(level) as level FROM ${this.driver.escapeTableName(tableName)} WHERE descendant = ${parentId}`); + return results && results[0] && results[0]["level"] ? parseInt(results[0]["level"]) + 1 : 1; + } + + /** + * Loads all tables (with given names) from the database and creates a TableSchema from them. + */ + async loadSchemaTables(tableNames: string[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + // if no tables given then no need to proceed + if (!tableNames) + return []; + + // load tables, columns, indices and foreign keys + const tablesSql = `SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '${this.dbName}'`; + const columnsSql = `SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = '${this.dbName}'`; + const indicesSql = `SELECT * FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = '${this.dbName}' AND INDEX_NAME != 'PRIMARY'`; + const foreignKeysSql = `SELECT * FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = '${this.dbName}' AND REFERENCED_COLUMN_NAME IS NOT NULL`; + const uniqueKeysSql = `SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = '${this.dbName}' AND CONSTRAINT_TYPE = 'UNIQUE'`; + const primaryKeysSql = `SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = '${this.dbName}' AND CONSTRAINT_TYPE = 'PRIMARY KEY'`; + const [dbTables, dbColumns, dbIndices, dbForeignKeys, dbUniqueKeys, primaryKeys]: ObjectLiteral[][] = await Promise.all([ + this.query(tablesSql), + this.query(columnsSql), + this.query(indicesSql), + this.query(foreignKeysSql), + this.query(uniqueKeysSql), + this.query(primaryKeysSql), + ]); + + // if tables were not found in the db, no need to proceed + if (!dbTables.length) + return []; + + // create table schemas for loaded tables + return dbTables.map(dbTable => { + const tableSchema = new TableSchema(dbTable["TABLE_NAME"]); + + // create column schemas from the loaded columns + tableSchema.columns = dbColumns + .filter(dbColumn => dbColumn["TABLE_NAME"] === tableSchema.name) + .map(dbColumn => { + const columnSchema = new ColumnSchema(); + columnSchema.name = dbColumn["COLUMN_NAME"]; + columnSchema.type = dbColumn["COLUMN_TYPE"].toLowerCase(); // todo: use normalize type? + columnSchema.default = dbColumn["COLUMN_DEFAULT"] !== null && dbColumn["COLUMN_DEFAULT"] !== undefined ? dbColumn["COLUMN_DEFAULT"] : undefined; + columnSchema.isNullable = dbColumn["IS_NULLABLE"] === "YES"; + columnSchema.isPrimary = dbColumn["COLUMN_KEY"].indexOf("PRI") !== -1; + columnSchema.isGenerated = dbColumn["EXTRA"].indexOf("auto_increment") !== -1; + columnSchema.comment = dbColumn["COLUMN_COMMENT"]; + return columnSchema; + }); + + // create primary key schema + const primaryKey = primaryKeys.find(primaryKey => primaryKey["TABLE_NAME"] === tableSchema.name); + if (primaryKey) + tableSchema.primaryKey = new PrimaryKeySchema(primaryKey["CONSTRAINT_NAME"]); + + // create foreign key schemas from the loaded indices + tableSchema.foreignKeys = dbForeignKeys + .filter(dbForeignKey => dbForeignKey["TABLE_NAME"] === tableSchema.name) + .map(dbForeignKey => new ForeignKeySchema(dbForeignKey["CONSTRAINT_NAME"])); + + // create unique key schemas from the loaded indices + tableSchema.uniqueKeys = dbUniqueKeys + .filter(dbUniqueKey => dbUniqueKey["TABLE_NAME"] === tableSchema.name) + .map(dbUniqueKey => new UniqueKeySchema(dbUniqueKey["CONSTRAINT_NAME"])); + + // create index schemas from the loaded indices + tableSchema.indices = dbIndices + .filter(dbIndex => { + return dbIndex["table_name"] === tableSchema.name && + (!tableSchema.foreignKeys || !tableSchema.foreignKeys.find(foreignKey => foreignKey.name === dbIndex["index_name"])) && + (!tableSchema.primaryKey || tableSchema.primaryKey.name !== dbIndex["index_name"]); + }) + .map(dbIndex => dbIndex["INDEX_NAME"]) + .filter((value, index, self) => self.indexOf(value) === index) // unqiue + .map(dbIndexName => { + const columnNames = dbIndices + .filter(dbIndex => dbIndex["TABLE_NAME"] === tableSchema.name && dbIndex["INDEX_NAME"] === dbIndexName) + .map(dbIndex => dbIndex["COLUMN_NAME"]); + + return new IndexSchema(dbIndexName, columnNames); + }); + + return tableSchema; + }); + } + + /** + * Creates a new table from the given table metadata and column metadatas. + */ + async createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const columnDefinitions = columns.map(column => this.buildCreateColumnSql(column, false)).join(", "); + const sql = `CREATE TABLE \`${table.name}\` (${columnDefinitions}) ENGINE=InnoDB;`; + await this.query(sql); + } + + /** + * Creates a new column from the column metadata in the table. + */ + async createColumn(tableName: string, column: ColumnMetadata): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `ALTER TABLE \`${tableName}\` ADD ${this.buildCreateColumnSql(column, false)}`; + await this.query(sql); + } + + /** + * Changes a column in the table. + */ + async changeColumn(tableName: string, oldColumn: ColumnSchema, newColumn: ColumnMetadata): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `ALTER TABLE \`${tableName}\` CHANGE \`${oldColumn.name}\` ${this.buildCreateColumnSql(newColumn, oldColumn.isPrimary)}`; // todo: CHANGE OR MODIFY COLUMN ???? + await this.query(sql); + } + + /** + * Drops the column in the table. + */ + async dropColumn(tableName: string, columnName: string): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `ALTER TABLE \`${tableName}\` DROP \`${columnName}\``; + await this.query(sql); + } + + /** + * Creates a new foreign. + */ + async createForeignKey(foreignKey: ForeignKeyMetadata): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const columnNames = foreignKey.columnNames.map(column => "`" + column + "`").join(", "); + const referencedColumnNames = foreignKey.referencedColumnNames.map(column => "`" + column + "`").join(","); + let sql = `ALTER TABLE ${foreignKey.tableName} ADD CONSTRAINT \`${foreignKey.name}\` ` + + `FOREIGN KEY (${columnNames}) ` + + `REFERENCES \`${foreignKey.referencedTable.name}\`(${referencedColumnNames})`; + if (foreignKey.onDelete) sql += " ON DELETE " + foreignKey.onDelete; + await this.query(sql); + } + + /** + * Drops a foreign key from the table. + */ + async dropForeignKey(tableName: string, foreignKeyName: string): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${foreignKeyName}\``; + await this.query(sql); + } + + /** + * Creates a new index. + */ + async createIndex(tableName: string, index: IndexMetadata): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const columns = index.columns.map(column => "`" + column + "`").join(", "); + const sql = `CREATE ${index.isUnique ? "UNIQUE" : ""} INDEX \`${index.name}\` ON \`${tableName}\`(${columns})`; + await this.query(sql); + } + + /** + * Drops an index from the table. + */ + async dropIndex(tableName: string, indexName: string): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `ALTER TABLE \`${tableName}\` DROP INDEX \`${indexName}\``; + await this.query(sql); + } + + /** + * Creates a new unique key. + */ + async createUniqueKey(tableName: string, columnName: string, keyName: string): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `ALTER TABLE \`${tableName}\` ADD CONSTRAINT \`${keyName}\` UNIQUE (\`${columnName}\`)`; + await this.query(sql); + } + + /** + * Creates a database type from a given column metadata. + */ + normalizeType(column: ColumnMetadata) { + switch (column.normalizedDataType) { + case "string": + return "varchar(" + (column.length ? column.length : 255) + ")"; + case "text": + return "text"; + case "boolean": + return "tinyint(1)"; + case "integer": + case "int": + return "int(" + (column.length ? column.length : 11) + ")"; + case "smallint": + return "smallint(" + (column.length ? column.length : 11) + ")"; + case "bigint": + return "bigint(" + (column.length ? column.length : 11) + ")"; + case "float": + return "float"; + case "double": + case "number": + return "double"; + case "decimal": + if (column.precision && column.scale) { + return `decimal(${column.precision},${column.scale})`; + + } else if (column.scale) { + return `decimal(${column.scale})`; + + } else if (column.precision) { + return `decimal(${column.precision})`; + + } else { + return "decimal"; + + } + case "date": + return "date"; + case "time": + return "time"; + case "datetime": + return "datetime"; + case "json": + return "text"; + case "simple_array": + return column.length ? "varchar(" + column.length + ")" : "text"; + } + + throw new DataTypeNotSupportedByDriverError(column.type, "MariaDB"); + } + + // ------------------------------------------------------------------------- + // Protected Methods + // ------------------------------------------------------------------------- + + /** + * Database name shortcut. + */ + protected get dbName(): string { + return this.driver.options.database as string; + } + + /** + * Parametrizes given object of values. Used to create column=value queries. + */ + protected parametrize(objectLiteral: ObjectLiteral): string[] { + return Object.keys(objectLiteral).map(key => this.driver.escapeColumnName(key) + "=?"); + } + + /** + * Builds a query for create column. + */ + protected buildCreateColumnSql(column: ColumnMetadata, skipPrimary: boolean) { + let c = "`" + column.name + "` " + this.normalizeType(column); + if (column.isNullable !== true) + c += " NOT NULL"; + if (column.isPrimary === true && !skipPrimary) + c += " PRIMARY KEY"; + if (column.isGenerated === true) // 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.columnDefinition) + c += " " + column.columnDefinition; + return c; + } + + +} \ No newline at end of file diff --git a/src/driver/mysql/MysqlDriver.ts b/src/driver/mysql/MysqlDriver.ts index 896c7eb420..a7225502c4 100644 --- a/src/driver/mysql/MysqlDriver.ts +++ b/src/driver/mysql/MysqlDriver.ts @@ -8,7 +8,7 @@ import {DriverUtils} from "../DriverUtils"; import {Logger} from "../../logger/Logger"; import {QueryRunner} from "../QueryRunner"; import {MysqlQueryRunner} from "./MysqlQueryRunner"; -import {ColumnTypes} from "../../metadata/types/ColumnTypes"; +import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes"; import * as moment from "moment"; import {ObjectLiteral} from "../../common/ObjectLiteral"; import {ColumnMetadata} from "../../metadata/ColumnMetadata"; @@ -114,7 +114,7 @@ export class MysqlDriver implements Driver { } /** - * Closes connection with database. + * Closes connection with the database. */ disconnect(): Promise { if (!this.databaseConnection && !this.pool) @@ -222,10 +222,21 @@ export class MysqlDriver implements Driver { } /** - * Prepares given value to a value to be persisted, based on its column type and metadata. + * Prepares given value to a value to be persisted, based on its column metadata. */ - prepareHydratedValue(value: any, column: ColumnMetadata): any { - switch (column.type) { + prepareHydratedValue(value: any, type: ColumnType): any; + + /** + * Prepares given value to a value to be persisted, based on its column type. + */ + prepareHydratedValue(value: any, column: ColumnMetadata): any; + + /** + * Prepares given value to a value to be persisted, based on its column type or metadata. + */ + prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any { + const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType; + switch (type) { case ColumnTypes.BOOLEAN: return value ? true : false; diff --git a/src/driver/mysql/MysqlQueryRunner.ts b/src/driver/mysql/MysqlQueryRunner.ts index 2582169956..effceb4493 100644 --- a/src/driver/mysql/MysqlQueryRunner.ts +++ b/src/driver/mysql/MysqlQueryRunner.ts @@ -18,8 +18,6 @@ import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema"; import {IndexSchema} from "../../schema-builder/IndexSchema"; import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError"; -// todo: throw exception if methods are used after release - /** * Runs queries on a single mysql database connection. */ @@ -152,7 +150,7 @@ export class MysqlQueryRunner implements QueryRunner { /** * Insert a new row with given values into given table. */ - async insert(tableName: string, keyValues: ObjectLiteral, idColumnName?: string): Promise { + async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); @@ -162,7 +160,7 @@ export class MysqlQueryRunner implements QueryRunner { const parameters = keys.map(key => keyValues[key]); const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values})`; const result = await this.query(sql, parameters); - return result.insertId; + return idColumn ? result.insertId : undefined; } /** diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 010cd8dd8c..054c3d56ab 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -6,7 +6,7 @@ import {DatabaseConnection} from "../DatabaseConnection"; import {DriverPackageNotInstalledError} from "../error/DriverPackageNotInstalledError"; import {DriverPackageLoadError} from "../error/DriverPackageLoadError"; import {DriverUtils} from "../DriverUtils"; -import {ColumnTypes} from "../../metadata/types/ColumnTypes"; +import {ColumnTypes, ColumnType} from "../../metadata/types/ColumnTypes"; import {ColumnMetadata} from "../../metadata/ColumnMetadata"; import {Logger} from "../../logger/Logger"; import * as moment from "moment"; @@ -194,10 +194,21 @@ export class PostgresDriver implements Driver { } /** - * Prepares given value to a value to be persisted, based on its column type and metadata. + * Prepares given value to a value to be persisted, based on its column metadata. */ - prepareHydratedValue(value: any, column: ColumnMetadata): any { - switch (column.type) { + prepareHydratedValue(value: any, type: ColumnType): any; + + /** + * Prepares given value to a value to be persisted, based on its column type. + */ + prepareHydratedValue(value: any, column: ColumnMetadata): any; + + /** + * Prepares given value to a value to be persisted, based on its column type or metadata. + */ + prepareHydratedValue(value: any, columnOrColumnType: ColumnMetadata|ColumnType): any { + const type = columnOrColumnType instanceof ColumnMetadata ? columnOrColumnType.type : columnOrColumnType; + switch (type) { case ColumnTypes.BOOLEAN: return value ? true : false; diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index a7955d95de..12ba599896 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -143,18 +143,18 @@ export class PostgresQueryRunner implements QueryRunner { /** * Insert a new row into given table. */ - async insert(tableName: string, keyValues: ObjectLiteral, idColumnName?: string): Promise { + async insert(tableName: string, keyValues: ObjectLiteral, idColumn?: ColumnMetadata): Promise { if (this.isReleased) throw new QueryRunnerAlreadyReleasedError(); const keys = Object.keys(keyValues); const columns = keys.map(key => this.driver.escapeColumnName(key)).join(", "); const values = keys.map((key, index) => "$" + (index + 1)).join(","); - const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ idColumnName ? " RETURNING " + this.driver.escapeColumnName(idColumnName) : "" }`; + const sql = `INSERT INTO ${this.driver.escapeTableName(tableName)}(${columns}) VALUES (${values}) ${ idColumn ? " RETURNING " + this.driver.escapeColumnName(idColumn.name) : "" }`; const parameters = keys.map(key => keyValues[key]); const result: ObjectLiteral[] = await this.query(sql, parameters); - if (idColumnName) - return result[0][idColumnName]; + if (idColumn) + return result[0][idColumn.name]; return result; } @@ -344,13 +344,6 @@ export class PostgresQueryRunner implements QueryRunner { if (oldColumn.type !== newType) { sql += ` TYPE ${newType}`; } - /*if (oldColumn.nullable !== newColumn.isNullable) { - if (newColumn.isNullable) { - sql += ` DROP NOT NULL`; - } else { - sql += ` SET NOT NULL`; - } - }*/ if (oldColumn.name !== newColumn.name) { // todo: make rename in a separate query too sql += ` RENAME TO ` + newColumn.name; } diff --git a/src/metadata/RelationMetadata.ts b/src/metadata/RelationMetadata.ts index efed3e57e5..bf31cc80e7 100644 --- a/src/metadata/RelationMetadata.ts +++ b/src/metadata/RelationMetadata.ts @@ -5,6 +5,7 @@ import {OnDeleteType} from "./ForeignKeyMetadata"; import {JoinTableMetadata} from "./JoinTableMetadata"; import {JoinColumnMetadata} from "./JoinColumnMetadata"; import {RelationMetadataArgs} from "../metadata-args/RelationMetadataArgs"; +import {ColumnMetadata} from "./ColumnMetadata"; /** * Function that returns a type of the field. Returned value must be a class used on the relation. @@ -234,7 +235,7 @@ export class RelationMetadata extends PropertyMetadata { if (this.inverseRelation.joinTable) { return this.inverseRelation.joinTable.inverseReferencedColumn.name; } else if (this.inverseRelation.joinColumn) { - return this.inverseRelation.joinColumn.name; + return this.inverseRelation.joinColumn.name; // todo: didn't get this logic here } } @@ -242,6 +243,30 @@ export class RelationMetadata extends PropertyMetadata { throw new Error(`Cannot get referenced column name of the relation ${this.entityMetadata.name}#${this.name}`); } + /** + * Gets the column to which this relation is referenced. + */ + get referencedColumn(): ColumnMetadata { + if (this.isOwning) { + if (this.joinTable) { + return this.joinTable.referencedColumn; + + } else if (this.joinColumn) { + return this.joinColumn.referencedColumn; + } + + } else if (this.hasInverseSide) { + if (this.inverseRelation.joinTable) { + return this.inverseRelation.joinTable.inverseReferencedColumn; + } else if (this.inverseRelation.joinColumn) { + return this.inverseRelation.joinColumn.referencedColumn; + } + } + + // this should not be possible, but anyway throw error + throw new Error(`Cannot get referenced column of the relation ${this.entityMetadata.name}#${this.name}`); + } + /** * Gets the property's type to which this relation is applied. */ diff --git a/src/metadata/TableMetadata.ts b/src/metadata/TableMetadata.ts index 10ea675fe9..5c7dabb7a9 100644 --- a/src/metadata/TableMetadata.ts +++ b/src/metadata/TableMetadata.ts @@ -7,6 +7,8 @@ import {TableMetadataArgs} from "../metadata-args/TableMetadataArgs"; */ export type TableType = "regular"|"abstract"|"junction"|"closure"|"closureJunction"|"embeddable"; +// todo: make table engine to be specified within @Table decorator + /** * This metadata interface contains all information about specific table. */ diff --git a/src/metadata/types/ColumnTypes.ts b/src/metadata/types/ColumnTypes.ts index d36310eada..68ea43dba7 100644 --- a/src/metadata/types/ColumnTypes.ts +++ b/src/metadata/types/ColumnTypes.ts @@ -155,4 +155,17 @@ export class ColumnTypes { return (type as any).name.toLowerCase(); } + /** + * Checks if column type is numeric. + */ + static isNumeric(type: ColumnType) { + return type === ColumnTypes.NUMBER || + type === ColumnTypes.INT || + type === ColumnTypes.INTEGER || + type === ColumnTypes.BIGINT || + type === ColumnTypes.SMALLINT || + type === ColumnTypes.DOUBLE || + type === ColumnTypes.FLOAT; + } + } \ No newline at end of file diff --git a/src/persistment/PersistOperationExecutor.ts b/src/persistment/PersistOperationExecutor.ts index 3682c7077a..d755165b6d 100644 --- a/src/persistment/PersistOperationExecutor.ts +++ b/src/persistment/PersistOperationExecutor.ts @@ -479,12 +479,8 @@ export class PersistOperationExecutor { }*/ // console.log("inserting: ", this.zipObject(allColumns, allValues)); - let idColumnName: string|undefined; - if (metadata.hasPrimaryColumn && metadata.primaryColumn.isGenerated) { - idColumnName = metadata.primaryColumn.name; - } - - return this.queryRunner.insert(metadata.table.name, this.zipObject(allColumns, allValues), idColumnName); + let idColumn = metadata.hasPrimaryColumn && metadata.primaryColumn.isGenerated ? metadata.primaryColumn : undefined; + return this.queryRunner.insert(metadata.table.name, this.zipObject(allColumns, allValues), idColumn); } private insertIntoClosureTable(operation: InsertOperation, updateMap: ObjectLiteral) { diff --git a/src/query-builder/QueryBuilder.ts b/src/query-builder/QueryBuilder.ts index 1ca61cbd07..71c1e3432f 100644 --- a/src/query-builder/QueryBuilder.ts +++ b/src/query-builder/QueryBuilder.ts @@ -622,7 +622,7 @@ export class QueryBuilder { throw new Error(`No ids found to load relation counters`); return queryBuilder - .select(`${parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName} as id`) + .select(`${parentMetadata.name + "." + parentMetadata.primaryColumn.propertyName} AS id`) .addSelect(`COUNT(${ this.driver.escapeAliasName(relation.propertyName) + "." + this.driver.escapeColumnName(relation.inverseEntityMetadata.primaryColumn.name) }) as cnt`) .from(parentMetadata.target, parentMetadata.name) .leftJoin(parentMetadata.name + "." + relation.propertyName, relation.propertyName, relationCountMeta.conditionType, relationCountMeta.condition) @@ -634,7 +634,9 @@ export class QueryBuilder { // console.log(relationCountMeta.entities); relationCountMeta.entities.forEach(entityWithMetadata => { const entityId = entityWithMetadata.entity[entityWithMetadata.metadata.primaryColumn.propertyName]; - const entityResult = results.find(result => result.id === entityId); + const entityResult = results.find(result => { + return entityId === this.driver.prepareHydratedValue(result.id, entityWithMetadata.metadata.primaryColumn); + }); if (entityResult) { if (relationCountMeta.mapToProperty) { diff --git a/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts b/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts index 5374104326..0377689d16 100644 --- a/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts +++ b/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts @@ -4,6 +4,7 @@ import {EntityMetadata} from "../../metadata/EntityMetadata"; import {OrmUtils} from "../../util/OrmUtils"; import {Driver} from "../../driver/Driver"; import {JoinMapping, RelationCountMeta} from "../QueryBuilder"; +import {ColumnTypes} from "../../metadata/types/ColumnTypes"; /** * Transforms raw sql results returned from the database into entity object. @@ -136,8 +137,9 @@ export class RawSqlResultsToEntityTransformer { rawSqlResults.forEach(results => { if (relationAlias) { const resultsKey = relationAlias.name + "_" + columnName; - if (results[resultsKey]) - ids.push(results[resultsKey]); + const value = this.driver.prepareHydratedValue(results[resultsKey], relation.referencedColumn); + if (value !== undefined && value !== null) + ids.push(value); } }); @@ -147,7 +149,7 @@ export class RawSqlResultsToEntityTransformer { } } else if (relation.idField) { const relationName = relation.name; - entity[relation.idField] = rawSqlResults[0][alias.name + "_" + relationName]; + entity[relation.idField] = this.driver.prepareHydratedValue(rawSqlResults[0][alias.name + "_" + relationName], relation.referencedColumn); } // if relation counter diff --git a/test/functional/decorators/relation-id/relation-id.ts b/test/functional/decorators/relation-id/relation-id.ts index 7eddb10717..771edc4d6b 100644 --- a/test/functional/decorators/relation-id/relation-id.ts +++ b/test/functional/decorators/relation-id/relation-id.ts @@ -63,6 +63,7 @@ describe("QueryBuilder > relation-id", () => { .leftJoinRelationId("post.categories") .where("post.id = :id", { id: post.id }) .getSingleResult(); + expect(loadedPost.tagId).to.not.be.empty; expect(loadedPost.tagId).to.be.equal(1); expect(loadedPost.categoryIds).to.not.be.empty; diff --git a/test/utils/test-utils.ts b/test/utils/test-utils.ts index c19a45bbdd..5eefbfc881 100644 --- a/test/utils/test-utils.ts +++ b/test/utils/test-utils.ts @@ -12,6 +12,7 @@ export interface TestingConnectionOptions { schemaCreate?: boolean; reloadAndCreateSchema?: boolean; skipMysql?: boolean; + skipMariadb?: boolean; skipPostgres?: boolean; } @@ -19,11 +20,19 @@ export function closeConnections(connections: Connection[]) { return Promise.all(connections.map(connection => connection.isConnected ? connection.close() : undefined)); } -export function createTestingConnectionOptions(type: "mysql"|"mysqlSecondary"|"postgres"|"postgresSecondary"): DriverOptions { +export function createTestingConnectionOptions(type: "mysql"|"mysqlSecondary"|"mariadb"|"mariadbSecondary"|"postgres"|"postgresSecondary"): DriverOptions { const parameters = require(__dirname + "/../../../../config/parameters.json"); // path is relative to compile directory // const parameters = require(__dirname + "/../../config/parameters.json"); - const driverType: "mysql"|"postgres" = type === "mysql" || type === "mysqlSecondary" ? "mysql" : "postgres"; + let driverType: "mysql"|"mariadb"|"postgres" = "mysql"; // = type === "mysql" || type === "mysqlSecondary" ? "mysql" : "postgres"; + if (type === "mysql" || type === "mysqlSecondary") { + driverType = "mysql"; + } else if (type === "mariadb" || type === "mariadbSecondary") { + driverType = "mariadb"; + } else if (type === "postgres" || type === "postgresSecondary") { + driverType = "postgres"; + } + return { type: driverType, host: parameters.connections[type].host, @@ -67,6 +76,34 @@ export async function setupTestingConnections(options?: TestingConnectionOptions }, }; + const mariadbParameters: ConnectionOptions = { + name: "mariadbPrimaryConnection", + driver: createTestingConnectionOptions("mariadb"), + autoSchemaCreate: options && options.entities ? options.schemaCreate : false, + entities: options && options.entities ? options.entities : [], + entitySchemas: options && options.entitySchemas ? options.entitySchemas : [], + entityDirectories: options && options.entityDirectories ? options.entityDirectories : [], + logging: { + // logQueries: true, // uncomment for debugging + logOnlyFailedQueries: true, + logFailedQueryError: true + }, + }; + + const mariadbSecondaryParameters: ConnectionOptions = { + name: "mariadbSecondaryConnection", + driver: createTestingConnectionOptions("mariadbSecondary"), + autoSchemaCreate: options && options.entities ? options.schemaCreate : false, + entities: options && options.entities ? options.entities : [], + entitySchemas: options && options.entitySchemas ? options.entitySchemas : [], + entityDirectories: options && options.entityDirectories ? options.entityDirectories : [], + logging: { + // logQueries: true, // uncomment for debugging + logOnlyFailedQueries: true, + logFailedQueryError: true + }, + }; + const postgresParameters: ConnectionOptions = { name: "postgresPrimaryConnection", driver: createTestingConnectionOptions("postgres"), @@ -96,15 +133,20 @@ export async function setupTestingConnections(options?: TestingConnectionOptions }; const mysql = !options || !options.skipMysql; + const mariadb = !options || !options.skipMariadb; const postgres = !options || !options.skipPostgres; const allParameters: ConnectionOptions[] = []; if (mysql) allParameters.push(mysqlParameters); + if (mariadb) + allParameters.push(mariadbParameters); if (postgres) allParameters.push(postgresParameters); if (mysql && options && options.secondaryConnections) allParameters.push(mysqlSecondaryParameters); + if (mariadb && options && options.secondaryConnections) + allParameters.push(mariadbSecondaryParameters); if (postgres && options && options.secondaryConnections) allParameters.push(postgresSecondaryParameters);