Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.

Commit f85f436

Browse files
authored
fix: MATERIALIZED VIEW is treated as a regular VIEW which causes issues on sync (typeorm#7592)
* improved materialized view support in Postgres; * improved materialized view support in Oracle; * fixed falling test;
1 parent 3f2a02c commit f85f436

File tree

7 files changed

+212
-26
lines changed

7 files changed

+212
-26
lines changed

src/driver/oracle/OracleQueryRunner.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,10 +1153,17 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
11531153
async clearDatabase(): Promise<void> {
11541154
await this.startTransaction();
11551155
try {
1156+
// drop views
11561157
const dropViewsQuery = `SELECT 'DROP VIEW "' || VIEW_NAME || '"' AS "query" FROM "USER_VIEWS"`;
11571158
const dropViewQueries: ObjectLiteral[] = await this.query(dropViewsQuery);
11581159
await Promise.all(dropViewQueries.map(query => this.query(query["query"])));
11591160

1161+
// drop materialized views
1162+
const dropMatViewsQuery = `SELECT 'DROP MATERIALIZED VIEW "' || MVIEW_NAME || '"' AS "query" FROM "USER_MVIEWS"`;
1163+
const dropMatViewQueries: ObjectLiteral[] = await this.query(dropMatViewsQuery);
1164+
await Promise.all(dropMatViewQueries.map(query => this.query(query["query"])));
1165+
1166+
// drop tables
11601167
const dropTablesQuery = `SELECT 'DROP TABLE "' || TABLE_NAME || '" CASCADE CONSTRAINTS' AS "query" FROM "USER_TABLES"`;
11611168
const dropTableQueries: ObjectLiteral[] = await this.query(dropTablesQuery);
11621169
await Promise.all(dropTableQueries.map(query => this.query(query["query"])));
@@ -1181,14 +1188,17 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
11811188
return Promise.resolve([]);
11821189

11831190
const viewNamesString = viewNames.map(name => "'" + name + "'").join(", ");
1184-
let query = `SELECT "T".* FROM "${this.getTypeormMetadataTableName()}" "T" INNER JOIN "USER_VIEWS" "V" ON "V"."VIEW_NAME" = "T"."name" WHERE "T"."type" = 'VIEW'`;
1191+
let query = `SELECT "T".* FROM "${this.getTypeormMetadataTableName()}" "T" ` +
1192+
`INNER JOIN "USER_OBJECTS" "O" ON "O"."OBJECT_NAME" = "T"."name" AND "O"."OBJECT_TYPE" IN ( 'MATERIALIZED VIEW', 'VIEW' ) ` +
1193+
`WHERE "T"."type" IN ( 'MATERIALIZED_VIEW', 'VIEW' )`;
11851194
if (viewNamesString.length > 0)
11861195
query += ` AND "T"."name" IN (${viewNamesString})`;
11871196
const dbViews = await this.query(query);
11881197
return dbViews.map((dbView: any) => {
11891198
const view = new View();
11901199
view.name = dbView["name"];
11911200
view.expression = dbView["value"];
1201+
view.materialized = dbView["type"] === "MATERIALIZED_VIEW";
11921202
return view;
11931203
});
11941204
}
@@ -1444,10 +1454,11 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
14441454

14451455
protected insertViewDefinitionSql(view: View): Query {
14461456
const expression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery();
1457+
const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW"
14471458
const [query, parameters] = this.connection.createQueryBuilder()
14481459
.insert()
14491460
.into(this.getTypeormMetadataTableName())
1450-
.values({ type: "VIEW", name: view.name, value: expression })
1461+
.values({ type: type, name: view.name, value: expression })
14511462
.getQueryAndParameters();
14521463

14531464
return new Query(query, parameters);
@@ -1456,21 +1467,21 @@ export class OracleQueryRunner extends BaseQueryRunner implements QueryRunner {
14561467
/**
14571468
* Builds drop view sql.
14581469
*/
1459-
protected dropViewSql(viewOrPath: View|string): Query {
1460-
const viewName = viewOrPath instanceof View ? viewOrPath.name : viewOrPath;
1461-
return new Query(`DROP VIEW "${viewName}"`);
1470+
protected dropViewSql(view: View): Query {
1471+
const materializedClause = view.materialized ? "MATERIALIZED " : "";
1472+
return new Query(`DROP ${materializedClause}VIEW "${view.name}"`);
14621473
}
14631474

14641475
/**
14651476
* Builds remove view sql.
14661477
*/
1467-
protected deleteViewDefinitionSql(viewOrPath: View|string): Query {
1468-
const viewName = viewOrPath instanceof View ? viewOrPath.name : viewOrPath;
1478+
protected deleteViewDefinitionSql(view: View): Query {
14691479
const qb = this.connection.createQueryBuilder();
1480+
const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW"
14701481
const [query, parameters] = qb.delete()
14711482
.from(this.getTypeormMetadataTableName())
1472-
.where(`${qb.escape("type")} = 'VIEW'`)
1473-
.andWhere(`${qb.escape("name")} = :name`, { name: viewName })
1483+
.where(`${qb.escape("type")} = :type`, { type })
1484+
.andWhere(`${qb.escape("name")} = :name`, { name: view.name })
14741485
.getQueryAndParameters();
14751486

14761487
return new Query(query, parameters);

src/driver/postgres/PostgresQueryRunner.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
326326
async createDatabase(database: string, ifNotExist?: boolean): Promise<void> {
327327
if (ifNotExist) {
328328
const databaseAlreadyExists = await this.hasDatabase(database);
329-
329+
330330
if (databaseAlreadyExists)
331331
return Promise.resolve();
332332
}
@@ -1401,16 +1401,27 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
14011401

14021402
await this.startTransaction();
14031403
try {
1404+
// drop views
14041405
const selectViewDropsQuery = `SELECT 'DROP VIEW IF EXISTS "' || schemaname || '"."' || viewname || '" CASCADE;' as "query" ` +
14051406
`FROM "pg_views" WHERE "schemaname" IN (${schemaNamesString}) AND "viewname" NOT IN ('geography_columns', 'geometry_columns', 'raster_columns', 'raster_overviews')`;
14061407
const dropViewQueries: ObjectLiteral[] = await this.query(selectViewDropsQuery);
14071408
await Promise.all(dropViewQueries.map(q => this.query(q["query"])));
14081409

1410+
// drop materialized views
1411+
const selectMatViewDropsQuery = `SELECT 'DROP MATERIALIZED VIEW IF EXISTS "' || schemaname || '"."' || matviewname || '" CASCADE;' as "query" ` +
1412+
`FROM "pg_matviews" WHERE "schemaname" IN (${schemaNamesString})`;
1413+
const dropMatViewQueries: ObjectLiteral[] = await this.query(selectMatViewDropsQuery);
1414+
await Promise.all(dropMatViewQueries.map(q => this.query(q["query"])));
1415+
14091416
// ignore spatial_ref_sys; it's a special table supporting PostGIS
14101417
// TODO generalize this as this.driver.ignoreTables
1418+
1419+
// drop tables
14111420
const selectTableDropsQuery = `SELECT 'DROP TABLE IF EXISTS "' || schemaname || '"."' || tablename || '" CASCADE;' as "query" FROM "pg_tables" WHERE "schemaname" IN (${schemaNamesString}) AND "tablename" NOT IN ('spatial_ref_sys')`;
14121421
const dropTableQueries: ObjectLiteral[] = await this.query(selectTableDropsQuery);
14131422
await Promise.all(dropTableQueries.map(q => this.query(q["query"])));
1423+
1424+
// drop enum types
14141425
await this.dropEnumTypes(schemaNamesString);
14151426

14161427
await this.commitTransaction();
@@ -1442,14 +1453,18 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
14421453
return `("t"."schema" = '${schema}' AND "t"."name" = '${name}')`;
14431454
}).join(" OR ");
14441455

1445-
const query = `SELECT "t".*, "v"."check_option" FROM ${this.escapePath(this.getTypeormMetadataTableName())} "t" ` +
1446-
`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})` : ""}`;
1456+
const query = `SELECT "t".* FROM ${this.escapePath(this.getTypeormMetadataTableName())} "t" ` +
1457+
`INNER JOIN "pg_catalog"."pg_class" "c" ON "c"."relname" = "t"."name" ` +
1458+
`INNER JOIN "pg_namespace" "n" ON "n"."oid" = "c"."relnamespace" AND "n"."nspname" = "t"."schema" ` +
1459+
`WHERE "t"."type" IN ('VIEW', 'MATERIALIZED_VIEW') ${viewsCondition ? `AND (${viewsCondition})` : ""}`;
1460+
14471461
const dbViews = await this.query(query);
14481462
return dbViews.map((dbView: any) => {
14491463
const view = new View();
14501464
const schema = dbView["schema"] === currentSchema && !this.driver.options.schema ? undefined : dbView["schema"];
14511465
view.name = this.driver.buildTableName(dbView["name"], schema);
14521466
view.expression = dbView["value"];
1467+
view.materialized = dbView["type"] === "MATERIALIZED_VIEW";
14531468
return view;
14541469
});
14551470
}
@@ -1463,8 +1478,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
14631478
if (!tableNames || !tableNames.length)
14641479
return [];
14651480

1466-
const currentSchemaQuery = await this.query(`SELECT * FROM current_schema()`);
1467-
const currentSchema: string = currentSchemaQuery[0]["current_schema"];
1481+
const currentSchema = await this.getCurrentSchema()
14681482

14691483
const tablesCondition = tableNames.map(tableName => {
14701484
let [schema, name] = tableName.split(".");
@@ -1918,8 +1932,7 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
19181932
}
19191933

19201934
protected async insertViewDefinitionSql(view: View): Promise<Query> {
1921-
const currentSchemaQuery = await this.query(`SELECT * FROM current_schema()`);
1922-
const currentSchema = currentSchemaQuery[0]["current_schema"];
1935+
const currentSchema = await this.getCurrentSchema()
19231936
const splittedName = view.name.split(".");
19241937
let schema = this.driver.options.schema || currentSchema;
19251938
let name = view.name;
@@ -1928,11 +1941,12 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
19281941
name = splittedName[1];
19291942
}
19301943

1944+
const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW"
19311945
const expression = typeof view.expression === "string" ? view.expression.trim() : view.expression(this.connection).getQuery();
19321946
const [query, parameters] = this.connection.createQueryBuilder()
19331947
.insert()
19341948
.into(this.getTypeormMetadataTableName())
1935-
.values({ type: "VIEW", schema: schema, name: name, value: expression })
1949+
.values({ type: type, schema: schema, name: name, value: expression })
19361950
.getQueryAndParameters();
19371951

19381952
return new Query(query, parameters);
@@ -1941,29 +1955,29 @@ export class PostgresQueryRunner extends BaseQueryRunner implements QueryRunner
19411955
/**
19421956
* Builds drop view sql.
19431957
*/
1944-
protected dropViewSql(viewOrPath: View|string): Query {
1945-
return new Query(`DROP VIEW ${this.escapePath(viewOrPath)}`);
1958+
protected dropViewSql(view: View): Query {
1959+
const materializedClause = view.materialized ? "MATERIALIZED " : "";
1960+
return new Query(`DROP ${materializedClause}VIEW ${this.escapePath(view)}`);
19461961
}
19471962

19481963
/**
19491964
* Builds remove view sql.
19501965
*/
1951-
protected async deleteViewDefinitionSql(viewOrPath: View|string): Promise<Query> {
1952-
const currentSchemaQuery = await this.query(`SELECT * FROM current_schema()`);
1953-
const currentSchema = currentSchemaQuery[0]["current_schema"];
1954-
const viewName = viewOrPath instanceof View ? viewOrPath.name : viewOrPath;
1955-
const splittedName = viewName.split(".");
1966+
protected async deleteViewDefinitionSql(view: View): Promise<Query> {
1967+
const currentSchema = await this.getCurrentSchema()
1968+
const splittedName = view.name.split(".");
19561969
let schema = this.driver.options.schema || currentSchema;
1957-
let name = viewName;
1970+
let name = view.name;
19581971
if (splittedName.length === 2) {
19591972
schema = splittedName[0];
19601973
name = splittedName[1];
19611974
}
19621975

1976+
const type = view.materialized ? "MATERIALIZED_VIEW" : "VIEW"
19631977
const qb = this.connection.createQueryBuilder();
19641978
const [query, parameters] = qb.delete()
19651979
.from(this.getTypeormMetadataTableName())
1966-
.where(`${qb.escape("type")} = 'VIEW'`)
1980+
.where(`${qb.escape("type")} = :type`, { type })
19671981
.andWhere(`${qb.escape("schema")} = :schema`, { schema })
19681982
.andWhere(`${qb.escape("name")} = :name`, { name })
19691983
.getQueryAndParameters();
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import "reflect-metadata";
2+
import { Connection } from "../../../src";
3+
import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
4+
import { View } from "../../../src/schema-builder/view/View";
5+
import { expect } from "chai";
6+
7+
describe("query runner > create view", () => {
8+
9+
let connections: Connection[];
10+
before(async () => {
11+
connections = await createTestingConnections({
12+
entities: [__dirname + "/view/*{.js,.ts}"],
13+
enabledDrivers: ["postgres", "oracle"],
14+
schemaCreate: true,
15+
dropSchema: true,
16+
});
17+
});
18+
beforeEach(() => reloadTestingDatabases(connections));
19+
after(() => closeTestingConnections(connections));
20+
21+
it("should correctly create VIEW and revert creation", () => Promise.all(connections.map(async connection => {
22+
const queryRunner = connection.createQueryRunner();
23+
const view = new View({
24+
name: "new_post_view",
25+
expression: `SELECT * from "post"`
26+
});
27+
await queryRunner.createView(view);
28+
29+
let postView = await queryRunner.getView("new_post_view");
30+
expect(postView).to.be.exist;
31+
32+
await queryRunner.executeMemoryDownSql();
33+
34+
postView = await queryRunner.getView("new_post_view");
35+
expect(postView).to.be.not.exist;
36+
37+
await queryRunner.release();
38+
})));
39+
40+
it("should correctly create MATERIALIZED VIEW and revert creation", () => Promise.all(connections.map(async connection => {
41+
const queryRunner = connection.createQueryRunner();
42+
const view = new View({
43+
name: "new_post_materialized_view",
44+
expression: `SELECT * from "post"`,
45+
materialized: true
46+
});
47+
await queryRunner.createView(view);
48+
49+
let postMatView = await queryRunner.getView("new_post_materialized_view");
50+
expect(postMatView).to.be.exist;
51+
expect(postMatView!.materialized).to.be.true
52+
53+
await queryRunner.executeMemoryDownSql();
54+
55+
postMatView = await queryRunner.getView("new_post_materialized_view");
56+
expect(postMatView).to.be.not.exist;
57+
58+
await queryRunner.release();
59+
})));
60+
61+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import "reflect-metadata";
2+
import { Connection } from "../../../src";
3+
import { closeTestingConnections, createTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
4+
import { expect } from "chai";
5+
6+
describe("query runner > drop view", () => {
7+
8+
let connections: Connection[];
9+
before(async () => {
10+
connections = await createTestingConnections({
11+
entities: [__dirname + "/view/*{.js,.ts}"],
12+
enabledDrivers: ["postgres", "oracle"],
13+
schemaCreate: true,
14+
dropSchema: true,
15+
});
16+
});
17+
beforeEach(() => reloadTestingDatabases(connections));
18+
after(() => closeTestingConnections(connections));
19+
20+
it("should correctly drop VIEW and revert dropping", () => Promise.all(connections.map(async connection => {
21+
const queryRunner = connection.createQueryRunner();
22+
23+
let postView = await queryRunner.getView("post_view");
24+
await queryRunner.dropView(postView!);
25+
26+
postView = await queryRunner.getView("post_view");
27+
expect(postView).to.be.not.exist;
28+
29+
await queryRunner.executeMemoryDownSql();
30+
31+
postView = await queryRunner.getView("post_view");
32+
expect(postView).to.be.exist;
33+
34+
await queryRunner.release();
35+
})));
36+
37+
it("should correctly drop MATERIALIZED VIEW and revert dropping", () => Promise.all(connections.map(async connection => {
38+
const queryRunner = connection.createQueryRunner();
39+
40+
let postMatView = await queryRunner.getView("post_materialized_view");
41+
await queryRunner.dropView(postMatView!);
42+
43+
postMatView = await queryRunner.getView("post_materialized_view");
44+
expect(postMatView).to.be.not.exist;
45+
46+
await queryRunner.executeMemoryDownSql();
47+
48+
postMatView = await queryRunner.getView("post_materialized_view");
49+
expect(postMatView).to.be.exist;
50+
51+
await queryRunner.release();
52+
})));
53+
54+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {Column, Entity, PrimaryColumn} from "../../../../src";
2+
3+
@Entity()
4+
export class Post {
5+
6+
@PrimaryColumn()
7+
id: number;
8+
9+
@Column({ unique: true })
10+
version: number;
11+
12+
@Column({ default: "My post" })
13+
name: string;
14+
15+
@Column()
16+
text: string;
17+
18+
@Column()
19+
tag: string;
20+
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {ViewColumn, ViewEntity} from "../../../../src";
2+
3+
@ViewEntity({
4+
expression: `SELECT * FROM "post"`,
5+
materialized: true,
6+
})
7+
export class PostMaterializedView {
8+
@ViewColumn()
9+
id: number
10+
11+
@ViewColumn()
12+
type: string;
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {ViewColumn, ViewEntity} from "../../../../src";
2+
3+
@ViewEntity({
4+
expression: `SELECT * FROM "post"`
5+
})
6+
export class PostView {
7+
@ViewColumn()
8+
id: number
9+
10+
@ViewColumn()
11+
type: string;
12+
}

0 commit comments

Comments
 (0)