From 3180d13b1be19128ce941da7f4a254cf2a12f5de Mon Sep 17 00:00:00 2001 From: Umed Khudoiberdiev Date: Mon, 29 Aug 2016 11:30:33 +0500 Subject: [PATCH] refactored database classes --- src/driver/DatabaseConnection.ts | 4 +- src/driver/QueryRunner.ts | 46 ++++++ src/driver/error/ConnectionIsNotSetError.ts | 2 + .../DataTypeNotSupportedByDriverError.ts | 2 + src/driver/error/DriverOptionNotSetError.ts | 2 + src/driver/error/DriverPackageLoadError.ts | 2 + .../error/DriverPackageNotInstalledError.ts | 2 + .../error/QueryRunnerAlreadyReleasedError.ts | 12 ++ .../error/TransactionAlreadyStartedError.ts | 2 + .../error/TransactionNotStartedError.ts | 2 + src/driver/mysql/MysqlDriver.ts | 2 +- src/driver/mysql/MysqlQueryRunner.ts | 148 +++++++++++++++--- src/driver/postgres/PostgresDriver.ts | 4 +- src/driver/postgres/PostgresQueryRunner.ts | 118 +++++++++++++- 14 files changed, 316 insertions(+), 32 deletions(-) create mode 100644 src/driver/error/QueryRunnerAlreadyReleasedError.ts diff --git a/src/driver/DatabaseConnection.ts b/src/driver/DatabaseConnection.ts index ad2293ec53..482d7929e6 100644 --- a/src/driver/DatabaseConnection.ts +++ b/src/driver/DatabaseConnection.ts @@ -6,12 +6,12 @@ export interface DatabaseConnection { /** * Id of the connection. */ - id: number; + readonly id: number; /** * Native driver's connection. */ - connection: any; + readonly connection: any; /** * Indicates if transaction is active for this connection. diff --git a/src/driver/QueryRunner.ts b/src/driver/QueryRunner.ts index 139a64afef..7d46df871a 100644 --- a/src/driver/QueryRunner.ts +++ b/src/driver/QueryRunner.ts @@ -5,6 +5,9 @@ import {ColumnMetadata} from "../metadata/ColumnMetadata"; import {TableMetadata} from "../metadata/TableMetadata"; import {TableSchema} from "../schema-builder/TableSchema"; +/** + * Runs queries on a single database connection. + */ export interface QueryRunner { /** @@ -64,16 +67,59 @@ export interface QueryRunner { */ insertIntoClosureTable(tableName: string, newEntityId: any, parentId: any, hasLevel: boolean): Promise; + /** + * Loads all tables (with given names) from the database and creates a TableSchema from them. + */ loadSchemaTables(tableNames: string[]): Promise; + + /** + * Creates a new table from the given table metadata and column metadatas. + */ createTable(table: TableMetadata, columns: ColumnMetadata[]): Promise; + + /** + * Creates a new column from the column metadata in the table. + */ createColumn(tableName: string, column: ColumnMetadata): Promise; + + /** + * Changes a column in the table. + */ changeColumn(tableName: string, oldColumn: ColumnSchema, newColumn: ColumnMetadata): Promise; + + /** + * Drops the column in the table. + */ dropColumn(tableName: string, columnName: string): Promise; + + /** + * Creates a new foreign. + */ createForeignKey(foreignKey: ForeignKeyMetadata): Promise; + + /** + * Drops a foreign key from the table. + */ dropForeignKey(tableName: string, foreignKeyName: string): Promise; + + /** + * Creates a new index. + */ createIndex(tableName: string, index: IndexMetadata): Promise; + + /** + * Drops an index from the table. + */ dropIndex(tableName: string, indexName: string): Promise; + + /** + * Creates a new unique key. + */ createUniqueKey(tableName: string, columnName: string, keyName: string): Promise; + + /** + * Creates a database type from a given column metadata. + */ normalizeType(column: ColumnMetadata): any; } \ No newline at end of file diff --git a/src/driver/error/ConnectionIsNotSetError.ts b/src/driver/error/ConnectionIsNotSetError.ts index da74be2c80..264232d3a4 100644 --- a/src/driver/error/ConnectionIsNotSetError.ts +++ b/src/driver/error/ConnectionIsNotSetError.ts @@ -1,4 +1,6 @@ /** + * Thrown when user tries to execute operation that requires connection to be established. + * * @internal */ export class ConnectionIsNotSetError extends Error { diff --git a/src/driver/error/DataTypeNotSupportedByDriverError.ts b/src/driver/error/DataTypeNotSupportedByDriverError.ts index 8723a8ec8c..4c7886666c 100644 --- a/src/driver/error/DataTypeNotSupportedByDriverError.ts +++ b/src/driver/error/DataTypeNotSupportedByDriverError.ts @@ -1,4 +1,6 @@ /** + * Thrown if some data type is not supported by a driver. + * * @internal */ export class DataTypeNotSupportedByDriverError extends Error { diff --git a/src/driver/error/DriverOptionNotSetError.ts b/src/driver/error/DriverOptionNotSetError.ts index e4ba8ddd04..c0f11baf72 100644 --- a/src/driver/error/DriverOptionNotSetError.ts +++ b/src/driver/error/DriverOptionNotSetError.ts @@ -1,4 +1,6 @@ /** + * Thrown if some required driver's option is not set. + * * @internal */ export class DriverOptionNotSetError extends Error { diff --git a/src/driver/error/DriverPackageLoadError.ts b/src/driver/error/DriverPackageLoadError.ts index 705f75cf48..86da63fdb4 100644 --- a/src/driver/error/DriverPackageLoadError.ts +++ b/src/driver/error/DriverPackageLoadError.ts @@ -1,4 +1,6 @@ /** + * Thrown when some unexpected error occur on driver packages load. + * * @internal */ export class DriverPackageLoadError extends Error { diff --git a/src/driver/error/DriverPackageNotInstalledError.ts b/src/driver/error/DriverPackageNotInstalledError.ts index 3fa2273699..42f88b62d1 100644 --- a/src/driver/error/DriverPackageNotInstalledError.ts +++ b/src/driver/error/DriverPackageNotInstalledError.ts @@ -1,4 +1,6 @@ /** + * Thrown when required driver's package is not installed. + * * @internal */ export class DriverPackageNotInstalledError extends Error { diff --git a/src/driver/error/QueryRunnerAlreadyReleasedError.ts b/src/driver/error/QueryRunnerAlreadyReleasedError.ts new file mode 100644 index 0000000000..38794f2c30 --- /dev/null +++ b/src/driver/error/QueryRunnerAlreadyReleasedError.ts @@ -0,0 +1,12 @@ +/** + * @internal + */ +export class QueryRunnerAlreadyReleasedError extends Error { + name = "QueryRunnerAlreadyReleasedError"; + + constructor() { + super(); + this.message = `Query runner already released. Cannot run queries anymore.`; + } + +} \ No newline at end of file diff --git a/src/driver/error/TransactionAlreadyStartedError.ts b/src/driver/error/TransactionAlreadyStartedError.ts index ad77681918..505e8e963d 100644 --- a/src/driver/error/TransactionAlreadyStartedError.ts +++ b/src/driver/error/TransactionAlreadyStartedError.ts @@ -1,4 +1,6 @@ /** + * Thrown when transaction is already started and user tries to run it again. + * * @internal */ export class TransactionAlreadyStartedError extends Error { diff --git a/src/driver/error/TransactionNotStartedError.ts b/src/driver/error/TransactionNotStartedError.ts index 0094b7d469..f95fc3d818 100644 --- a/src/driver/error/TransactionNotStartedError.ts +++ b/src/driver/error/TransactionNotStartedError.ts @@ -1,4 +1,6 @@ /** + * Thrown when transaction is not started yet and user tries to run commit or rollback. + * * @internal */ export class TransactionNotStartedError extends Error { diff --git a/src/driver/mysql/MysqlDriver.ts b/src/driver/mysql/MysqlDriver.ts index 16334e65de..896c7eb420 100644 --- a/src/driver/mysql/MysqlDriver.ts +++ b/src/driver/mysql/MysqlDriver.ts @@ -14,7 +14,7 @@ import {ObjectLiteral} from "../../common/ObjectLiteral"; import {ColumnMetadata} from "../../metadata/ColumnMetadata"; /** - * This driver organizes work with mysql database. + * Organizes communication with MySQL DBMS. */ export class MysqlDriver implements Driver { diff --git a/src/driver/mysql/MysqlQueryRunner.ts b/src/driver/mysql/MysqlQueryRunner.ts index fd76c466a6..2582169956 100644 --- a/src/driver/mysql/MysqlQueryRunner.ts +++ b/src/driver/mysql/MysqlQueryRunner.ts @@ -3,10 +3,7 @@ import {DatabaseConnection} from "../DatabaseConnection"; import {ObjectLiteral} from "../../common/ObjectLiteral"; import {TransactionAlreadyStartedError} from "../error/TransactionAlreadyStartedError"; import {TransactionNotStartedError} from "../error/TransactionNotStartedError"; -import {ColumnTypes} from "../../metadata/types/ColumnTypes"; -import * as moment from "moment"; import {Logger} from "../../logger/Logger"; -import {Driver} from "../Driver"; import {MysqlDriver} from "./MysqlDriver"; import {DataTypeNotSupportedByDriverError} from "../error/DataTypeNotSupportedByDriverError"; import {IndexMetadata} from "../../metadata/IndexMetadata"; @@ -19,11 +16,25 @@ 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"; // todo: throw exception if methods are used after release +/** + * Runs queries on a single mysql database connection. + */ export class MysqlQueryRunner 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 // ------------------------------------------------------------------------- @@ -44,6 +55,7 @@ export class MysqlQueryRunner implements QueryRunner { */ release(): Promise { if (this.databaseConnection.releaseCallback) { + this.isReleased = true; return this.databaseConnection.releaseCallback(); } @@ -54,6 +66,8 @@ export class MysqlQueryRunner implements QueryRunner { * 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}'`; @@ -69,6 +83,9 @@ export class MysqlQueryRunner implements QueryRunner { * Starts transaction. */ async beginTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + if (this.databaseConnection.isTransactionActive) throw new TransactionAlreadyStartedError(); @@ -80,6 +97,9 @@ export class MysqlQueryRunner implements QueryRunner { * Commits transaction. */ async commitTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + if (!this.databaseConnection.isTransactionActive) throw new TransactionNotStartedError(); @@ -91,6 +111,9 @@ export class MysqlQueryRunner implements QueryRunner { * Rollbacks transaction. */ async rollbackTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + if (!this.databaseConnection.isTransactionActive) throw new TransactionNotStartedError(); @@ -109,6 +132,9 @@ export class MysqlQueryRunner implements QueryRunner { * Executes a given SQL query. */ query(query: string, parameters?: any[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + this.logger.logQuery(query); return new Promise((ok, fail) => { this.databaseConnection.connection.query(query, parameters, (err: any, result: any) => { @@ -127,6 +153,9 @@ export class MysqlQueryRunner implements QueryRunner { * Insert a new row with given values into given table. */ async insert(tableName: string, keyValues: ObjectLiteral, idColumnName?: string): 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(","); @@ -140,6 +169,9 @@ export class MysqlQueryRunner implements QueryRunner { * 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) : ""}`; @@ -153,6 +185,9 @@ export class MysqlQueryRunner implements QueryRunner { * 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]); @@ -163,22 +198,30 @@ export class MysqlQueryRunner implements QueryRunner { * 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`; + 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}`; + 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) @@ -259,58 +302,115 @@ export class MysqlQueryRunner implements QueryRunner { }); } + /** + * 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})`; + 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 { - const sql = `ALTER TABLE \`${tableName}\` DROP FOREIGN KEY \`${foreignKeyName}\``; - await this.query(sql); - } + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); - async dropIndex(tableName: string, indexName: string): Promise { - const sql = `ALTER TABLE \`${tableName}\` DROP INDEX \`${indexName}\``; + 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": @@ -364,6 +464,13 @@ export class MysqlQueryRunner implements QueryRunner { // 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. */ @@ -371,10 +478,9 @@ export class MysqlQueryRunner implements QueryRunner { return Object.keys(objectLiteral).map(key => this.driver.escapeColumnName(key) + "=?"); } - protected get dbName(): string { - return this.driver.options.database as string; - } - + /** + * Builds a query for create column. + */ protected buildCreateColumnSql(column: ColumnMetadata, skipPrimary: boolean) { let c = "`" + column.name + "` " + this.normalizeType(column); if (column.isNullable !== true) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 67c3155329..010cd8dd8c 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -9,8 +9,6 @@ import {DriverUtils} from "../DriverUtils"; import {ColumnTypes} from "../../metadata/types/ColumnTypes"; import {ColumnMetadata} from "../../metadata/ColumnMetadata"; import {Logger} from "../../logger/Logger"; -import {TransactionAlreadyStartedError} from "../error/TransactionAlreadyStartedError"; -import {TransactionNotStartedError} from "../error/TransactionNotStartedError"; import * as moment from "moment"; import {PostgresQueryRunner} from "./PostgresQueryRunner"; import {QueryRunner} from "../QueryRunner"; @@ -21,7 +19,7 @@ import {QueryRunner} from "../QueryRunner"; // /** - * This driver organizes work with postgres database. + * Organizes communication with PostgreSQL DBMS. */ export class PostgresDriver implements Driver { diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index f794595188..a7955d95de 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -16,9 +16,23 @@ import {IndexSchema} from "../../schema-builder/IndexSchema"; import {ForeignKeySchema} from "../../schema-builder/ForeignKeySchema"; import {PrimaryKeySchema} from "../../schema-builder/PrimaryKeySchema"; import {UniqueKeySchema} from "../../schema-builder/UniqueKeySchema"; +import {QueryRunnerAlreadyReleasedError} from "../error/QueryRunnerAlreadyReleasedError"; +/** + * Runs queries on a single mysql database connection. + */ export class PostgresQueryRunner 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 // ------------------------------------------------------------------------- @@ -38,6 +52,7 @@ export class PostgresQueryRunner implements QueryRunner { */ release(): Promise { if (this.databaseConnection.releaseCallback) { + this.isReleased = true; return this.databaseConnection.releaseCallback(); } @@ -48,6 +63,9 @@ export class PostgresQueryRunner implements QueryRunner { * Removes all tables from the currently connected database. */ async clearDatabase(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + const selectDropsQuery = `SELECT 'DROP TABLE IF EXISTS "' || tablename || '" CASCADE;' as query FROM pg_tables WHERE schemaname = 'public'`; const dropQueries: ObjectLiteral[] = await this.query(selectDropsQuery); await Promise.all(dropQueries.map(q => this.query(q["query"]))); @@ -57,6 +75,8 @@ export class PostgresQueryRunner implements QueryRunner { * Starts transaction. */ async beginTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); if (this.databaseConnection.isTransactionActive) throw new TransactionAlreadyStartedError(); @@ -68,6 +88,8 @@ export class PostgresQueryRunner implements QueryRunner { * Commits transaction. */ async commitTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); if (!this.databaseConnection.isTransactionActive) throw new TransactionNotStartedError(); @@ -79,6 +101,8 @@ export class PostgresQueryRunner implements QueryRunner { * Rollbacks transaction. */ async rollbackTransaction(): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); if (!this.databaseConnection.isTransactionActive) throw new TransactionNotStartedError(); @@ -97,6 +121,9 @@ export class PostgresQueryRunner implements QueryRunner { * Executes a given SQL query. */ query(query: string, parameters?: any[]): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + // console.log("query: ", query); // console.log("parameters: ", parameters); this.logger.logQuery(query); @@ -117,6 +144,9 @@ export class PostgresQueryRunner implements QueryRunner { * Insert a new row into given table. */ async insert(tableName: string, keyValues: ObjectLiteral, idColumnName?: string): 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(","); @@ -133,6 +163,9 @@ export class PostgresQueryRunner implements QueryRunner { * 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, Object.keys(valuesMap).length).join(" AND "); const query = `UPDATE ${this.driver.escapeTableName(tableName)} SET ${updateValues} ${conditionString ? (" WHERE " + conditionString) : ""}`; @@ -146,6 +179,9 @@ export class PostgresQueryRunner implements QueryRunner { * 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 parameters = Object.keys(conditions).map(key => conditions[key]); const query = `DELETE FROM "${tableName}" WHERE ${conditionString}`; @@ -156,6 +192,9 @@ export class PostgresQueryRunner implements QueryRunner { * Inserts rows into 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) ` + @@ -171,7 +210,13 @@ export class PostgresQueryRunner implements QueryRunner { 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) @@ -260,18 +305,36 @@ export class PostgresQueryRunner implements QueryRunner { }); } + /** + * 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})`; 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(); + // update name, type, nullable const newType = this.normalizeType(newColumn); if (oldColumn.type !== newType || @@ -320,12 +383,24 @@ export class PostgresQueryRunner implements QueryRunner { } } + /** + * 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(); + let sql = `ALTER TABLE "${foreignKey.tableName}" ADD CONSTRAINT "${foreignKey.name}" ` + `FOREIGN KEY ("${foreignKey.columnNames.join("\", \"")}") ` + `REFERENCES "${foreignKey.referencedTable.name}"("${foreignKey.referencedColumnNames.join("\", \"")}")`; @@ -334,12 +409,35 @@ export class PostgresQueryRunner implements QueryRunner { 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 CONSTRAINT "${foreignKeyName}"`; await this.query(sql); } + /** + * Creates a new index. + */ + async createIndex(tableName: string, index: IndexMetadata): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + + const sql = `CREATE ${index.isUnique ? "UNIQUE" : ""} INDEX "${index.name}" ON "${tableName}"("${index.columns.join("\", \"")}")`; + await this.query(sql); + } + + /** + * Drops an index from the table. + */ async dropIndex(tableName: string, indexName: string, isGenerated: boolean = false): Promise { + if (this.isReleased) + throw new QueryRunnerAlreadyReleasedError(); + if (isGenerated) { await this.query(`ALTER SEQUENCE "${tableName}_id_seq" OWNED BY NONE`); // todo: what is it? } @@ -348,16 +446,20 @@ export class PostgresQueryRunner implements QueryRunner { await this.query(sql); } - async createIndex(tableName: string, index: IndexMetadata): Promise { - const sql = `CREATE ${index.isUnique ? "UNIQUE" : ""} INDEX "${index.name}" ON "${tableName}"("${index.columns.join("\", \"")}")`; - 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": @@ -419,6 +521,9 @@ export class PostgresQueryRunner implements QueryRunner { // Protected Methods // ------------------------------------------------------------------------- + /** + * Database name shortcut. + */ protected get dbName(): string { return this.driver.options.database as string; } @@ -430,6 +535,9 @@ export class PostgresQueryRunner implements QueryRunner { return Object.keys(objectLiteral).map((key, index) => this.driver.escapeColumnName(key) + "=$" + (startIndex + index + 1)); } + /** + * Builds a query for create column. + */ protected buildCreateColumnSql(column: ColumnMetadata, skipPrimary: boolean) { let c = "\"" + column.name + "\""; if (column.isGenerated === true) // don't use skipPrimary here since updates can update already exist primary without auto inc.