Skip to content

Commit

Permalink
feat: index support for materialized views of PostgreSQL (#9414)
Browse files Browse the repository at this point in the history
* feat: Added new indices attribute to View

* feat: Added view indices methods

Such as dropViewIndex, addViewIndices, addViewIndex

Added "View" type in some parameters of methods

* feat: Added view indices support when creating new indices and dropping old indices

* ref: Renamed "table" to "view" in log when dropping view index

* feat: changed order of schema sync operations

To create a new view index, a view has to be created first.

* feat: removed unreachable code

A view object don't have its indices when creation. The indices are added to the view through the createViewIndex method.

* feat: Added view when returning TableIndex

* feat: Added view paths as argument in getViews on log method

* feat: Created createViewIndexSql

This method reuses code from createIndexSql, but eliminates the isSpatial part, because a viewColumn doesn't support this attribute.

* fix: Added missing columns const to createViewIndexSql

* feat: Removed isSpatial attribute when returning TableIndex

* feat: Added unit tests

* fix: Dropped current index to leave unique index on indices array

There was a bug that when asserting the unique index, it would compare with the previous index, even when explicitly selecting the unique index in the indices array.

* ref: lint files

* feat: added "postgres" in enabledDrivers attribute

This is to enable only PostgreSQL for the tests

* feat: added doc for materialized view indices

* ref: lint files

* feat: Added new method to create mat. view indices

This new method goes after creating the views. Aditionally, the views are now created at the end (as it was before)

* ref: prettify files

* feat: revamped tests

Replaced previous unit tests with more significant ones
  • Loading branch information
rodzalo committed Dec 3, 2022
1 parent f07fb2c commit 1cb738a
Show file tree
Hide file tree
Showing 10 changed files with 545 additions and 6 deletions.
37 changes: 37 additions & 0 deletions docs/view-entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,43 @@ List of available options in `ViewColumnOptions`:
- `name: string` - Column name in the database view.
- `transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType }` - Used to unmarshal properties of arbitrary type `DatabaseType` supported by the database into a type `EntityType`. Arrays of transformers are also supported and are applied in reverse order when reading. Note that because database views are read-only, `transformer.to(value)` will never be used.

## Materialized View Indices

There's support for creation of indices for materialized views if using `PostgreSQL`.

```typescript
@ViewEntity({
materialized: true,
expression: (dataSource: DataSource) =>
dataSource
.createQueryBuilder()
.select("post.id", "id")
.addSelect("post.name", "name")
.addSelect("category.name", "categoryName")
.from(Post, "post")
.leftJoin(Category, "category", "category.id = post.categoryId"),
})
export class PostCategory {
@ViewColumn()
id: number

@Index()
@ViewColumn()
name: string

@Index("catname-idx")
@ViewColumn()
categoryName: string
}
```
However, `unique` is currently the only supported option for indices in materialized views. The rest of the indices options will be ignored.

````typescript
@Index("name-idx", { unique: true })
@ViewColumn()
name: string
````

## Complete example

Let create two entities and a view containing aggregated data from these entities:
Expand Down
133 changes: 132 additions & 1 deletion src/driver/postgres/PostgresQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2921,6 +2921,26 @@ export class PostgresQueryRunner
table.addIndex(index)
}

/**
* Create a new view index.
*/
async createViewIndex(
viewOrName: View | string,
index: TableIndex,
): Promise<void> {
const view = InstanceChecker.isView(viewOrName)
? viewOrName
: await this.getCachedView(viewOrName)

// new index may be passed without name. In this case we generate index name manually.
if (!index.name) index.name = this.generateIndexName(view, index)

const up = this.createViewIndexSql(view, index)
const down = this.dropIndexSql(view, index)
await this.executeQueries(up, down)
view.addIndex(index)
}

/**
* Creates a new indices
*/
Expand All @@ -2933,6 +2953,18 @@ export class PostgresQueryRunner
}
}

/**
* Creates new view indices
*/
async createViewIndices(
viewOrName: View | string,
indices: TableIndex[],
): Promise<void> {
for (const index of indices) {
await this.createViewIndex(viewOrName, index)
}
}

/**
* Drops an index from the table.
*/
Expand All @@ -2959,6 +2991,32 @@ export class PostgresQueryRunner
table.removeIndex(index)
}

/**
* Drops an index from a view.
*/
async dropViewIndex(
viewOrName: View | string,
indexOrName: TableIndex | string,
): Promise<void> {
const view = InstanceChecker.isView(viewOrName)
? viewOrName
: await this.getCachedView(viewOrName)
const index = InstanceChecker.isTableIndex(indexOrName)
? indexOrName
: view.indices.find((i) => i.name === indexOrName)
if (!index)
throw new TypeORMError(
`Supplied index ${indexOrName} was not found in view ${view.name}`,
)
// old index may be passed without name. In this case we generate index name manually.
if (!index.name) index.name = this.generateIndexName(view, index)

const up = this.dropIndexSql(view, index)
const down = this.createViewIndexSql(view, index)
await this.executeQueries(up, down)
view.removeIndex(index)
}

/**
* Drops an indices from the table.
*/
Expand Down Expand Up @@ -3087,6 +3145,34 @@ export class PostgresQueryRunner
})
.join(" OR ")

