From e9acb71822e86a262adeeba45350f9a66287c9dd Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 17:50:11 +0200 Subject: [PATCH 01/24] feat: implement generated columns for postgres 12 driver The implementation has the potential to make full text search much faster when using postgres. You can simply pre-generate all tsvector's --- src/driver/postgres/PostgresQueryRunner.ts | 103 ++++++++++++++++++--- src/schema-builder/RdbmsSchemaBuilder.ts | 32 +++++++ 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index ae94bd8bb7..cf6f84ef3e 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -344,6 +344,18 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner return Promise.resolve(); })); + // if table have column with generated type, we must add the expression to the meta table + await Promise.all(table.columns + .filter(column => column.generatedType === "STORED" && column.asExpression) + .map(async column => { + const tableName = await this.getTableNameWithSchema(table.name); + const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + upQueries.push(deleteQuery); + upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + downQueries.push(deleteQuery); + return Promise.resolve(); + })); + upQueries.push(this.createTableSql(table, createForeignKeys)); downQueries.push(this.dropTableSql(table)); @@ -563,6 +575,14 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP CONSTRAINT "${uniqueConstraint.name}"`)); } + if (column.generatedType === "STORED" && column.asExpression) { + const tableName = await this.getTableNameWithSchema(table.name); + const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + upQueries.push(deleteQuery); + upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + downQueries.push(deleteQuery); + } + await this.executeQueries(upQueries, downQueries); clonedTable.addColumn(column); @@ -613,7 +633,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner if (!oldColumn) throw new Error(`Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`); - if (oldColumn.type !== newColumn.type || oldColumn.length !== newColumn.length) { + if (oldColumn.type !== newColumn.type + || oldColumn.length !== newColumn.length + || (!oldColumn.generatedType && newColumn.generatedType === "STORED") + || (oldColumn.asExpression !== newColumn.asExpression && newColumn.generatedType === "STORED")) { // To avoid data conversion, we just recreate column await this.dropColumn(table, oldColumn); await this.addColumn(table, newColumn); @@ -883,6 +906,24 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`)); } + if (newColumn.generatedType !== oldColumn.generatedType) { + // Convert generated column data to normal column + if (!newColumn.generatedType || newColumn.generatedType === "VIRTUAL") { + // We can copy the generated data to the new column + const tableName = await this.getTableNameWithSchema(table.name); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} RENAME COLUMN "${oldColumn.name}" TO "TEMP_OLD_${oldColumn.name}"`)); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, newColumn)}`)); + upQueries.push(new Query(`UPDATE ${this.escapePath(table)} SET "${newColumn.name}" = "TEMP_OLD_${oldColumn.name}"`)); + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "TEMP_OLD_${oldColumn.name}"`)); + upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, oldColumn.name])); + // However, we can't copy it back on downgrade. It needs to regenerate. + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "${newColumn.name}"`)); + downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, oldColumn)}`)); + downQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, newColumn.name])); + downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, oldColumn.name, oldColumn.asExpression])); + } + } + } await this.executeQueries(upQueries, downQueries); @@ -969,6 +1010,12 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } + if (column.generatedType === "STORED") { + const tableName = await this.getTableNameWithSchema(table.name); + upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name])); + downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + } + await this.executeQueries(upQueries, downQueries); clonedTable.removeColumn(column); @@ -1592,6 +1639,19 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } + if (dbColumn["is_generated"] === "ALWAYS" && dbColumn["generation_expression"]) { + // In postgres there is no VIRTUAL generated column type + tableColumn.generatedType = "STORED"; + // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. + const asExpressionQuery = `SELECT * FROM typeorm_generation_meta WHERE table_name = '${await this.getTableNameWithSchema(tableFullName)}' and column_name = '${tableColumn.name}'`; + const results: ObjectLiteral[] = await this.query(asExpressionQuery); + if (results[0] && results[0].generation_expression) { + tableColumn.asExpression = results[0].generation_expression; + } else { + tableColumn.asExpression = ""; + } + } + tableColumn.comment = ""; // dbColumn["COLUMN_COMMENT"]; if (dbColumn["character_set_name"]) tableColumn.charset = dbColumn["character_set_name"]; @@ -2094,6 +2154,21 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } + /** + * Get the table name with table schema + * Note: Without ' or " + */ + protected async getTableNameWithSchema(target: Table|string) { + const tableName = target instanceof Table ? target.name : target; + if (tableName.indexOf(".") === -1) { + const schemaResult = await this.query(`SELECT current_schema()`); + const schema = schemaResult[0]["current_schema"]; + return `${schema}.${tableName}`; + } else { + return `${tableName.split(".")[0]}.${tableName.split(".")[1]}`; + } + } + /** * Builds a query for create column. */ @@ -2115,16 +2190,22 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } else if (!column.isGenerated || column.type === "uuid") { c += " " + this.connection.driver.createFullType(column); } - if (column.charset) - c += " CHARACTER SET \"" + column.charset + "\""; - if (column.collation) - c += " COLLATE \"" + column.collation + "\""; - if (column.isNullable !== true) - c += " NOT NULL"; - if (column.default !== undefined && column.default !== null) - c += " DEFAULT " + column.default; - if (column.isGenerated && column.generationStrategy === "uuid" && !column.default) - c += ` DEFAULT ${this.driver.uuidGenerator}`; + // CHARACTER SET, COLLATE, NOT NULL and DEFAULT do not exist on generated (virtual) columns + // Also, postgres only supports the stored generated column type + if (column.generatedType === "STORED" && column.asExpression) { + c += ` GENERATED ALWAYS AS (${column.asExpression}) STORED`; + } else { + if (column.charset) + c += " CHARACTER SET \"" + column.charset + "\""; + if (column.collation) + c += " COLLATE \"" + column.collation + "\""; + if (column.isNullable !== true) + c += " NOT NULL"; + if (column.default !== undefined && column.default !== null) + c += " DEFAULT " + column.default; + if (column.isGenerated && column.generationStrategy === "uuid" && !column.default) + c += ` DEFAULT ${this.driver.uuidGenerator}`; + } return c; } diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index 7251efbfc6..d7c9b1e530 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -75,6 +75,8 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { // Remove condition or add new conditions if necessary (for CHECK constraints for example). if (this.viewEntityToSyncMetadatas.length > 0) await this.createTypeormMetadataTable(); + if (this.connection.driver instanceof PostgresDriver) + await this.createTypeormGeneratedMetadataTable(); await this.queryRunner.getTables(tablePaths); await this.queryRunner.getViews([]); await this.executeSchemaSyncOperationsInProperOrder(); @@ -786,6 +788,36 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { ), true); } + /** + * Creates typeorm service table for storing user defined generated columns as expressions. + */ + protected async createTypeormGeneratedMetadataTable() { + const options = this.connection.driver.options; + const typeormMetadataTable = this.connection.driver.buildTableName("typeorm_generation_meta", options.schema, options.database); + + await this.queryRunner.createTable(new Table( + { + name: typeormMetadataTable, + columns: [ + { + name: "table_name", + type: this.connection.driver.normalizeType({type: this.connection.driver.mappedDataTypes.metadataType}), + isNullable: false, + }, + { + name: "column_name", + type: this.connection.driver.normalizeType({type: this.connection.driver.mappedDataTypes.metadataDatabase}), + isNullable: false + }, + { + name: "generation_expression", + type: this.connection.driver.normalizeType({type: this.connection.driver.mappedDataTypes.metadataValue}), + isNullable: true + }, + ] + }, + ), true); + } } function foreignKeysMatch( From e9ba0c0a02fdec0a5bf08e19ab7facb7da64081d Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 17:50:59 +0200 Subject: [PATCH 02/24] test: add tests for generated columns in postgres 12 --- test/functional/query-runner/add-column.ts | 34 +++++++++++++++ test/functional/query-runner/change-column.ts | 42 +++++++++++++++++++ test/functional/query-runner/entity/Post.ts | 2 + 3 files changed, 78 insertions(+) diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 36f4d63637..f3d747da8c 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -6,6 +6,7 @@ import {MysqlDriver} from "../../../src/driver/mysql/MysqlDriver"; import {AbstractSqliteDriver} from "../../../src/driver/sqlite-abstract/AbstractSqliteDriver"; import {TableColumn} from "../../../src/schema-builder/table/TableColumn"; import {closeTestingConnections, createTestingConnections} from "../../utils/test-utils"; +import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver"; describe("query runner > add column", () => { @@ -48,6 +49,22 @@ describe("query runner > add column", () => { default: "'this is description'" }); + let column3 = new TableColumn({ + name: "textAndTag", + type: "varchar", + length: "200", + generatedType: "STORED", + asExpression: "text || tag" + }); + + let column4 = new TableColumn({ + name: "textAndTag2", + type: "varchar", + length: "200", + generatedType: "VIRTUAL", + asExpression: "text || tag" + }); + await queryRunner.addColumn(table!, column1); await queryRunner.addColumn("post", column2); @@ -72,6 +89,23 @@ describe("query runner > add column", () => { column2.length.should.be.equal("100"); column2!.default!.should.be.equal("'this is description'"); + if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) { + await queryRunner.addColumn(table!, column3); + table = await queryRunner.getTable("post"); + column3 = table!.findColumnByName("textAndTag")!; + column3.should.be.exist; + column3!.generatedType!.should.be.equals("STORED"); + column3!.asExpression!.should.be.a("string"); + } + if (connection.driver instanceof MysqlDriver) { + await queryRunner.addColumn(table!, column4); + table = await queryRunner.getTable("post"); + column4 = table!.findColumnByName("textAndTag2")!; + column4.should.be.exist; + column4!.generatedType!.should.be.equals("VIRTUAL"); + column4!.asExpression!.should.be.a("string"); + } + await queryRunner.executeMemoryDownSql(); table = await queryRunner.getTable("post"); diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index 732dbfd71c..d40398628d 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -4,6 +4,7 @@ import {Connection} from "../../../src/connection/Connection"; import {CockroachDriver} from "../../../src/driver/cockroachdb/CockroachDriver"; import {closeTestingConnections, createTestingConnections} from "../../utils/test-utils"; import {AbstractSqliteDriver} from "../../../src/driver/sqlite-abstract/AbstractSqliteDriver"; +import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver"; describe("query runner > change column", () => { @@ -135,4 +136,45 @@ describe("query runner > change column", () => { }))); + it("should correctly change generated as expression", () => Promise.all(connections.map(async connection => { + + // Only works on postgres + if (!(connection.driver instanceof PostgresDriver)) return; + + const queryRunner = connection.createQueryRunner(); + let table = await queryRunner.getTable("post"); + + let generatedColumn = table!.findColumnByName("generated")!; + generatedColumn!.generatedType!.should.be.equals("STORED"); + generatedColumn!.asExpression!.should.be.equals("text || tag"); + + let changedGeneratedColumn = generatedColumn.clone(); + changedGeneratedColumn.asExpression = "text || tag || name"; + + await queryRunner.changeColumn(table!, generatedColumn, changedGeneratedColumn); + + table = await queryRunner.getTable("post"); + generatedColumn = table!.findColumnByName("generated")!; + generatedColumn!.generatedType!.should.be.equals("STORED"); + generatedColumn!.asExpression!.should.be.equals("text || tag || name"); + + changedGeneratedColumn = generatedColumn.clone(); + delete changedGeneratedColumn.generatedType; + await queryRunner.changeColumn(table!, generatedColumn, changedGeneratedColumn); + + table = await queryRunner.getTable("post"); + generatedColumn = table!.findColumnByName("generated")!; + generatedColumn!.should.not.haveOwnProperty("generatedType"); + generatedColumn!.should.not.haveOwnProperty("asExpression"); + + changedGeneratedColumn = generatedColumn.clone(); + changedGeneratedColumn.asExpression = "text || tag || name"; + changedGeneratedColumn.generatedType = "STORED"; + await queryRunner.changeColumn(table!, generatedColumn, changedGeneratedColumn); + + table = await queryRunner.getTable("post"); + generatedColumn = table!.findColumnByName("generated")!; + generatedColumn!.generatedType!.should.be.equals("STORED"); + generatedColumn!.asExpression!.should.be.equals("text || tag || name"); + }))); }); diff --git a/test/functional/query-runner/entity/Post.ts b/test/functional/query-runner/entity/Post.ts index ba7bf13271..87235d9302 100644 --- a/test/functional/query-runner/entity/Post.ts +++ b/test/functional/query-runner/entity/Post.ts @@ -26,4 +26,6 @@ export class Post { @Column() tag: string; + @Column({ generatedType: "STORED", asExpression: "text || tag" }) + generated: string; } From dcbfe932d097db151cf4955ecbfa624f3b0db705 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 17:51:39 +0200 Subject: [PATCH 03/24] docs: document generated columns for postgres 12 --- docs/decorator-reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/decorator-reference.md b/docs/decorator-reference.md index 438dbd20eb..67b6e3231f 100644 --- a/docs/decorator-reference.md +++ b/docs/decorator-reference.md @@ -194,8 +194,8 @@ If `true`, MySQL automatically adds the `UNSIGNED` attribute to this column. * `enum: string[]|AnyEnum` - Used in `enum` column type to specify list of allowed enum values. You can specify array of values or specify a enum class. * `enumName: string` - A name for generated enum type. If not specified, TypeORM will generate a enum type from entity and column names - so it's neccessary if you intend to use the same enum type in different tables. -* `asExpression: string` - Generated column expression. Used only in [MySQL](https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html). -* `generatedType: "VIRTUAL"|"STORED"` - Generated column type. Used only in [MySQL](https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html). +* `asExpression: string` - Generated column expression. Used only in [MySQL](https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html) and [Postgres](https://www.postgresql.org/docs/12/ddl-generated-columns.html). +* `generatedType: "VIRTUAL"|"STORED"` - Generated column type. Used only in [MySQL](https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html) and [Postgres (Only "STORED")](https://www.postgresql.org/docs/12/ddl-generated-columns.html). * `hstoreType: "object"|"string"` - Return type of `HSTORE` column. Returns value as string or as object. Used only in [Postgres](https://www.postgresql.org/docs/9.6/static/hstore.html). * `array: boolean` - Used for postgres and cockroachdb column types which can be array (for example int[]). * `transformer: ValueTransformer|ValueTransformer[]` - Specifies a value transformer (or array of value transformers) that is to be used to (un)marshal this column when reading or writing to the database. In case of an array, the value transformers will be applied in the natural order from entityValue to databaseValue, and in reverse order from databaseValue to entityValue. From 8b0a7af934c91e647ee55d5da0f6a9eb00176149 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 19:05:16 +0200 Subject: [PATCH 04/24] fix: check postgres version for generated columns Generated columns are only available on postgres version 12+ --- src/driver/postgres/PostgresDriver.ts | 12 ++++++ src/driver/postgres/PostgresQueryRunner.ts | 36 ++++++++++-------- src/schema-builder/RdbmsSchemaBuilder.ts | 7 +++- test/functional/query-runner/add-column.ts | 38 ++++++++++++------- test/functional/query-runner/change-column.ts | 3 ++ 5 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 2b8c0f02ee..f409dd3df6 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -910,6 +910,18 @@ export class PostgresDriver implements Driver { return true; } + /** + * Returns true if postgres supports generated columns + */ + async isGeneratedColumnsSupported(): Promise { + const runner = this.createQueryRunner(); + const results = await runner.query("SHOW server_version;", []); + await runner.release(); + const versionString = results[0]["server_version"] as string; + const version = parseFloat(versionString); + return version >= 12; + } + /** * Returns true if driver supports fulltext indices. */ diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index cf6f84ef3e..445b6749f6 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -344,17 +344,21 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner return Promise.resolve(); })); - // if table have column with generated type, we must add the expression to the meta table - await Promise.all(table.columns - .filter(column => column.generatedType === "STORED" && column.asExpression) - .map(async column => { - const tableName = await this.getTableNameWithSchema(table.name); - const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); - upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); - downQueries.push(deleteQuery); - return Promise.resolve(); - })); + if (await this.driver.isGeneratedColumnsSupported()) { + // if table have column with generated type, we must add the expression to the meta table + await Promise.all(table.columns + .filter(column => column.generatedType === "STORED" && column.asExpression) + .map(async column => { + const tableName = await this.getTableNameWithSchema(table.name); + const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + upQueries.push(deleteQuery); + upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + downQueries.push(deleteQuery); + return Promise.resolve(); + })); + } else { + table.columns = table.columns.filter(column => !column.generatedType); + } upQueries.push(this.createTableSql(table, createForeignKeys)); downQueries.push(this.dropTableSql(table)); @@ -575,7 +579,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP CONSTRAINT "${uniqueConstraint.name}"`)); } - if (column.generatedType === "STORED" && column.asExpression) { + if (column.generatedType === "STORED" && column.asExpression && await this.driver.isGeneratedColumnsSupported()) { const tableName = await this.getTableNameWithSchema(table.name); const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); upQueries.push(deleteQuery); @@ -906,7 +910,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`)); } - if (newColumn.generatedType !== oldColumn.generatedType) { + if (newColumn.generatedType !== oldColumn.generatedType && await this.driver.isGeneratedColumnsSupported()) { // Convert generated column data to normal column if (!newColumn.generatedType || newColumn.generatedType === "VIRTUAL") { // We can copy the generated data to the new column @@ -1010,7 +1014,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } - if (column.generatedType === "STORED") { + if (column.generatedType === "STORED" && await this.driver.isGeneratedColumnsSupported()) { const tableName = await this.getTableNameWithSchema(table.name); upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name])); downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); @@ -1514,6 +1518,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner if (!dbTables.length) return []; + const supportsGeneratedColumns = await this.driver.isGeneratedColumnsSupported(); + // create tables for loaded tables return Promise.all(dbTables.map(async dbTable => { const table = new Table(); @@ -1639,7 +1645,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } - if (dbColumn["is_generated"] === "ALWAYS" && dbColumn["generation_expression"]) { + if (dbColumn["is_generated"] === "ALWAYS" && dbColumn["generation_expression"] && supportsGeneratedColumns) { // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index d7c9b1e530..a790b32de7 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -75,8 +75,11 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { // Remove condition or add new conditions if necessary (for CHECK constraints for example). if (this.viewEntityToSyncMetadatas.length > 0) await this.createTypeormMetadataTable(); - if (this.connection.driver instanceof PostgresDriver) - await this.createTypeormGeneratedMetadataTable(); + if (this.connection.driver instanceof PostgresDriver) { + if (await this.connection.driver.isGeneratedColumnsSupported()) { + await this.createTypeormGeneratedMetadataTable(); + } + } await this.queryRunner.getTables(tablePaths); await this.queryRunner.getViews([]); await this.executeSchemaSyncOperationsInProperOrder(); diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index f3d747da8c..ef54f6230e 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -90,20 +90,30 @@ describe("query runner > add column", () => { column2!.default!.should.be.equal("'this is description'"); if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) { - await queryRunner.addColumn(table!, column3); - table = await queryRunner.getTable("post"); - column3 = table!.findColumnByName("textAndTag")!; - column3.should.be.exist; - column3!.generatedType!.should.be.equals("STORED"); - column3!.asExpression!.should.be.a("string"); - } - if (connection.driver instanceof MysqlDriver) { - await queryRunner.addColumn(table!, column4); - table = await queryRunner.getTable("post"); - column4 = table!.findColumnByName("textAndTag2")!; - column4.should.be.exist; - column4!.generatedType!.should.be.equals("VIRTUAL"); - column4!.asExpression!.should.be.a("string"); + const isMySQL = connection.driver instanceof MysqlDriver; + let postgresSupported = false; + + if (connection.driver instanceof PostgresDriver) { + postgresSupported = await connection.driver.isGeneratedColumnsSupported(); + } + + if (isMySQL || postgresSupported) { + await queryRunner.addColumn(table!, column3); + table = await queryRunner.getTable("post"); + column3 = table!.findColumnByName("textAndTag")!; + column3.should.be.exist; + column3!.generatedType!.should.be.equals("STORED"); + column3!.asExpression!.should.be.a("string"); + + if (connection.driver instanceof MysqlDriver) { + await queryRunner.addColumn(table!, column4); + table = await queryRunner.getTable("post"); + column4 = table!.findColumnByName("textAndTag2")!; + column4.should.be.exist; + column4!.generatedType!.should.be.equals("VIRTUAL"); + column4!.asExpression!.should.be.a("string"); + } + } } await queryRunner.executeMemoryDownSql(); diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index d40398628d..fb31d28f11 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -141,6 +141,9 @@ describe("query runner > change column", () => { // Only works on postgres if (!(connection.driver instanceof PostgresDriver)) return; + // Database is running < postgres 12 + if (!await connection.driver.isGeneratedColumnsSupported()) return; + const queryRunner = connection.createQueryRunner(); let table = await queryRunner.getTable("post"); From 216c3040bf9a974aa40ccb08c2fa588233a884b0 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 19:06:20 +0200 Subject: [PATCH 05/24] test: add postgres 12 to tests Currently, there are only tests for postgres 9. This adds postgres 12 as test target --- docker-compose.yml | 13 ++++ ormconfig.circleci-common.json | 11 ++++ ormconfig.circleci-postgres-12.json | 92 +++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 ormconfig.circleci-postgres-12.json diff --git a/docker-compose.yml b/docker-compose.yml index 47415bce4e..a0a04ff0df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,6 +50,19 @@ services: POSTGRES_PASSWORD: "test" POSTGRES_DB: "test" + postgres-12: + # mdillon/postgis is postgres + PostGIS (only). if you need additional + # extensions, it's probably time to create a purpose-built image with all + # necessary extensions. sorry, and thanks for adding support for them! + image: "postgis/postgis:12-2.5-alpine" + container_name: "typeorm-postgres-12" + ports: + - "5532:5432" + environment: + POSTGRES_USER: "test" + POSTGRES_PASSWORD: "test" + POSTGRES_DB: "test" + # mssql mssql: image: "mcr.microsoft.com/mssql/server:2017-latest-ubuntu" diff --git a/ormconfig.circleci-common.json b/ormconfig.circleci-common.json index d9f558a78a..f9cc9a449d 100644 --- a/ormconfig.circleci-common.json +++ b/ormconfig.circleci-common.json @@ -46,6 +46,17 @@ "database": "test", "logging": false }, + { + "skip": false, + "name": "postgres-12", + "type": "postgres", + "host": "localhost", + "port": 5532, + "username": "test", + "password": "test", + "database": "test", + "logging": false + }, { "skip": false, "name": "sqljs", diff --git a/ormconfig.circleci-postgres-12.json b/ormconfig.circleci-postgres-12.json new file mode 100644 index 0000000000..0b5bec41be --- /dev/null +++ b/ormconfig.circleci-postgres-12.json @@ -0,0 +1,92 @@ +[ + { + "skip": true, + "name": "mysql", + "type": "mysql", + "host": "localhost", + "port": 3306, + "username": "root", + "password": "admin", + "database": "test" + }, + { + "skip": true, + "name": "mariadb", + "type": "mariadb", + "host": "mariadb", + "port": 3306, + "username": "root", + "password": "admin", + "database": "test" + }, + { + "skip": true, + "name": "sqlite", + "type": "sqlite", + "database": "temp/sqlitedb.db" + }, + { + "skip": false, + "name": "postgres", + "type": "postgres", + "host": "localhost", + "port": 5432, + "username": "test", + "password": "test", + "database": "test" + }, + { + "skip": true, + "name": "sqljs", + "type": "sqljs" + }, + { + "skip": true, + "name": "mssql", + "type": "mssql", + "host": "localhost", + "username": "sa", + "password": "Admin12345", + "database": "tempdb" + }, + { + "skip": true, + "name": "sap", + "type": "sap", + "host": "localhost", + "port": 39015, + "username": "SYSTEM", + "password": "HXEHana1", + "database": "HXE", + "logging": false + }, + { + "skip": true, + "disabledIfNotEnabledImplicitly": true, + "name": "mongodb", + "type": "mongodb", + "database": "test", + "useNewUrlParser": true, + "useUnifiedTopology": true + }, + { + "skip": true, + "name": "cockroachdb", + "type": "cockroachdb", + "host": "localhost", + "port": 26257, + "username": "root", + "password": "", + "database": "defaultdb" + }, + { + "skip": true, + "name": "oracle", + "type": "oracle", + "host": "localhost", + "username": "typeorm", + "password": "Passw0rd", + "port": 1521, + "sid": "orclpdb1.localdomain" + } +] From 12ca7111d6b7d82e712e359e3b81513900b416f1 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 19:24:38 +0200 Subject: [PATCH 06/24] test: remove generated column from model MariaDB will fail with a generated column type --- test/functional/query-runner/change-column.ts | 15 ++++++++++++++- test/functional/query-runner/entity/Post.ts | 2 -- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index fb31d28f11..35bdf30ead 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -5,6 +5,7 @@ import {CockroachDriver} from "../../../src/driver/cockroachdb/CockroachDriver"; import {closeTestingConnections, createTestingConnections} from "../../utils/test-utils"; import {AbstractSqliteDriver} from "../../../src/driver/sqlite-abstract/AbstractSqliteDriver"; import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver"; +import {TableColumn} from "../../../src"; describe("query runner > change column", () => { @@ -145,9 +146,21 @@ describe("query runner > change column", () => { if (!await connection.driver.isGeneratedColumnsSupported()) return; const queryRunner = connection.createQueryRunner(); + + let generatedColumn = new TableColumn({ + name: "generated", + type: "character varying", + generatedType: "STORED", + asExpression: "text || tag" + }); + let table = await queryRunner.getTable("post"); - let generatedColumn = table!.findColumnByName("generated")!; + await queryRunner.addColumn(table!, generatedColumn); + + table = await queryRunner.getTable("post"); + + generatedColumn = table!.findColumnByName("generated")!; generatedColumn!.generatedType!.should.be.equals("STORED"); generatedColumn!.asExpression!.should.be.equals("text || tag"); diff --git a/test/functional/query-runner/entity/Post.ts b/test/functional/query-runner/entity/Post.ts index 87235d9302..ba7bf13271 100644 --- a/test/functional/query-runner/entity/Post.ts +++ b/test/functional/query-runner/entity/Post.ts @@ -26,6 +26,4 @@ export class Post { @Column() tag: string; - @Column({ generatedType: "STORED", asExpression: "text || tag" }) - generated: string; } From be0497f3007ed962453c3a332e94435f012b75dc Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 19:25:06 +0200 Subject: [PATCH 07/24] test: use non alpine container for postgres 12 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a0a04ff0df..0fec9c6c5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -54,7 +54,7 @@ services: # mdillon/postgis is postgres + PostGIS (only). if you need additional # extensions, it's probably time to create a purpose-built image with all # necessary extensions. sorry, and thanks for adding support for them! - image: "postgis/postgis:12-2.5-alpine" + image: "postgis/postgis:12-2.5" container_name: "typeorm-postgres-12" ports: - "5532:5432" From 326540a0a4b703d6cbed6fd1fbc0a70a85638932 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sun, 26 Jul 2020 19:35:56 +0200 Subject: [PATCH 08/24] test: skip generated columns test on mariadb --- test/functional/query-runner/add-column.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index ef54f6230e..223f5bac0a 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -90,7 +90,7 @@ describe("query runner > add column", () => { column2!.default!.should.be.equal("'this is description'"); if (connection.driver instanceof MysqlDriver || connection.driver instanceof PostgresDriver) { - const isMySQL = connection.driver instanceof MysqlDriver; + const isMySQL = connection.driver instanceof MysqlDriver && connection.options.type === "mysql"; let postgresSupported = false; if (connection.driver instanceof PostgresDriver) { From 818d00feb446651437aba4e5abada72a7d1ef61f Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Mon, 27 Jul 2020 14:04:32 +0200 Subject: [PATCH 09/24] fix: detect generated column change --- src/driver/postgres/PostgresDriver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index f409dd3df6..5a2aa80ea7 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -883,7 +883,9 @@ export class PostgresDriver implements Driver { || (tableColumn.enum && columnMetadata.enum && !OrmUtils.isArraysEqual(tableColumn.enum, columnMetadata.enum.map(val => val + ""))) // enums in postgres are always strings || tableColumn.isGenerated !== columnMetadata.isGenerated || (tableColumn.spatialFeatureType || "").toLowerCase() !== (columnMetadata.spatialFeatureType || "").toLowerCase() - || tableColumn.srid !== columnMetadata.srid; + || tableColumn.srid !== columnMetadata.srid + || tableColumn.generatedType !== columnMetadata.generatedType + || (tableColumn.asExpression || "").trim() !== (columnMetadata.asExpression || "").trim(); }); } From 396400554c1bb9d000ee34141b9a316e2c4dd43f Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Thu, 13 Aug 2020 13:01:46 +0200 Subject: [PATCH 10/24] fix: circle ci postgres version --- .circleci/config.yml | 44 ++++++++++++++ ormconfig.circleci-common.json | 11 ---- ormconfig.circleci-postgres-12.json | 92 ----------------------------- 3 files changed, 44 insertions(+), 103 deletions(-) delete mode 100644 ormconfig.circleci-postgres-12.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 58256e0dd9..aa73913e49 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -108,9 +108,38 @@ jobs: node-version: type: string default: "10" + postgres-version: + type: string + default: "9.6.11" working_directory: ~/typeorm docker: - image: circleci/node:<< parameters.node-version >> + - image: mysql:5.7.24 + environment: + MYSQL_ROOT_PASSWORD: "admin" + MYSQL_DATABASE: "test" + - image: mariadb:10.1.37 + name: mariadb + environment: + MYSQL_ROOT_PASSWORD: "admin" + MYSQL_DATABASE: "test" + - image: circleci/postgres:<< parameters.postgres-version >>-postgis + name: postgres + environment: + POSTGRES_USER: "test" + POSTGRES_PASSWORD: "test" + POSTGRES_DB: "test" + - image: cockroachdb/cockroach:latest + name: cockroachdb + command: start --insecure + - image: circleci/mongo:3.4.18 + name: mongodb + - image: mcr.microsoft.com/mssql/server:2017-latest + name: mssql + environment: + SA_PASSWORD: "Admin123" + ACCEPT_EULA: "Y" + steps: - checkout - setup_remote_docker @@ -194,6 +223,7 @@ workflows: - build databases: "mysql mariadb postgres mssql mongodb sqlite better-sqlite3 sqljs" matrix: + databases: "mysql mariadb sqlite better-sqlite3 postgres sqljs mssql mongodb" parameters: node-version: - "10" @@ -206,6 +236,20 @@ workflows: - build databases: "cockroachdb" node-version: "12" + - test: + name: test (postgres-12) - Node v<< matrix.node-version >> + requires: + - lint + - build + matrix: + databases: "postgres" + parameters: + node-version: + - "10" + - "12" + - "13" + postgres-version: + - "12.3" - test: name: test (oracle) - Node v12 requires: diff --git a/ormconfig.circleci-common.json b/ormconfig.circleci-common.json index f9cc9a449d..d9f558a78a 100644 --- a/ormconfig.circleci-common.json +++ b/ormconfig.circleci-common.json @@ -46,17 +46,6 @@ "database": "test", "logging": false }, - { - "skip": false, - "name": "postgres-12", - "type": "postgres", - "host": "localhost", - "port": 5532, - "username": "test", - "password": "test", - "database": "test", - "logging": false - }, { "skip": false, "name": "sqljs", diff --git a/ormconfig.circleci-postgres-12.json b/ormconfig.circleci-postgres-12.json deleted file mode 100644 index 0b5bec41be..0000000000 --- a/ormconfig.circleci-postgres-12.json +++ /dev/null @@ -1,92 +0,0 @@ -[ - { - "skip": true, - "name": "mysql", - "type": "mysql", - "host": "localhost", - "port": 3306, - "username": "root", - "password": "admin", - "database": "test" - }, - { - "skip": true, - "name": "mariadb", - "type": "mariadb", - "host": "mariadb", - "port": 3306, - "username": "root", - "password": "admin", - "database": "test" - }, - { - "skip": true, - "name": "sqlite", - "type": "sqlite", - "database": "temp/sqlitedb.db" - }, - { - "skip": false, - "name": "postgres", - "type": "postgres", - "host": "localhost", - "port": 5432, - "username": "test", - "password": "test", - "database": "test" - }, - { - "skip": true, - "name": "sqljs", - "type": "sqljs" - }, - { - "skip": true, - "name": "mssql", - "type": "mssql", - "host": "localhost", - "username": "sa", - "password": "Admin12345", - "database": "tempdb" - }, - { - "skip": true, - "name": "sap", - "type": "sap", - "host": "localhost", - "port": 39015, - "username": "SYSTEM", - "password": "HXEHana1", - "database": "HXE", - "logging": false - }, - { - "skip": true, - "disabledIfNotEnabledImplicitly": true, - "name": "mongodb", - "type": "mongodb", - "database": "test", - "useNewUrlParser": true, - "useUnifiedTopology": true - }, - { - "skip": true, - "name": "cockroachdb", - "type": "cockroachdb", - "host": "localhost", - "port": 26257, - "username": "root", - "password": "", - "database": "defaultdb" - }, - { - "skip": true, - "name": "oracle", - "type": "oracle", - "host": "localhost", - "username": "typeorm", - "password": "Passw0rd", - "port": 1521, - "sid": "orclpdb1.localdomain" - } -] From 7ff79e28cefd72fee55d9011d527463848826f4c Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sat, 10 Oct 2020 13:10:49 +0200 Subject: [PATCH 11/24] fix: add replication mode to isGeneratedColumnsSupported() function Latest changes in master introduce replication mode. This commit adjust the the pull request #6469 to this change --- src/driver/postgres/PostgresDriver.ts | 4 ++-- src/driver/postgres/PostgresQueryRunner.ts | 10 +++++----- src/schema-builder/RdbmsSchemaBuilder.ts | 2 +- test/functional/query-runner/add-column.ts | 2 +- test/functional/query-runner/change-column.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 5a2aa80ea7..a93d2fbfb3 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -915,8 +915,8 @@ export class PostgresDriver implements Driver { /** * Returns true if postgres supports generated columns */ - async isGeneratedColumnsSupported(): Promise { - const runner = this.createQueryRunner(); + async isGeneratedColumnsSupported(mode: ReplicationMode): Promise { + const runner = this.createQueryRunner(mode); const results = await runner.query("SHOW server_version;", []); await runner.release(); const versionString = results[0]["server_version"] as string; diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 445b6749f6..aef4f98d20 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -344,7 +344,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner return Promise.resolve(); })); - if (await this.driver.isGeneratedColumnsSupported()) { + if (await this.driver.isGeneratedColumnsSupported(this.mode)) { // if table have column with generated type, we must add the expression to the meta table await Promise.all(table.columns .filter(column => column.generatedType === "STORED" && column.asExpression) @@ -579,7 +579,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP CONSTRAINT "${uniqueConstraint.name}"`)); } - if (column.generatedType === "STORED" && column.asExpression && await this.driver.isGeneratedColumnsSupported()) { + if (column.generatedType === "STORED" && column.asExpression && await this.driver.isGeneratedColumnsSupported(this.mode)) { const tableName = await this.getTableNameWithSchema(table.name); const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); upQueries.push(deleteQuery); @@ -910,7 +910,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`)); } - if (newColumn.generatedType !== oldColumn.generatedType && await this.driver.isGeneratedColumnsSupported()) { + if (newColumn.generatedType !== oldColumn.generatedType && await this.driver.isGeneratedColumnsSupported(this.mode)) { // Convert generated column data to normal column if (!newColumn.generatedType || newColumn.generatedType === "VIRTUAL") { // We can copy the generated data to the new column @@ -1014,7 +1014,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } - if (column.generatedType === "STORED" && await this.driver.isGeneratedColumnsSupported()) { + if (column.generatedType === "STORED" && await this.driver.isGeneratedColumnsSupported(this.mode)) { const tableName = await this.getTableNameWithSchema(table.name); upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name])); downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); @@ -1518,7 +1518,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner if (!dbTables.length) return []; - const supportsGeneratedColumns = await this.driver.isGeneratedColumnsSupported(); + const supportsGeneratedColumns = await this.driver.isGeneratedColumnsSupported(this.mode); // create tables for loaded tables return Promise.all(dbTables.map(async dbTable => { diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index a790b32de7..dac1e3cb8f 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -76,7 +76,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { if (this.viewEntityToSyncMetadatas.length > 0) await this.createTypeormMetadataTable(); if (this.connection.driver instanceof PostgresDriver) { - if (await this.connection.driver.isGeneratedColumnsSupported()) { + if (await this.connection.driver.isGeneratedColumnsSupported("master")) { await this.createTypeormGeneratedMetadataTable(); } } diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 223f5bac0a..70ca4067a5 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -94,7 +94,7 @@ describe("query runner > add column", () => { let postgresSupported = false; if (connection.driver instanceof PostgresDriver) { - postgresSupported = await connection.driver.isGeneratedColumnsSupported(); + postgresSupported = await connection.driver.isGeneratedColumnsSupported("master"); } if (isMySQL || postgresSupported) { diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index 35bdf30ead..a56596a014 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -143,7 +143,7 @@ describe("query runner > change column", () => { if (!(connection.driver instanceof PostgresDriver)) return; // Database is running < postgres 12 - if (!await connection.driver.isGeneratedColumnsSupported()) return; + if (!await connection.driver.isGeneratedColumnsSupported("master")) return; const queryRunner = connection.createQueryRunner(); From 3278244d8506387044c59008b988008a0908182c Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Sat, 10 Oct 2020 13:27:22 +0200 Subject: [PATCH 12/24] fix: ci testing for postgres 12 Latest changes in master broke the postgres 12 test setup --- .circleci/config.yml | 54 +++++----------------------------- ormconfig.circleci-common.json | 11 +++++++ 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index aa73913e49..2fa9445062 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -63,7 +63,6 @@ commands: curl -sf -o node_modules/oracledb/instantclient.zip $BLOB_URL unzip -qqo node_modules/oracledb/instantclient.zip -d node_modules/oracledb/ rm node_modules/oracledb/instantclient.zip - DEBIAN_FRONTEND=noninteractive sudo apt-get -qq -y install libaio1 cp /lib/*/libaio.so.* node_modules/oracledb/instantclient_19_8/ fi @@ -108,38 +107,9 @@ jobs: node-version: type: string default: "10" - postgres-version: - type: string - default: "9.6.11" working_directory: ~/typeorm docker: - image: circleci/node:<< parameters.node-version >> - - image: mysql:5.7.24 - environment: - MYSQL_ROOT_PASSWORD: "admin" - MYSQL_DATABASE: "test" - - image: mariadb:10.1.37 - name: mariadb - environment: - MYSQL_ROOT_PASSWORD: "admin" - MYSQL_DATABASE: "test" - - image: circleci/postgres:<< parameters.postgres-version >>-postgis - name: postgres - environment: - POSTGRES_USER: "test" - POSTGRES_PASSWORD: "test" - POSTGRES_DB: "test" - - image: cockroachdb/cockroach:latest - name: cockroachdb - command: start --insecure - - image: circleci/mongo:3.4.18 - name: mongodb - - image: mcr.microsoft.com/mssql/server:2017-latest - name: mssql - environment: - SA_PASSWORD: "Admin123" - ACCEPT_EULA: "Y" - steps: - checkout - setup_remote_docker @@ -154,10 +124,9 @@ jobs: npx js-yaml ./docker-compose.yml \ | jq -r '.services | keys | map(select(. | IN($ARGS.positional[]))) | join(" ")' --args << parameters.databases >> ) - docker-compose --project-name typeorm --no-ansi up --detach $SERVICES - install-packages: - cache-key: node<< parameters.node-version >> + cache-key: node<< parameters.node-version >> - run: name: Set up TypeORM Test Runner command: | @@ -182,7 +151,6 @@ jobs: ' ) echo "Running '$COMMANDS'" - docker run \ --network typeorm_default \ --tty \ @@ -201,7 +169,6 @@ jobs: --name typeorm-testrunner \ circleci/node:<< parameters.node-version >> \ npx nyc npm run test-fast - docker cp typeorm-testrunner:/typeorm/coverage/ ./ - run: name: Stop all Relevant Services @@ -223,7 +190,6 @@ workflows: - build databases: "mysql mariadb postgres mssql mongodb sqlite better-sqlite3 sqljs" matrix: - databases: "mysql mariadb sqlite better-sqlite3 postgres sqljs mssql mongodb" parameters: node-version: - "10" @@ -237,23 +203,17 @@ workflows: databases: "cockroachdb" node-version: "12" - test: - name: test (postgres-12) - Node v<< matrix.node-version >> + name: test (oracle) - Node v12 requires: - lint - build - matrix: - databases: "postgres" - parameters: - node-version: - - "10" - - "12" - - "13" - postgres-version: - - "12.3" + databases: "oracle" + node-version: "12" + - test: - name: test (oracle) - Node v12 + name: test (postgres 12) - Node v12 requires: - lint - build - databases: "oracle" + databases: "postgres-12" node-version: "12" diff --git a/ormconfig.circleci-common.json b/ormconfig.circleci-common.json index d9f558a78a..d67d180bb5 100644 --- a/ormconfig.circleci-common.json +++ b/ormconfig.circleci-common.json @@ -46,6 +46,17 @@ "database": "test", "logging": false }, + { + "skip": true, + "name": "postgres-12", + "type": "postgres", + "host": "typeorm-postgres-12", + "port": 5432, + "username": "test", + "password": "test", + "database": "test", + "logging": false + }, { "skip": false, "name": "sqljs", From ac395e73686ca1e56780c8cba8fc859ba23aca29 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Mon, 12 Oct 2020 12:58:29 +0200 Subject: [PATCH 13/24] style: remove SqlServerConnectionOptions generic parameter for createTypeormGeneratedMetadataTable function imnotjames notice this in his review of the pull request --- src/schema-builder/RdbmsSchemaBuilder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index dac1e3cb8f..3a92b6b0ff 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -795,7 +795,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { * Creates typeorm service table for storing user defined generated columns as expressions. */ protected async createTypeormGeneratedMetadataTable() { - const options = this.connection.driver.options; + const options = this.connection.driver.options; const typeormMetadataTable = this.connection.driver.buildTableName("typeorm_generation_meta", options.schema, options.database); await this.queryRunner.createTable(new Table( From 6cceda8b4b904de213716298935c9d4534eef3bb Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Mon, 12 Oct 2020 13:09:43 +0200 Subject: [PATCH 14/24] style: remove unnecessary return of Promise.resolve() This return of Promise.resolve() has no effect. We can leave it out --- src/driver/postgres/PostgresQueryRunner.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index aef4f98d20..6eb3c7dbdf 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -354,7 +354,6 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner upQueries.push(deleteQuery); upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); downQueries.push(deleteQuery); - return Promise.resolve(); })); } else { table.columns = table.columns.filter(column => !column.generatedType); From e8445628f4e14ccebf3c2310842971e9eeb47fad Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Mon, 12 Oct 2020 13:16:26 +0200 Subject: [PATCH 15/24] style: fix whitespace issue for config.yml --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2fa9445062..c4ccb9d5b6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -63,6 +63,7 @@ commands: curl -sf -o node_modules/oracledb/instantclient.zip $BLOB_URL unzip -qqo node_modules/oracledb/instantclient.zip -d node_modules/oracledb/ rm node_modules/oracledb/instantclient.zip + DEBIAN_FRONTEND=noninteractive sudo apt-get -qq -y install libaio1 cp /lib/*/libaio.so.* node_modules/oracledb/instantclient_19_8/ fi @@ -124,6 +125,7 @@ jobs: npx js-yaml ./docker-compose.yml \ | jq -r '.services | keys | map(select(. | IN($ARGS.positional[]))) | join(" ")' --args << parameters.databases >> ) + docker-compose --project-name typeorm --no-ansi up --detach $SERVICES - install-packages: cache-key: node<< parameters.node-version >> @@ -151,6 +153,7 @@ jobs: ' ) echo "Running '$COMMANDS'" + docker run \ --network typeorm_default \ --tty \ @@ -169,6 +172,7 @@ jobs: --name typeorm-testrunner \ circleci/node:<< parameters.node-version >> \ npx nyc npm run test-fast + docker cp typeorm-testrunner:/typeorm/coverage/ ./ - run: name: Stop all Relevant Services @@ -209,7 +213,6 @@ workflows: - build databases: "oracle" node-version: "12" - - test: name: test (postgres 12) - Node v12 requires: From 7856c5ef969091eab5de1e584794dbb997a06c26 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Tue, 19 Jan 2021 21:01:13 +0100 Subject: [PATCH 16/24] refactor: use VersionUtils Instead of parsing the version string with parseFloat, use the typeorm VersionUtils --- src/driver/postgres/PostgresDriver.ts | 8 ++-- src/driver/postgres/PostgresQueryRunner.ts | 37 ++++++++----------- src/schema-builder/RdbmsSchemaBuilder.ts | 2 +- test/functional/query-runner/add-column.ts | 5 ++- test/functional/query-runner/change-column.ts | 6 +-- 5 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 8bcea32cb7..f9097f91b3 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -19,6 +19,7 @@ import {EntityMetadata} from "../../metadata/EntityMetadata"; import {OrmUtils} from "../../util/OrmUtils"; import {ApplyValueTransformers} from "../../util/ApplyValueTransformers"; import {ReplicationMode} from "../types/ReplicationMode"; +import {VersionUtils} from "../../util/VersionUtils"; /** * Organizes communication with PostgreSQL DBMS. @@ -915,13 +916,10 @@ export class PostgresDriver implements Driver { /** * Returns true if postgres supports generated columns */ - async isGeneratedColumnsSupported(mode: ReplicationMode): Promise { - const runner = this.createQueryRunner(mode); + async isGeneratedColumnsSupported(runner: QueryRunner): Promise { const results = await runner.query("SHOW server_version;", []); - await runner.release(); const versionString = results[0]["server_version"] as string; - const version = parseFloat(versionString); - return version >= 12; + return VersionUtils.isGreaterOrEqual(versionString, '12.0'); } /** diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 8becc2fd37..32d1887ada 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -361,20 +361,16 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner return Promise.resolve(); })); - if (await this.driver.isGeneratedColumnsSupported(this.mode)) { - // if table have column with generated type, we must add the expression to the meta table - await Promise.all(table.columns - .filter(column => column.generatedType === "STORED" && column.asExpression) - .map(async column => { - const tableName = await this.getTableNameWithSchema(table.name); - const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); - upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); - downQueries.push(deleteQuery); - })); - } else { - table.columns = table.columns.filter(column => !column.generatedType); - } + // if table have column with generated type, we must add the expression to the meta table + await Promise.all(table.columns + .filter(column => column.generatedType === "STORED" && column.asExpression) + .map(async column => { + const tableName = await this.getTableNameWithSchema(table.name); + const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + upQueries.push(deleteQuery); + upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + downQueries.push(deleteQuery); + })); upQueries.push(this.createTableSql(table, createForeignKeys)); downQueries.push(this.dropTableSql(table)); @@ -595,8 +591,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP CONSTRAINT "${uniqueConstraint.name}"`)); } - - if (column.generatedType === "STORED" && column.asExpression && await this.driver.isGeneratedColumnsSupported(this.mode)) { + if (column.generatedType === "STORED" && column.asExpression) { const tableName = await this.getTableNameWithSchema(table.name); const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); upQueries.push(deleteQuery); @@ -933,7 +928,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ALTER COLUMN "${newColumn.name}" TYPE ${this.driver.createFullType(oldColumn)}`)); } - if (newColumn.generatedType !== oldColumn.generatedType && await this.driver.isGeneratedColumnsSupported(this.mode)) { + if (newColumn.generatedType !== oldColumn.generatedType) { // Convert generated column data to normal column if (!newColumn.generatedType || newColumn.generatedType === "VIRTUAL") { // We can copy the generated data to the new column @@ -1037,7 +1032,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } - if (column.generatedType === "STORED" && await this.driver.isGeneratedColumnsSupported(this.mode)) { + if (column.generatedType === "STORED") { const tableName = await this.getTableNameWithSchema(table.name); upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name])); downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); @@ -1548,8 +1543,6 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner if (!dbTables.length) return []; - const supportsGeneratedColumns = await this.driver.isGeneratedColumnsSupported(this.mode); - // create tables for loaded tables return Promise.all(dbTables.map(async dbTable => { const table = new Table(); @@ -1677,7 +1670,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } - if (dbColumn["is_generated"] === "ALWAYS" && dbColumn["generation_expression"] && supportsGeneratedColumns) { + if (dbColumn["is_generated"] === "ALWAYS" && dbColumn["generation_expression"]) { // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. @@ -1689,7 +1682,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner tableColumn.asExpression = ""; } } - + tableColumn.comment = dbColumn["description"] == null ? undefined : dbColumn["description"]; if (dbColumn["character_set_name"]) diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index 3a92b6b0ff..1b4bea2b46 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -76,7 +76,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { if (this.viewEntityToSyncMetadatas.length > 0) await this.createTypeormMetadataTable(); if (this.connection.driver instanceof PostgresDriver) { - if (await this.connection.driver.isGeneratedColumnsSupported("master")) { + if (await this.connection.driver.isGeneratedColumnsSupported(this.queryRunner)) { await this.createTypeormGeneratedMetadataTable(); } } diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 70ca4067a5..85357e55c9 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -7,6 +7,7 @@ import {AbstractSqliteDriver} from "../../../src/driver/sqlite-abstract/Abstract import {TableColumn} from "../../../src/schema-builder/table/TableColumn"; import {closeTestingConnections, createTestingConnections} from "../../utils/test-utils"; import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver"; +import {VersionUtils} from "../../../src/util/VersionUtils"; describe("query runner > add column", () => { @@ -94,7 +95,9 @@ describe("query runner > add column", () => { let postgresSupported = false; if (connection.driver instanceof PostgresDriver) { - postgresSupported = await connection.driver.isGeneratedColumnsSupported("master"); + const results = await queryRunner.query("SHOW server_version;"); + const versionString = results[0]["server_version"] as string; + postgresSupported = VersionUtils.isGreaterOrEqual(versionString, '12.0'); } if (isMySQL || postgresSupported) { diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index a56596a014..74b5d456eb 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -142,11 +142,11 @@ describe("query runner > change column", () => { // Only works on postgres if (!(connection.driver instanceof PostgresDriver)) return; - // Database is running < postgres 12 - if (!await connection.driver.isGeneratedColumnsSupported("master")) return; - const queryRunner = connection.createQueryRunner(); + // Database is running < postgres 12 + if (!await connection.driver.isGeneratedColumnsSupported(queryRunner)) return; + let generatedColumn = new TableColumn({ name: "generated", type: "character varying", From 9e13e4dd032617f2aaed97293e9be3f1482d316a Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Tue, 10 Aug 2021 13:56:20 +0200 Subject: [PATCH 17/24] fix: fix failing build After merging the upstream into the pr fork, the build stopped working. The reason why the build fails, is because in the upstream one import is missing and one variable was removed --- src/driver/postgres/PostgresQueryRunner.ts | 9 +++++---- src/schema-builder/RdbmsSchemaBuilder.ts | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 0e49020182..833be925b7 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -751,9 +751,9 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner if (!oldColumn) throw new TypeORMError(`Column "${oldTableColumnOrName}" was not found in the "${table.name}" table.`); - - if (oldColumn.type !== newColumn.type - || oldColumn.length !== newColumn.length + + if (oldColumn.type !== newColumn.type + || oldColumn.length !== newColumn.length || newColumn.isArray !== oldColumn.isArray || (!oldColumn.generatedType && newColumn.generatedType === "STORED") || (oldColumn.asExpression !== newColumn.asExpression && newColumn.generatedType === "STORED")) { @@ -1870,7 +1870,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. - const asExpressionQuery = `SELECT * FROM typeorm_generation_meta WHERE table_name = '${await this.getTableNameWithSchema(tableFullName)}' and column_name = '${tableColumn.name}'`; + const fullTableName = this.driver.buildTableName(dbTable["table_name"], dbTable["table_schema"]); + const asExpressionQuery = `SELECT * FROM typeorm_generation_meta WHERE table_name = '${await this.getTableNameWithSchema(fullTableName)}' and column_name = '${tableColumn.name}'`; const results: ObjectLiteral[] = await this.query(asExpressionQuery); if (results[0] && results[0].generation_expression) { tableColumn.asExpression = results[0].generation_expression; diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index d355a34c1e..e30fd109d7 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -19,6 +19,7 @@ import {TableCheck} from "./table/TableCheck"; import {TableExclusion} from "./table/TableExclusion"; import {View} from "./view/View"; import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver"; +import {PostgresConnectionOptions} from "../driver/postgres/PostgresConnectionOptions"; /** * Creates complete tables schemas in the database based on the entity metadatas. @@ -76,7 +77,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { if (this.viewEntityToSyncMetadatas.length > 0) { await this.createTypeormMetadataTable(); } - + if (this.connection.driver instanceof PostgresDriver) { if (await this.connection.driver.isGeneratedColumnsSupported(this.queryRunner)) { await this.createTypeormGeneratedMetadataTable(); @@ -85,7 +86,7 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { // Flush the queryrunner table & view cache const tablePaths = this.entityToSyncMetadatas.map(metadata => this.getTablePath(metadata)); - + await this.queryRunner.getTables(tablePaths); await this.queryRunner.getViews([]); From cf8e09916253efd32d71773864527570849134b5 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Wed, 10 Nov 2021 16:37:46 +0100 Subject: [PATCH 18/24] refactor: replace promise.all() with for loop --- src/driver/postgres/PostgresQueryRunner.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 7f2b4ab183..d80332009c 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -417,15 +417,14 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } // if table have column with generated type, we must add the expression to the meta table - await Promise.all(table.columns - .filter(column => column.generatedType === "STORED" && column.asExpression) - .map(async column => { - const tableName = await this.getTableNameWithSchema(table.name); - const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); - upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); - downQueries.push(deleteQuery); - })); + for (const column of table.columns) { + if (column.generatedType !== "STORED" || !column.asExpression) continue; + const tableName = await this.getTableNameWithSchema(table.name); + const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + upQueries.push(deleteQuery); + upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + downQueries.push(deleteQuery); + } upQueries.push(this.createTableSql(table, createForeignKeys)); downQueries.push(this.dropTableSql(table)); From 714e46df6d09f6211749ea1791add36fc88efb16 Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Wed, 10 Nov 2021 19:31:44 +0100 Subject: [PATCH 19/24] refactor: make requested changes --- src/driver/postgres/PostgresDriver.ts | 15 +++---- src/driver/postgres/PostgresQueryRunner.ts | 41 +++++++++++-------- src/schema-builder/RdbmsSchemaBuilder.ts | 40 +----------------- test/functional/query-runner/add-column.ts | 5 +-- test/functional/query-runner/change-column.ts | 2 +- 5 files changed, 33 insertions(+), 70 deletions(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index a6766f034e..20c395bc31 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -271,6 +271,8 @@ export class PostgresDriver implements Driver { */ maxAliasLength = 63; + isGeneratedColumnsSupported: boolean = false; + // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- @@ -354,6 +356,10 @@ export class PostgresDriver implements Driver { await this.enableExtensions(extensionsMetadata, connection); await release() } + + const results = await this.executeQuery(this.connection, 'SHOW server_version;') as any; + const versionString = results[0]["server_version"] as string; + this.isGeneratedColumnsSupported = VersionUtils.isGreaterOrEqual(versionString, '12.0'); } protected async enableExtensions(extensionsMetadata: any, connection: any) { @@ -1065,15 +1071,6 @@ export class PostgresDriver implements Driver { return true; } - /** - * Returns true if postgres supports generated columns - */ - async isGeneratedColumnsSupported(runner: QueryRunner): Promise { - const results = await runner.query("SHOW server_version;", []); - const versionString = results[0]["server_version"] as string; - return VersionUtils.isGreaterOrEqual(versionString, '12.0'); - } - /** * Returns true if driver supports fulltext indices. */ diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index d80332009c..2d53d2fa66 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -419,10 +419,12 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // if table have column with generated type, we must add the expression to the meta table for (const column of table.columns) { if (column.generatedType !== "STORED" || !column.asExpression) continue; - const tableName = await this.getTableNameWithSchema(table.name); - const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); + const tableName = tableNameWithSchema[1]; + const schema = tableNameWithSchema[0]; + const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND database = $3 AND type = 'generated_column' AND schema = $4`, [tableName, column.name, this.connection.driver.database, schema]); upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + upQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); downQueries.push(deleteQuery); } @@ -667,10 +669,12 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } if (column.generatedType === "STORED" && column.asExpression) { - const tableName = await this.getTableNameWithSchema(table.name); - const deleteQuery = new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name]); + const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); + const tableName = tableNameWithSchema[1]; + const schema = tableNameWithSchema[0]; + const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database]); upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + upQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); downQueries.push(deleteQuery); } @@ -1026,17 +1030,19 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // Convert generated column data to normal column if (!newColumn.generatedType || newColumn.generatedType === "VIRTUAL") { // We can copy the generated data to the new column - const tableName = await this.getTableNameWithSchema(table.name); + const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); + const tableName = tableNameWithSchema[1]; + const schema = tableNameWithSchema[0]; upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} RENAME COLUMN "${oldColumn.name}" TO "TEMP_OLD_${oldColumn.name}"`)); upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, newColumn)}`)); upQueries.push(new Query(`UPDATE ${this.escapePath(table)} SET "${newColumn.name}" = "TEMP_OLD_${oldColumn.name}"`)); upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "TEMP_OLD_${oldColumn.name}"`)); - upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, oldColumn.name])); + upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, oldColumn.name, schema, this.driver.database])); // However, we can't copy it back on downgrade. It needs to regenerate. downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "${newColumn.name}"`)); downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, oldColumn)}`)); - downQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, newColumn.name])); - downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, oldColumn.name, oldColumn.asExpression])); + downQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, newColumn.name, schema, this.driver.database])); + downQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, oldColumn.name, oldColumn.asExpression, schema, this.driver.database])); } } @@ -1127,9 +1133,11 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } if (column.generatedType === "STORED") { - const tableName = await this.getTableNameWithSchema(table.name); - upQueries.push(new Query(`DELETE FROM typeorm_generation_meta WHERE table_name = $1 AND column_name = $2`, [tableName, column.name])); - downQueries.push(new Query(`INSERT INTO typeorm_generation_meta(table_name, column_name, generation_expression) VALUES ($1, $2, $3)`, [tableName, column.name, column.asExpression])); + const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); + const tableName = tableNameWithSchema[1]; + const schema = tableNameWithSchema[0]; + upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database])); + downQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, schema, this.driver.database])); } await this.executeQueries(upQueries, downQueries); @@ -1856,11 +1864,10 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. - const fullTableName = this.driver.buildTableName(dbTable["table_name"], dbTable["table_schema"]); - const asExpressionQuery = `SELECT * FROM typeorm_generation_meta WHERE table_name = '${await this.getTableNameWithSchema(fullTableName)}' and column_name = '${tableColumn.name}'`; + const asExpressionQuery = `SELECT * FROM typeorm_generation_meta WHERE table = '${dbTable["table_name"]}' AND name = '${tableColumn.name}' AND schema = '${dbTable["table_schema"]}' AND database = '${this.driver.database}' AND type = 'generated_column'`; const results: ObjectLiteral[] = await this.query(asExpressionQuery); - if (results[0] && results[0].generation_expression) { - tableColumn.asExpression = results[0].generation_expression; + if (results[0] && results[0].value) { + tableColumn.asExpression = results[0].value; } else { tableColumn.asExpression = ""; } diff --git a/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index b132e672b0..a84581bd59 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -19,7 +19,6 @@ import {TableExclusion} from "./table/TableExclusion"; import {View} from "./view/View"; import { ViewUtils } from "./util/ViewUtils"; import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver"; -import {PostgresConnectionOptions} from "../driver/postgres/PostgresConnectionOptions"; /** * Creates complete tables schemas in the database based on the entity metadatas. @@ -78,16 +77,10 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { } try { - if (this.viewEntityToSyncMetadatas.length > 0) { + if (this.viewEntityToSyncMetadatas.length > 0 || (this.connection.driver instanceof PostgresDriver && this.connection.driver.isGeneratedColumnsSupported)) { await this.createTypeormMetadataTable(); } - if (this.connection.driver instanceof PostgresDriver) { - if (await this.connection.driver.isGeneratedColumnsSupported(this.queryRunner)) { - await this.createTypeormGeneratedMetadataTable(); - } - } - // Flush the queryrunner table & view cache const tablePaths = this.entityToSyncMetadatas.map(metadata => this.getTablePath(metadata)); @@ -883,35 +876,4 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { }, ), true); } - - /** - * Creates typeorm service table for storing user defined generated columns as expressions. - */ - protected async createTypeormGeneratedMetadataTable() { - const options = this.connection.driver.options; - const typeormMetadataTable = this.connection.driver.buildTableName("typeorm_generation_meta", options.schema, options.database); - - await this.queryRunner.createTable(new Table( - { - name: typeormMetadataTable, - columns: [ - { - name: "table_name", - type: this.connection.driver.normalizeType({type: this.connection.driver.mappedDataTypes.metadataType}), - isNullable: false, - }, - { - name: "column_name", - type: this.connection.driver.normalizeType({type: this.connection.driver.mappedDataTypes.metadataDatabase}), - isNullable: false - }, - { - name: "generation_expression", - type: this.connection.driver.normalizeType({type: this.connection.driver.mappedDataTypes.metadataValue}), - isNullable: true - }, - ] - }, - ), true); - } } diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 85357e55c9..5af7e347f1 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -7,7 +7,6 @@ import {AbstractSqliteDriver} from "../../../src/driver/sqlite-abstract/Abstract import {TableColumn} from "../../../src/schema-builder/table/TableColumn"; import {closeTestingConnections, createTestingConnections} from "../../utils/test-utils"; import {PostgresDriver} from "../../../src/driver/postgres/PostgresDriver"; -import {VersionUtils} from "../../../src/util/VersionUtils"; describe("query runner > add column", () => { @@ -95,9 +94,7 @@ describe("query runner > add column", () => { let postgresSupported = false; if (connection.driver instanceof PostgresDriver) { - const results = await queryRunner.query("SHOW server_version;"); - const versionString = results[0]["server_version"] as string; - postgresSupported = VersionUtils.isGreaterOrEqual(versionString, '12.0'); + postgresSupported = connection.driver.isGeneratedColumnsSupported; } if (isMySQL || postgresSupported) { diff --git a/test/functional/query-runner/change-column.ts b/test/functional/query-runner/change-column.ts index 74b5d456eb..f956adf61a 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -145,7 +145,7 @@ describe("query runner > change column", () => { const queryRunner = connection.createQueryRunner(); // Database is running < postgres 12 - if (!await connection.driver.isGeneratedColumnsSupported(queryRunner)) return; + if (!connection.driver.isGeneratedColumnsSupported) return; let generatedColumn = new TableColumn({ name: "generated", From 036337d221dd924483f65dee74f4076417f8520b Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Wed, 10 Nov 2021 19:33:03 +0100 Subject: [PATCH 20/24] fix: update table name --- src/driver/postgres/PostgresQueryRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 2d53d2fa66..51bec80280 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -1864,7 +1864,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. - const asExpressionQuery = `SELECT * FROM typeorm_generation_meta WHERE table = '${dbTable["table_name"]}' AND name = '${tableColumn.name}' AND schema = '${dbTable["table_schema"]}' AND database = '${this.driver.database}' AND type = 'generated_column'`; + const asExpressionQuery = `SELECT * FROM typeorm_metadata WHERE table = '${dbTable["table_name"]}' AND name = '${tableColumn.name}' AND schema = '${dbTable["table_schema"]}' AND database = '${this.driver.database}' AND type = 'generated_column'`; const results: ObjectLiteral[] = await this.query(asExpressionQuery); if (results[0] && results[0].value) { tableColumn.asExpression = results[0].value; From 13b265844b870758ac07b922620150d1fa65cfbd Mon Sep 17 00:00:00 2001 From: Nils Bergmann Date: Thu, 11 Nov 2021 19:08:11 +0100 Subject: [PATCH 21/24] fix: server version query and escape table column in queries --- src/driver/postgres/PostgresDriver.ts | 2 +- src/driver/postgres/PostgresQueryRunner.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index 20c395bc31..bc4515e8d3 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -358,7 +358,7 @@ export class PostgresDriver implements Driver { } const results = await this.executeQuery(this.connection, 'SHOW server_version;') as any; - const versionString = results[0]["server_version"] as string; + const versionString = results.rows[0]["server_version"] as string; this.isGeneratedColumnsSupported = VersionUtils.isGreaterOrEqual(versionString, '12.0'); } diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index 51bec80280..a94b4fd347 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -422,9 +422,9 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; - const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND database = $3 AND type = 'generated_column' AND schema = $4`, [tableName, column.name, this.connection.driver.database, schema]); + const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND database = $3 AND type = 'generated_column' AND schema = $4`, [tableName, column.name, this.connection.driver.database, schema]); upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); + upQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); downQueries.push(deleteQuery); } @@ -672,9 +672,9 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; - const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database]); + const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database]); upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); + upQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); downQueries.push(deleteQuery); } @@ -1037,12 +1037,12 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, newColumn)}`)); upQueries.push(new Query(`UPDATE ${this.escapePath(table)} SET "${newColumn.name}" = "TEMP_OLD_${oldColumn.name}"`)); upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "TEMP_OLD_${oldColumn.name}"`)); - upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, oldColumn.name, schema, this.driver.database])); + upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, oldColumn.name, schema, this.driver.database])); // However, we can't copy it back on downgrade. It needs to regenerate. downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "${newColumn.name}"`)); downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, oldColumn)}`)); - downQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, newColumn.name, schema, this.driver.database])); - downQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, oldColumn.name, oldColumn.asExpression, schema, this.driver.database])); + downQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, newColumn.name, schema, this.driver.database])); + downQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, oldColumn.name, oldColumn.asExpression, schema, this.driver.database])); } } @@ -1136,8 +1136,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; - upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE table = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database])); - downQueries.push(new Query(`INSERT INTO typeorm_metadata(table, name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, schema, this.driver.database])); + upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database])); + downQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, schema, this.driver.database])); } await this.executeQueries(upQueries, downQueries); @@ -1864,7 +1864,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. - const asExpressionQuery = `SELECT * FROM typeorm_metadata WHERE table = '${dbTable["table_name"]}' AND name = '${tableColumn.name}' AND schema = '${dbTable["table_schema"]}' AND database = '${this.driver.database}' AND type = 'generated_column'`; + const asExpressionQuery = `SELECT * FROM typeorm_metadata WHERE "table" = '${dbTable["table_name"]}' AND name = '${tableColumn.name}' AND schema = '${dbTable["table_schema"]}' AND database = '${this.driver.database}' AND type = 'generated_column'`; const results: ObjectLiteral[] = await this.query(asExpressionQuery); if (results[0] && results[0].value) { tableColumn.asExpression = results[0].value; From 75e3a7301ca2b96509190a1eba40a07378a2aac1 Mon Sep 17 00:00:00 2001 From: Dmitry Zotov Date: Sat, 13 Nov 2021 21:04:04 +0500 Subject: [PATCH 22/24] code refactoring --- .../AuroraDataApiQueryRunner.ts | 26 +-- .../cockroachdb/CockroachQueryRunner.ts | 26 +-- src/driver/mysql/MysqlQueryRunner.ts | 30 ++-- src/driver/oracle/OracleQueryRunner.ts | 26 +-- src/driver/postgres/PostgresDriver.ts | 15 +- src/driver/postgres/PostgresQueryRunner.ts | 170 ++++++++++++------ src/driver/sap/SapQueryRunner.ts | 26 +-- .../AbstractSqliteQueryRunner.ts | 24 +-- src/driver/sqlserver/SqlServerQueryRunner.ts | 33 ++-- src/driver/types/MetadataTableType.ts | 5 + src/query-runner/BaseQueryRunner.ts | 67 +++++++ test/functional/query-runner/add-column.ts | 1 + 12 files changed, 270 insertions(+), 179 deletions(-) create mode 100644 src/driver/types/MetadataTableType.ts diff --git a/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts b/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts index 246bee195f..3dbaf14e79 100644 --- a/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts +++ b/src/driver/aurora-data-api/AuroraDataApiQueryRunner.ts @@ -22,6 +22,7 @@ import {TableCheck} from "../../schema-builder/table/TableCheck"; import {IsolationLevel} from "../types/IsolationLevel"; import {TableExclusion} from "../../schema-builder/table/TableExclusion"; import { TypeORMError } from "../../error"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single mysql database connection. @@ -1172,7 +1173,7 @@ export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRu }).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})` : ""}`; + `INNER JOIN \`information_schema\`.\`views\` \`v\` ON \`v\`.\`table_schema\` = \`t\`.\`schema\` AND \`v\`.\`table_name\` = \`t\`.\`name\` WHERE \`t\`.\`type\` = '${MetadataTableType.VIEW}' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; const dbViews = await this.query(query); return dbViews.map((dbView: any) => { const view = new View(); @@ -1532,13 +1533,12 @@ export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRu 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); + return this.insertTypeormMetadataSql({ + type: MetadataTableType.VIEW, + schema: currentDatabase, + name: view.name, + value: expression + }); } /** @@ -1554,15 +1554,7 @@ export class AuroraDataApiQueryRunner extends BaseQueryRunner implements QueryRu 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); + return this.deleteTypeormMetadataSql({ type: MetadataTableType.VIEW, schema: currentDatabase, name: viewName }); } /** diff --git a/src/driver/cockroachdb/CockroachQueryRunner.ts b/src/driver/cockroachdb/CockroachQueryRunner.ts index 78e29f8c35..d3c05778e6 100644 --- a/src/driver/cockroachdb/CockroachQueryRunner.ts +++ b/src/driver/cockroachdb/CockroachQueryRunner.ts @@ -24,6 +24,7 @@ import {IsolationLevel} from "../types/IsolationLevel"; import {TableExclusion} from "../../schema-builder/table/TableExclusion"; import {ReplicationMode} from "../types/ReplicationMode"; import { TypeORMError } from "../../error"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single postgres database connection. @@ -1380,7 +1381,7 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner }).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})` : ""}`; + `INNER JOIN "information_schema"."views" "v" ON "v"."table_schema" = "t"."schema" AND "v"."table_name" = "t"."name" WHERE "t"."type" = '${MetadataTableType.VIEW}' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; const dbViews = await this.query(query); return dbViews.map((dbView: any) => { const view = new View(); @@ -1798,13 +1799,12 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner } 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: schema, name: name, value: expression }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.insertTypeormMetadataSql({ + type: MetadataTableType.VIEW, + schema: schema, + name: name, + value: expression + }); } /** @@ -1826,15 +1826,7 @@ export class CockroachQueryRunner extends BaseQueryRunner implements QueryRunner schema = currentSchema; } - const qb = this.connection.createQueryBuilder(); - const [query, parameters] = qb.delete() - .from(this.getTypeormMetadataTableName()) - .where(`${qb.escape("type")} = 'VIEW'`) - .andWhere(`${qb.escape("schema")} = :schema`, { schema }) - .andWhere(`${qb.escape("name")} = :name`, { name }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.deleteTypeormMetadataSql({ type: MetadataTableType.VIEW, schema, name }); } /** diff --git a/src/driver/mysql/MysqlQueryRunner.ts b/src/driver/mysql/MysqlQueryRunner.ts index e8c3622b01..c906f730e6 100644 --- a/src/driver/mysql/MysqlQueryRunner.ts +++ b/src/driver/mysql/MysqlQueryRunner.ts @@ -25,6 +25,7 @@ import {TableExclusion} from "../../schema-builder/table/TableExclusion"; import {VersionUtils} from "../../util/VersionUtils"; import {ReplicationMode} from "../types/ReplicationMode"; import { TypeORMError } from "../../error"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single mysql database connection. @@ -1227,7 +1228,7 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { }).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})` : ""}`; + `INNER JOIN \`information_schema\`.\`views\` \`v\` ON \`v\`.\`table_schema\` = \`t\`.\`schema\` AND \`v\`.\`table_name\` = \`t\`.\`name\` WHERE \`t\`.\`type\` = '${MetadataTableType.VIEW}' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; const dbViews = await this.query(query); return dbViews.map((dbView: any) => { const view = new View(); @@ -1720,13 +1721,12 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { 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); + return this.insertTypeormMetadataSql({ + type: MetadataTableType.VIEW, + schema: currentDatabase, + name: view.name, + value: expression + }); } /** @@ -1742,15 +1742,11 @@ export class MysqlQueryRunner extends BaseQueryRunner implements QueryRunner { 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); + return this.deleteTypeormMetadataSql({ + type: MetadataTableType.VIEW, + schema: currentDatabase, + name: viewName + }); } /** diff --git a/src/driver/oracle/OracleQueryRunner.ts b/src/driver/oracle/OracleQueryRunner.ts index 0859f52e60..1df251734c 100644 --- a/src/driver/oracle/OracleQueryRunner.ts +++ b/src/driver/oracle/OracleQueryRunner.ts @@ -23,6 +23,7 @@ import {TableExclusion} from "../../schema-builder/table/TableExclusion"; import {ReplicationMode} from "../types/ReplicationMode"; import { TypeORMError } from "../../error"; import { QueryResult } from "../../query-runner/QueryResult"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single oracle database connection. @@ -1248,7 +1249,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { const viewNamesString = viewNames.map(name => "'" + name + "'").join(", "); let query = `SELECT "T".* FROM ${this.escapePath(this.getTypeormMetadataTableName())} "T" ` + `INNER JOIN "USER_OBJECTS" "O" ON "O"."OBJECT_NAME" = "T"."name" AND "O"."OBJECT_TYPE" IN ( 'MATERIALIZED VIEW', 'VIEW' ) ` + - `WHERE "T"."type" IN ( 'MATERIALIZED_VIEW', 'VIEW' )`; + `WHERE "T"."type" IN ( '${MetadataTableType.MATERIALIZED_VIEW}', '${MetadataTableType.VIEW}' )`; if (viewNamesString.length > 0) query += ` AND "T"."name" IN (${viewNamesString})`; const dbViews = await this.query(query); @@ -1260,7 +1261,7 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { view.schema = parsedName.schema || dbView["schema"] || currentSchema; view.name = parsedName.tableName; view.expression = dbView["value"]; - view.materialized = dbView["type"] === "MATERIALIZED_VIEW"; + view.materialized = dbView["type"] === MetadataTableType.MATERIALIZED_VIEW; return view; }); } @@ -1584,14 +1585,8 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { protected insertViewDefinitionSql(view: View): Query { const expression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery(); - const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW" - const [query, parameters] = this.connection.createQueryBuilder() - .insert() - .into(this.getTypeormMetadataTableName()) - .values({ type: type, name: view.name, value: expression }) - .getQueryAndParameters(); - - return new Query(query, parameters); + const type = view.materialized ? MetadataTableType.MATERIALIZED_VIEW : MetadataTableType.VIEW; + return this.insertTypeormMetadataSql({ type: type, name: view.name, value: expression }); } /** @@ -1606,15 +1601,8 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner { * Builds remove view sql. */ protected deleteViewDefinitionSql(view: View): Query { - const qb = this.connection.createQueryBuilder(); - const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW" - const [query, parameters] = qb.delete() - .from(this.getTypeormMetadataTableName()) - .where(`${qb.escape("type")} = :type`, { type }) - .andWhere(`${qb.escape("name")} = :name`, { name: view.name }) - .getQueryAndParameters(); - - return new Query(query, parameters); + const type = view.materialized ? MetadataTableType.MATERIALIZED_VIEW : MetadataTableType.VIEW; + return this.deleteTypeormMetadataSql({ type, name: view.name }); } /** diff --git a/src/driver/postgres/PostgresDriver.ts b/src/driver/postgres/PostgresDriver.ts index bc4515e8d3..9dbd07fe94 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -349,17 +349,22 @@ export class PostgresDriver implements Driver { */ async afterConnect(): Promise { const extensionsMetadata = await this.checkMetadataForExtensions(); + const [ connection, release ] = await this.obtainMasterConnection() const installExtensions = this.options.installExtensions === undefined || this.options.installExtensions; if (installExtensions && extensionsMetadata.hasExtensions) { - const [ connection, release ] = await this.obtainMasterConnection() await this.enableExtensions(extensionsMetadata, connection); - await release() } - const results = await this.executeQuery(this.connection, 'SHOW server_version;') as any; - const versionString = results.rows[0]["server_version"] as string; - this.isGeneratedColumnsSupported = VersionUtils.isGreaterOrEqual(versionString, '12.0'); + const results = await this.executeQuery(connection, "SHOW server_version;") as { + rows: { + server_version: string; + }[]; + }; + const versionString = results.rows[0].server_version; + this.isGeneratedColumnsSupported = VersionUtils.isGreaterOrEqual(versionString, "12.0"); + + await release() } protected async enableExtensions(extensionsMetadata: any, connection: any) { diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index a94b4fd347..7404327864 100644 --- a/src/driver/postgres/PostgresQueryRunner.ts +++ b/src/driver/postgres/PostgresQueryRunner.ts @@ -23,8 +23,9 @@ import {IsolationLevel} from "../types/IsolationLevel"; import {PostgresDriver} from "./PostgresDriver"; import {ReplicationMode} from "../types/ReplicationMode"; import {VersionUtils} from "../../util/VersionUtils"; -import { TypeORMError } from "../../error"; -import { QueryResult } from "../../query-runner/QueryResult"; +import {TypeORMError} from "../../error"; +import {QueryResult} from "../../query-runner/QueryResult"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single postgres database connection. @@ -216,27 +217,28 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner this.driver.connection.logger.logQuerySlow(queryExecutionTime, query, parameters, this); const result = new QueryResult(); + if (raw) { + if (raw.hasOwnProperty('rows')) { + result.records = raw.rows; + } - if (raw?.hasOwnProperty('rows')) { - result.records = raw.rows; - } - - if (raw?.hasOwnProperty('rowCount')) { - result.affected = raw.rowCount; - } + if (raw.hasOwnProperty('rowCount')) { + result.affected = raw.rowCount; + } - switch (raw.command) { - case "DELETE": - case "UPDATE": - // for UPDATE and DELETE query additionally return number of affected rows - result.raw = [raw.rows, raw.rowCount]; - break; - default: - result.raw = raw.rows; - } + switch (raw.command) { + case "DELETE": + case "UPDATE": + // for UPDATE and DELETE query additionally return number of affected rows + result.raw = [raw.rows, raw.rowCount]; + break; + default: + result.raw = raw.rows; + } - if (!useStructuredResult) { - return result.raw; + if (!useStructuredResult) { + return result.raw; + } } return result; @@ -416,15 +418,32 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } - // if table have column with generated type, we must add the expression to the meta table - for (const column of table.columns) { - if (column.generatedType !== "STORED" || !column.asExpression) continue; + // if table have column with generated type, we must add the expression to the metadata table + const generatedColumns = table.columns.filter(column => column.generatedType === "STORED" && column.asExpression) + for (const column of generatedColumns) { const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; - const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND database = $3 AND type = 'generated_column' AND schema = $4`, [tableName, column.name, this.connection.driver.database, schema]); + + const insertQuery = this.insertTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: column.name, + value: column.asExpression + }) + + const deleteQuery = this.deleteTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: column.name + }) + upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); + upQueries.push(insertQuery); downQueries.push(deleteQuery); } @@ -672,9 +691,26 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; - const deleteQuery = new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database]); + + const insertQuery = this.insertTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: column.name, + value: column.asExpression + }) + + const deleteQuery = this.deleteTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: column.name + }) + upQueries.push(deleteQuery); - upQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, database, schema, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, this.driver.database, schema])); + upQueries.push(insertQuery); downQueries.push(deleteQuery); } @@ -1033,16 +1069,36 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; + upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} RENAME COLUMN "${oldColumn.name}" TO "TEMP_OLD_${oldColumn.name}"`)); upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, newColumn)}`)); upQueries.push(new Query(`UPDATE ${this.escapePath(table)} SET "${newColumn.name}" = "TEMP_OLD_${oldColumn.name}"`)); upQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "TEMP_OLD_${oldColumn.name}"`)); - upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, oldColumn.name, schema, this.driver.database])); + upQueries.push(this.deleteTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: oldColumn.name + })); // However, we can't copy it back on downgrade. It needs to regenerate. downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} DROP COLUMN "${newColumn.name}"`)); downQueries.push(new Query(`ALTER TABLE ${this.escapePath(table)} ADD ${this.buildCreateColumnSql(table, oldColumn)}`)); - downQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, newColumn.name, schema, this.driver.database])); - downQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, oldColumn.name, oldColumn.asExpression, schema, this.driver.database])); + downQueries.push(this.deleteTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: newColumn.name + })); + downQueries.push(this.insertTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: oldColumn.name, + value: oldColumn.asExpression + })); } } @@ -1136,8 +1192,24 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); const tableName = tableNameWithSchema[1]; const schema = tableNameWithSchema[0]; - upQueries.push(new Query(`DELETE FROM typeorm_metadata WHERE "table" = $1 AND name = $2 AND schema = $3 AND database = $4 AND type = 'generated_column'`, [tableName, column.name, schema, this.driver.database])); - downQueries.push(new Query(`INSERT INTO typeorm_metadata("table", name, value, schema, database, type) VALUES ($1, $2, $3, $4, $5, 'generated_column')`, [tableName, column.name, column.asExpression, schema, this.driver.database])); + const insertQuery = this.deleteTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: column.name + }) + const deleteQuery = this.insertTypeormMetadataSql({ + database: this.driver.database, + schema, + table: tableName, + type: MetadataTableType.GENERATED_COLUMN, + name: column.name, + value: column.asExpression + }) + + upQueries.push(insertQuery); + downQueries.push(deleteQuery); } await this.executeQueries(upQueries, downQueries); @@ -1556,7 +1628,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner const query = `SELECT "t".* FROM ${this.escapePath(this.getTypeormMetadataTableName())} "t" ` + `INNER JOIN "pg_catalog"."pg_class" "c" ON "c"."relname" = "t"."name" ` + `INNER JOIN "pg_namespace" "n" ON "n"."oid" = "c"."relnamespace" AND "n"."nspname" = "t"."schema" ` + - `WHERE "t"."type" IN ('VIEW', 'MATERIALIZED_VIEW') ${viewsCondition ? `AND (${viewsCondition})` : ""}`; + `WHERE "t"."type" IN ('${MetadataTableType.VIEW}', '${MetadataTableType.MATERIALIZED_VIEW}') ${viewsCondition ? `AND (${viewsCondition})` : ""}`; const dbViews = await this.query(query); return dbViews.map((dbView: any) => { @@ -1566,7 +1638,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner view.schema = dbView["schema"]; view.name = this.driver.buildTableName(dbView["name"], schema); view.expression = dbView["value"]; - view.materialized = dbView["type"] === "MATERIALIZED_VIEW"; + view.materialized = dbView["type"] === MetadataTableType.MATERIALIZED_VIEW; return view; }); } @@ -1864,7 +1936,13 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner // In postgres there is no VIRTUAL generated column type tableColumn.generatedType = "STORED"; // We cannot relay on information_schema.columns.generation_expression, because it is formatted different. - const asExpressionQuery = `SELECT * FROM typeorm_metadata WHERE "table" = '${dbTable["table_name"]}' AND name = '${tableColumn.name}' AND schema = '${dbTable["table_schema"]}' AND database = '${this.driver.database}' AND type = 'generated_column'`; + const asExpressionQuery = `SELECT * FROM "typeorm_metadata" ` + + ` WHERE "table" = '${dbTable["table_name"]}'` + + ` AND "name" = '${tableColumn.name}'` + + ` AND "schema" = '${dbTable["table_schema"]}'` + + ` AND "database" = '${this.driver.database}'` + + ` AND "type" = '${MetadataTableType.GENERATED_COLUMN}'`; + const results: ObjectLiteral[] = await this.query(asExpressionQuery); if (results[0] && results[0].value) { tableColumn.asExpression = results[0].value; @@ -2106,15 +2184,9 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner schema = currentSchema; } - const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW" + const type = view.materialized ? MetadataTableType.MATERIALIZED_VIEW : MetadataTableType.VIEW 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: type, schema: schema, name: name, value: expression }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.insertTypeormMetadataSql({ type, schema, name, value: expression }) } /** @@ -2137,16 +2209,8 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner schema = currentSchema; } - const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW" - const qb = this.connection.createQueryBuilder(); - const [query, parameters] = qb.delete() - .from(this.getTypeormMetadataTableName()) - .where(`${qb.escape("type")} = :type`, { type }) - .andWhere(`${qb.escape("schema")} = :schema`, { schema }) - .andWhere(`${qb.escape("name")} = :name`, { name }) - .getQueryAndParameters(); - - return new Query(query, parameters); + const type = view.materialized ? MetadataTableType.MATERIALIZED_VIEW : MetadataTableType.VIEW + return this.deleteTypeormMetadataSql({ type, schema, name }) } /** diff --git a/src/driver/sap/SapQueryRunner.ts b/src/driver/sap/SapQueryRunner.ts index 8c51b2c380..a3d4fc07a6 100644 --- a/src/driver/sap/SapQueryRunner.ts +++ b/src/driver/sap/SapQueryRunner.ts @@ -24,6 +24,7 @@ import {ReplicationMode} from "../types/ReplicationMode"; import { QueryFailedError, TypeORMError } from "../../error"; import { QueryResult } from "../../query-runner/QueryResult"; import { QueryLock } from "../../query-runner/QueryLock"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single SQL Server database connection. @@ -1483,7 +1484,7 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { return `("t"."schema" = '${schema}' AND "t"."name" = '${name}')`; }).join(" OR "); - const query = `SELECT "t".* FROM ${this.escapePath(this.getTypeormMetadataTableName())} "t" WHERE "t"."type" = 'VIEW' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; + const query = `SELECT "t".* FROM ${this.escapePath(this.getTypeormMetadataTableName())} "t" WHERE "t"."type" = '${MetadataTableType.VIEW}' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; const dbViews = await this.query(query); return dbViews.map((dbView: any) => { const view = new View(); @@ -1842,13 +1843,12 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { } 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: schema, name: name, value: expression }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.insertTypeormMetadataSql({ + type: MetadataTableType.VIEW, + schema: schema, + name: name, + value: expression + }); } /** @@ -1868,15 +1868,7 @@ export class SapQueryRunner extends BaseQueryRunner implements QueryRunner { schema = await this.getCurrentSchema(); } - const qb = this.connection.createQueryBuilder(); - const [query, parameters] = qb.delete() - .from(this.getTypeormMetadataTableName()) - .where(`${qb.escape("type")} = 'VIEW'`) - .andWhere(`${qb.escape("schema")} = :schema`, { schema }) - .andWhere(`${qb.escape("name")} = :name`, { name }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.deleteTypeormMetadataSql({ type: MetadataTableType.VIEW, schema, name }); } protected addColumnSql(table: Table, column: TableColumn): string { diff --git a/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts b/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts index 0f728f6d9d..0e69b39c86 100644 --- a/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts +++ b/src/driver/sqlite-abstract/AbstractSqliteQueryRunner.ts @@ -19,6 +19,7 @@ import {TableCheck} from "../../schema-builder/table/TableCheck"; import {IsolationLevel} from "../types/IsolationLevel"; import {TableExclusion} from "../../schema-builder/table/TableExclusion"; import { TypeORMError } from "../../error"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single sqlite database connection. @@ -765,7 +766,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen } const viewNamesString = viewNames.map(name => "'" + name + "'").join(", "); - let query = `SELECT "t".* FROM "${this.getTypeormMetadataTableName()}" "t" INNER JOIN "sqlite_master" s ON "s"."name" = "t"."name" AND "s"."type" = 'view' WHERE "t"."type" = 'VIEW'`; + let query = `SELECT "t".* FROM "${this.getTypeormMetadataTableName()}" "t" INNER JOIN "sqlite_master" s ON "s"."name" = "t"."name" AND "s"."type" = 'view' WHERE "t"."type" = '${MetadataTableType.VIEW}'`; if (viewNamesString.length > 0) query += ` AND "t"."name" IN (${viewNamesString})`; const dbViews = await this.query(query); @@ -1095,13 +1096,11 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen protected insertViewDefinitionSql(view: View): Query { 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", name: view.name, value: expression }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.insertTypeormMetadataSql({ + type: MetadataTableType.VIEW, + name: view.name, + value: expression + }); } /** @@ -1117,14 +1116,7 @@ export abstract class AbstractSqliteQueryRunner extends BaseQueryRunner implemen */ protected deleteViewDefinitionSql(viewOrPath: View|string): Query { 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("name")} = :name`, { name: viewName }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.deleteTypeormMetadataSql({ type: MetadataTableType.VIEW, name: viewName }); } /** diff --git a/src/driver/sqlserver/SqlServerQueryRunner.ts b/src/driver/sqlserver/SqlServerQueryRunner.ts index 55efb6518f..7146f1bde9 100644 --- a/src/driver/sqlserver/SqlServerQueryRunner.ts +++ b/src/driver/sqlserver/SqlServerQueryRunner.ts @@ -26,6 +26,7 @@ import {SqlServerDriver} from "./SqlServerDriver"; import {ReplicationMode} from "../types/ReplicationMode"; import { TypeORMError } from "../../error"; import { QueryLock } from "../../query-runner/QueryLock"; +import {MetadataTableType} from "../types/MetadataTableType"; /** * Runs queries on a single SQL Server database connection. @@ -1521,7 +1522,7 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner const query = dbNames.map(dbName => { return `SELECT "T".*, "V"."CHECK_OPTION" FROM ${this.escapePath(this.getTypeormMetadataTableName())} "t" ` + - `INNER JOIN "${dbName}"."INFORMATION_SCHEMA"."VIEWS" "V" ON "V"."TABLE_SCHEMA" = "T"."SCHEMA" AND "v"."TABLE_NAME" = "T"."NAME" WHERE "T"."TYPE" = 'VIEW' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; + `INNER JOIN "${dbName}"."INFORMATION_SCHEMA"."VIEWS" "V" ON "V"."TABLE_SCHEMA" = "T"."SCHEMA" AND "v"."TABLE_NAME" = "T"."NAME" WHERE "T"."TYPE" = '${MetadataTableType.VIEW}' ${viewsCondition ? `AND (${viewsCondition})` : ""}`; }).join(" UNION ALL "); const dbViews = await this.query(query); @@ -2016,13 +2017,13 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner } 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", database: parsedTableName.database, schema: parsedTableName.schema, name: parsedTableName.tableName, value: expression }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.insertTypeormMetadataSql({ + type: MetadataTableType.VIEW, + database: parsedTableName.database, + schema: parsedTableName.schema, + name: parsedTableName.tableName, + value: expression + }); } /** @@ -2042,16 +2043,12 @@ export class SqlServerQueryRunner extends BaseQueryRunner implements QueryRunner parsedTableName.schema = await this.getCurrentSchema(); } - const qb = this.connection.createQueryBuilder(); - const [query, parameters] = qb.delete() - .from(this.getTypeormMetadataTableName()) - .where(`${qb.escape("type")} = 'VIEW'`) - .andWhere(`${qb.escape("database")} = :database`, { database: parsedTableName.database }) - .andWhere(`${qb.escape("schema")} = :schema`, { schema: parsedTableName.schema }) - .andWhere(`${qb.escape("name")} = :name`, { name: parsedTableName.tableName }) - .getQueryAndParameters(); - - return new Query(query, parameters); + return this.deleteTypeormMetadataSql({ + type: MetadataTableType.VIEW, + database: parsedTableName.database, + schema: parsedTableName.schema, + name: parsedTableName.tableName + }); } /** diff --git a/src/driver/types/MetadataTableType.ts b/src/driver/types/MetadataTableType.ts new file mode 100644 index 0000000000..e66057dace --- /dev/null +++ b/src/driver/types/MetadataTableType.ts @@ -0,0 +1,5 @@ +export enum MetadataTableType { + VIEW = "VIEW", + MATERIALIZED_VIEW = "MATERIALIZED_VIEW", + GENERATED_COLUMN = "GENERATED_COLUMN" +} diff --git a/src/query-runner/BaseQueryRunner.ts b/src/query-runner/BaseQueryRunner.ts index 4259684574..24c4eb2dfa 100644 --- a/src/query-runner/BaseQueryRunner.ts +++ b/src/query-runner/BaseQueryRunner.ts @@ -13,6 +13,7 @@ import { TypeORMError } from "../error/TypeORMError"; import { EntityMetadata } from "../metadata/EntityMetadata"; import { TableForeignKey } from "../schema-builder/table/TableForeignKey"; import { OrmUtils } from "../util/OrmUtils"; +import {MetadataTableType} from "../driver/types/MetadataTableType"; export abstract class BaseQueryRunner { @@ -297,6 +298,72 @@ export abstract class BaseQueryRunner { return this.connection.driver.buildTableName("typeorm_metadata", options.schema, options.database); } + /** + * Generates SQL query to insert a record into "typeorm_metadata" table. + */ + protected insertTypeormMetadataSql({ + database, + schema, + table, + type, + name, + value + }: { + database?: string, + schema?: string, + table? : string, + type: MetadataTableType + name: string, + value?: string + }): Query { + const [query, parameters] = this.connection.createQueryBuilder() + .insert() + .into(this.getTypeormMetadataTableName()) + .values({ database: database, schema: schema, table: table, type: type, name: name, value: value }) + .getQueryAndParameters(); + + return new Query(query, parameters); + } + + /** + * Generates SQL query to delete a record from "typeorm_metadata" table. + */ + protected deleteTypeormMetadataSql({ + database, + schema, + table, + type, + name + }: { + database?: string, + schema?: string, + table?: string, + type: MetadataTableType, + name: string + }): Query { + + const qb = this.connection.createQueryBuilder(); + const deleteQb = qb.delete() + .from(this.getTypeormMetadataTableName()) + .where(`${qb.escape("type")} = :type`, { type }) + .andWhere(`${qb.escape("name")} = :name`, { name }); + + if (database) { + deleteQb.andWhere(`${qb.escape("database")} = :database`, { database }); + } + + if (schema) { + deleteQb.andWhere(`${qb.escape("schema")} = :schema`, { schema }); + } + + if (table) { + deleteQb.andWhere(`${qb.escape("table")} = :table`, { table }); + } + + const [query, parameters] = deleteQb.getQueryAndParameters(); + return new Query(query, parameters); + } + /** * Checks if at least one of column properties was changed. * Does not checks column type, length and autoincrement, because these properties changes separately. diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 5af7e347f1..0fba7b67f0 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -14,6 +14,7 @@ describe("query runner > add column", () => { before(async () => { connections = await createTestingConnections({ entities: [__dirname + "/entity/*{.js,.ts}"], + enabledDrivers: ["postgres"], schemaCreate: true, dropSchema: true, }); From 05a2a869cfdc2e910197d3622b9e33f308123a6f Mon Sep 17 00:00:00 2001 From: Dmitry Zotov Date: Sun, 14 Nov 2021 16:53:23 +0500 Subject: [PATCH 23/24] fixed lint issue --- src/query-runner/BaseQueryRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/query-runner/BaseQueryRunner.ts b/src/query-runner/BaseQueryRunner.ts index 24c4eb2dfa..37fb0f0702 100644 --- a/src/query-runner/BaseQueryRunner.ts +++ b/src/query-runner/BaseQueryRunner.ts @@ -311,7 +311,7 @@ export abstract class BaseQueryRunner { }: { database?: string, schema?: string, - table? : string, + table?: string, type: MetadataTableType name: string, value?: string From 5f767386b6b3e6f8a63bd102f1105770ddea1721 Mon Sep 17 00:00:00 2001 From: Dmitry Zotov Date: Sun, 14 Nov 2021 17:46:45 +0500 Subject: [PATCH 24/24] removed "enabledDrivers" from test --- test/functional/query-runner/add-column.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 0fba7b67f0..5af7e347f1 100644 --- a/test/functional/query-runner/add-column.ts +++ b/test/functional/query-runner/add-column.ts @@ -14,7 +14,6 @@ describe("query runner > add column", () => { before(async () => { connections = await createTestingConnections({ entities: [__dirname + "/entity/*{.js,.ts}"], - enabledDrivers: ["postgres"], schemaCreate: true, dropSchema: true, });