diff --git a/.circleci/config.yml b/.circleci/config.yml index f31fa2e246..59cdad9ba4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,7 +128,7 @@ jobs: 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: | @@ -213,3 +213,10 @@ workflows: - build databases: "oracle" node-version: "12" + - test: + name: test (postgres 12) - Node v12 + requires: + - lint + - build + databases: "postgres-12" + node-version: "12" diff --git a/docker-compose.yml b/docker-compose.yml index 992f3e6dbf..a7c0920151 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" + 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/docs/decorator-reference.md b/docs/decorator-reference.md index 3d7370613a..fc57a6efe3 100644 --- a/docs/decorator-reference.md +++ b/docs/decorator-reference.md @@ -193,8 +193,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. diff --git a/ormconfig.circleci-common.json b/ormconfig.circleci-common.json index c319913edf..36e108453b 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", 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 a902558852..9dbd07fe94 100644 --- a/src/driver/postgres/PostgresDriver.ts +++ b/src/driver/postgres/PostgresDriver.ts @@ -16,6 +16,7 @@ import {ColumnType} from "../types/ColumnTypes"; import {DataTypeDefaults} from "../types/DataTypeDefaults"; import {MappedColumnTypes} from "../types/MappedColumnTypes"; import {ReplicationMode} from "../types/ReplicationMode"; +import {VersionUtils} from "../../util/VersionUtils"; import {PostgresConnectionCredentialsOptions} from "./PostgresConnectionCredentialsOptions"; import {PostgresConnectionOptions} from "./PostgresConnectionOptions"; import {PostgresQueryRunner} from "./PostgresQueryRunner"; @@ -270,6 +271,8 @@ export class PostgresDriver implements Driver { */ maxAliasLength = 63; + isGeneratedColumnsSupported: boolean = false; + // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- @@ -346,13 +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(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) { @@ -1010,7 +1022,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(); // DEBUG SECTION // if (isColumnChanged) { diff --git a/src/driver/postgres/PostgresQueryRunner.ts b/src/driver/postgres/PostgresQueryRunner.ts index a0e5677e8c..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,6 +418,35 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } + // 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 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(insertQuery); + downQueries.push(deleteQuery); + } + upQueries.push(this.createTableSql(table, createForeignKeys)); downQueries.push(this.dropTableSql(table)); @@ -656,6 +687,33 @@ 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 tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); + const tableName = tableNameWithSchema[1]; + const schema = tableNameWithSchema[0]; + + 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(insertQuery); + downQueries.push(deleteQuery); + } + // create column's comment if (column.comment) { upQueries.push(new Query(`COMMENT ON COLUMN ${this.escapePath(table)}."${column.name}" IS ${this.escapeComment(column.comment)}`)); @@ -713,7 +771,12 @@ 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 || newColumn.isArray !== oldColumn.isArray) { + + 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")) { // To avoid data conversion, we just recreate column await this.dropColumn(table, oldColumn); await this.addColumn(table, newColumn); @@ -999,6 +1062,46 @@ 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 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(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(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 + })); + } + } + } await this.executeQueries(upQueries, downQueries); @@ -1085,6 +1188,30 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner } } + if (column.generatedType === "STORED") { + const tableNameWithSchema = (await this.getTableNameWithSchema(table.name)).split('.'); + const tableName = tableNameWithSchema[1]; + const schema = tableNameWithSchema[0]; + 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); clonedTable.removeColumn(column); @@ -1501,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) => { @@ -1511,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; }); } @@ -1805,6 +1932,25 @@ 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_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; + } else { + tableColumn.asExpression = ""; + } + } + tableColumn.comment = dbColumn["description"] ? dbColumn["description"] : undefined; if (dbColumn["character_set_name"]) tableColumn.charset = dbColumn["character_set_name"]; @@ -2038,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 }) } /** @@ -2069,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 }) } /** @@ -2327,6 +2459,21 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner return `"${tableName}"`; } + /** + * 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. */ @@ -2352,16 +2499,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/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..37fb0f0702 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/src/schema-builder/RdbmsSchemaBuilder.ts b/src/schema-builder/RdbmsSchemaBuilder.ts index d318922a62..a84581bd59 100644 --- a/src/schema-builder/RdbmsSchemaBuilder.ts +++ b/src/schema-builder/RdbmsSchemaBuilder.ts @@ -77,12 +77,13 @@ 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(); } // Flush the queryrunner table & view cache const tablePaths = this.entityToSyncMetadatas.map(metadata => this.getTablePath(metadata)); + await this.queryRunner.getTables(tablePaths); await this.queryRunner.getViews([]); @@ -875,5 +876,4 @@ export class RdbmsSchemaBuilder implements SchemaBuilder { }, ), true); } - } diff --git a/test/functional/query-runner/add-column.ts b/test/functional/query-runner/add-column.ts index 36f4d63637..5af7e347f1 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,33 @@ 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) { + const isMySQL = connection.driver instanceof MysqlDriver && connection.options.type === "mysql"; + let postgresSupported = false; + + if (connection.driver instanceof PostgresDriver) { + postgresSupported = 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(); 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..f956adf61a 100644 --- a/test/functional/query-runner/change-column.ts +++ b/test/functional/query-runner/change-column.ts @@ -4,6 +4,8 @@ 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"; +import {TableColumn} from "../../../src"; describe("query runner > change column", () => { @@ -135,4 +137,60 @@ 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(); + + // Database is running < postgres 12 + if (!connection.driver.isGeneratedColumnsSupported) return; + + let generatedColumn = new TableColumn({ + name: "generated", + type: "character varying", + generatedType: "STORED", + asExpression: "text || tag" + }); + + let table = await queryRunner.getTable("post"); + + 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"); + + 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"); + }))); });