const constraintsCondition =
viewNames.length === 0
? "1=1"
: viewNames
.map((tableName) => this.driver.parseTableName(tableName))
.map(({ schema, tableName }) => {
if (!schema) {
schema =
this.driver.options.schema || currentSchema
}

return `("ns"."nspname" = '${schema}' AND "t"."relname" = '${tableName}')`
})
.join(" OR ")

const indicesSql =
`SELECT "ns"."nspname" AS "table_schema", "t"."relname" AS "table_name", "i"."relname" AS "constraint_name", "a"."attname" AS "column_name", ` +
`CASE "ix"."indisunique" WHEN 't' THEN 'TRUE' ELSE'FALSE' END AS "is_unique", pg_get_expr("ix"."indpred", "ix"."indrelid") AS "condition", ` +
`"types"."typname" AS "type_name" ` +
`FROM "pg_class" "t" ` +
`INNER JOIN "pg_index" "ix" ON "ix"."indrelid" = "t"."oid" ` +
`INNER JOIN "pg_attribute" "a" ON "a"."attrelid" = "t"."oid" AND "a"."attnum" = ANY ("ix"."indkey") ` +
`INNER JOIN "pg_namespace" "ns" ON "ns"."oid" = "t"."relnamespace" ` +
`INNER JOIN "pg_class" "i" ON "i"."oid" = "ix"."indexrelid" ` +
`INNER JOIN "pg_type" "types" ON "types"."oid" = "a"."atttypid" ` +
`LEFT JOIN "pg_constraint" "cnst" ON "cnst"."conname" = "i"."relname" ` +
`WHERE "t"."relkind" IN ('m') AND "cnst"."contype" IS NULL AND (${constraintsCondition})`

const query =
`SELECT "t".* FROM ${this.escapePath(
this.getTypeormMetadataTableName(),
Expand All @@ -3098,7 +3184,18 @@ export class PostgresQueryRunner
}') ${viewsCondition ? `AND (${viewsCondition})` : ""}`

const dbViews = await this.query(query)
const dbIndices: ObjectLiteral[] = await this.query(indicesSql)
return dbViews.map((dbView: any) => {
// find index constraints of table, group them by constraint name and build TableIndex.
const tableIndexConstraints = OrmUtils.uniq(
dbIndices.filter((dbIndex) => {
return (
dbIndex["table_name"] === dbView["name"] &&
dbIndex["table_schema"] === dbView["schema"]
)
}),
(dbIndex) => dbIndex["constraint_name"],
)
const view = new View()
const schema =
dbView["schema"] === currentSchema &&
Expand All @@ -3111,6 +3208,24 @@ export class PostgresQueryRunner
view.expression = dbView["value"]
view.materialized =
dbView["type"] === MetadataTableType.MATERIALIZED_VIEW
view.indices = tableIndexConstraints.map((constraint) => {
const indices = dbIndices.filter((index) => {
return (
index["table_schema"] === constraint["table_schema"] &&
index["table_name"] === constraint["table_name"] &&
index["constraint_name"] ===
constraint["constraint_name"]
)
})
return new TableIndex(<TableIndexOptions>{
view: view,
name: constraint["constraint_name"],
columnNames: indices.map((i) => i["column_name"]),
isUnique: constraint["is_unique"] === "TRUE",
where: constraint["condition"],
isFulltext: false,
})
})
return view
})
}
Expand Down Expand Up @@ -4163,11 +4278,27 @@ export class PostgresQueryRunner
)
}

/**
* Builds create view index sql.
*/
protected createViewIndexSql(view: View, index: TableIndex): Query {
const columns = index.columnNames
.map((columnName) => `"${columnName}"`)
.join(", ")
return new Query(
`CREATE ${index.isUnique ? "UNIQUE " : ""}INDEX "${
index.name
}" ON ${this.escapePath(view)} (${columns}) ${
index.where ? "WHERE " + index.where : ""
}`,
)
}

/**
* Builds drop index sql.
*/
protected dropIndexSql(
table: Table,
table: Table | View,
indexOrName: TableIndex | string,
): Query {
let indexName = InstanceChecker.isTableIndex(indexOrName)
Expand Down
3 changes: 2 additions & 1 deletion src/naming-strategy/NamingStrategyInterface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Table } from "../schema-builder/table/Table"
import { View } from "../schema-builder/view/View"

/**
* Naming strategy defines how auto-generated names for such things like table name, or table column gonna be
Expand Down Expand Up @@ -84,7 +85,7 @@ export interface NamingStrategyInterface {
* Gets the name of the index - simple and compose index.
*/
indexName(
tableOrName: Table | string,
tableOrName: Table | View | string,
columns: string[],
where?: string,
): string
Expand Down
5 changes: 4 additions & 1 deletion src/query-runner/BaseQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,10 @@ export abstract class BaseQueryRunner {
/**
* Generated an index name for a table and index
*/
protected generateIndexName(table: Table, index: TableIndex): string {
protected generateIndexName(
table: Table | View,
index: TableIndex,
): string {
// new index may be passed without name. In this case we generate index name manually.
return this.connection.namingStrategy.indexName(
table,
Expand Down

0 comments on commit 1cb738a

Please sign in to comment.