feat: implement generated columns for postgres 12 driver (#6469)

* 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

* test: add tests for generated columns in postgres 12

* docs: document generated columns for postgres 12

* fix: check postgres version for generated columns

Generated columns are only available on postgres version 12+

* test: add postgres 12 to tests

Currently, there are only tests for postgres 9. This adds postgres 12 as test target

* test: remove generated column from model

MariaDB will fail with a generated column type

* test: use non alpine container for postgres 12

* test: skip generated columns test on mariadb

* fix: detect generated column change

* fix: circle ci postgres version

* 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

* fix: ci testing for postgres 12

Latest changes in master broke the postgres 12 test setup

* style: remove SqlServerConnectionOptions generic parameter for createTypeormGeneratedMetadataTable function

imnotjames notice this in his review of the pull request

* style: remove unnecessary return of Promise.resolve()
This return of Promise.resolve() has no effect. We can leave it out

* style: fix whitespace issue for config.yml

* refactor: use VersionUtils

Instead of parsing the version string with parseFloat, use the typeorm VersionUtils

* 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

* refactor: replace promise.all() with for loop

* refactor: make requested changes

* fix: update table name

* fix: server version query and escape table column in queries

* code refactoring

* fixed lint issue

* removed "enabledDrivers" from test

Co-authored-by: Dmitry Zotov <dmzt08@gmail.com>
This commit is contained in:
Nils Bergmann 2021-11-14 14:02:35 +01:00 committed by GitHub
parent c895680dce
commit 91080be0cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 501 additions and 180 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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.

View File

@ -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",

View File

@ -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<Query> {
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<Query> {
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 });
}
/**

View File

@ -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 });
}
/**

View File

@ -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<Query> {
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<Query> {
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
});
}
/**

View File

@ -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 });
}
/**

View File

@ -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<void> {
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) {

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 });
}
/**

View File

@ -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
});
}
/**

View File

@ -0,0 +1,5 @@
export enum MetadataTableType {
VIEW = "VIEW",
MATERIALIZED_VIEW = "MATERIALIZED_VIEW",
GENERATED_COLUMN = "GENERATED_COLUMN"
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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");
})));
});