Skip to content

Commit

Permalink
feat: support refresh materialized view (#990)
Browse files Browse the repository at this point in the history
* feat: add postgres range types (#1086)

* feat: support refresh naterialized view

* fix tests by adding .materialized() to remove the matview

* fix failing test

* fix: References typo (#1092)

* chore: refresh-view-node.ts => refresh-materialized-view-node.ts

* chore: export node in index.ts

---------

Co-authored-by: Isak <[email protected]>
Co-authored-by: Jonathan Wu <[email protected]>
  • Loading branch information
3 people authored and igalklebanov committed Oct 24, 2024
1 parent c72db46 commit 32d17da
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export * from './schema/column-definition-builder.js'
export * from './schema/foreign-key-constraint-builder.js'
export * from './schema/alter-table-builder.js'
export * from './schema/create-view-builder.js'
export * from './schema/refresh-materialized-view-builder.js'
export * from './schema/drop-view-builder.js'
export * from './schema/alter-column-builder.js'

Expand Down Expand Up @@ -133,6 +134,7 @@ export * from './operation-node/create-schema-node.js'
export * from './operation-node/create-table-node.js'
export * from './operation-node/create-type-node.js'
export * from './operation-node/create-view-node.js'
export * from './operation-node/refresh-materialized-view-node.js'
export * from './operation-node/data-type-node.js'
export * from './operation-node/default-insert-value-node.js'
export * from './operation-node/default-value-node.js'
Expand Down
11 changes: 11 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import { CastNode } from './cast-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'

/**
* Transforms an operation node tree into another one.
Expand Down Expand Up @@ -189,6 +190,7 @@ export class OperationNodeTransformer {
DropConstraintNode: this.transformDropConstraint.bind(this),
ForeignKeyConstraintNode: this.transformForeignKeyConstraint.bind(this),
CreateViewNode: this.transformCreateView.bind(this),
RefreshMaterializedViewNode: this.transformRefreshMaterializedView.bind(this),
DropViewNode: this.transformDropView.bind(this),
GeneratedNode: this.transformGenerated.bind(this),
DefaultValueNode: this.transformDefaultValue.bind(this),
Expand Down Expand Up @@ -796,6 +798,15 @@ export class OperationNodeTransformer {
})
}

protected transformRefreshMaterializedView(node: RefreshMaterializedViewNode): RefreshMaterializedViewNode {
return requireAllProps<RefreshMaterializedViewNode>({
kind: 'RefreshMaterializedViewNode',
name: this.transformNode(node.name),
concurrently: node.concurrently,
withNoData: node.withNoData,
})
}

protected transformDropView(node: DropViewNode): DropViewNode {
return requireAllProps<DropViewNode>({
kind: 'DropViewNode',
Expand Down
3 changes: 3 additions & 0 deletions src/operation-node/operation-node-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import { CastNode } from './cast-node.js'
import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'

export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
Expand Down Expand Up @@ -166,6 +167,7 @@ export abstract class OperationNodeVisitor {
DropConstraintNode: this.visitDropConstraint.bind(this),
ForeignKeyConstraintNode: this.visitForeignKeyConstraint.bind(this),
CreateViewNode: this.visitCreateView.bind(this),
RefreshMaterializedViewNode: this.visitRefreshMaterializedView.bind(this),
DropViewNode: this.visitDropView.bind(this),
GeneratedNode: this.visitGenerated.bind(this),
DefaultValueNode: this.visitDefaultValue.bind(this),
Expand Down Expand Up @@ -277,6 +279,7 @@ export abstract class OperationNodeVisitor {
protected abstract visitPrimitiveValueList(node: PrimitiveValueListNode): void
protected abstract visitOperator(node: OperatorNode): void
protected abstract visitCreateView(node: CreateViewNode): void
protected abstract visitRefreshMaterializedView(node: RefreshMaterializedViewNode): void
protected abstract visitDropView(node: DropViewNode): void
protected abstract visitGenerated(node: GeneratedNode): void
protected abstract visitDefaultValue(node: DefaultValueNode): void
Expand Down
1 change: 1 addition & 0 deletions src/operation-node/operation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type OperationNodeKind =
| 'AddConstraintNode'
| 'DropConstraintNode'
| 'CreateViewNode'
| 'RefreshMaterializedViewNode'
| 'DropViewNode'
| 'GeneratedNode'
| 'DefaultValueNode'
Expand Down
41 changes: 41 additions & 0 deletions src/operation-node/refresh-materialized-view-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { freeze } from '../util/object-utils.js'
import { OperationNode } from './operation-node.js'
import { SchemableIdentifierNode } from './schemable-identifier-node.js'

export type RefreshMaterializedViewNodeParams = Omit<
Partial<RefreshMaterializedViewNode>,
'kind' | 'name'
>

export interface RefreshMaterializedViewNode extends OperationNode {
readonly kind: 'RefreshMaterializedViewNode'
readonly name: SchemableIdentifierNode
readonly concurrently?: boolean
readonly withNoData?: boolean
}

/**
* @internal
*/
export const RefreshMaterializedViewNode = freeze({
is(node: OperationNode): node is RefreshMaterializedViewNode {
return node.kind === 'RefreshMaterializedViewNode'
},

create(name: string): RefreshMaterializedViewNode {
return freeze({
kind: 'RefreshMaterializedViewNode',
name: SchemableIdentifierNode.create(name),
})
},

cloneWith(
createView: RefreshMaterializedViewNode,
params: RefreshMaterializedViewNodeParams,
): RefreshMaterializedViewNode {
return freeze({
...createView,
...params,
})
},
})
1 change: 1 addition & 0 deletions src/plugin/with-schema/with-schema-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const ROOT_OPERATION_NODES: Record<RootOperationNode['kind'], true> = freeze({
CreateTableNode: true,
CreateTypeNode: true,
CreateViewNode: true,
RefreshMaterializedViewNode: true,
DeleteQueryNode: true,
DropIndexNode: true,
DropSchemaNode: true,
Expand Down
17 changes: 17 additions & 0 deletions src/query-compiler/default-query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import { CastNode } from '../operation-node/cast-node.js'
import { FetchNode } from '../operation-node/fetch-node.js'
import { TopNode } from '../operation-node/top-node.js'
import { OutputNode } from '../operation-node/output-node.js'
import { RefreshMaterializedViewNode } from '../operation-node/refresh-materialized-view-node.js'

export class DefaultQueryCompiler
extends OperationNodeVisitor
Expand Down Expand Up @@ -1253,6 +1254,22 @@ export class DefaultQueryCompiler
this.visitNode(node.as)
}
}

protected override visitRefreshMaterializedView(node: RefreshMaterializedViewNode): void {
this.append('refresh materialized view ')

if (node.concurrently) {
this.append('concurrently ')
}

this.visitNode(node.name)

if (node.withNoData) {
this.append(' with no data')
} else {
this.append(' with data')
}
}

protected override visitDropView(node: DropViewNode): void {
this.append('drop ')
Expand Down
2 changes: 2 additions & 0 deletions src/query-compiler/query-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DropViewNode } from '../operation-node/drop-view-node.js'
import { MergeQueryNode } from '../operation-node/merge-query-node.js'
import { QueryNode } from '../operation-node/query-node.js'
import { RawNode } from '../operation-node/raw-node.js'
import { RefreshMaterializedViewNode } from '../operation-node/refresh-materialized-view-node.js'
import { CompiledQuery } from './compiled-query.js'

export type RootOperationNode =
Expand All @@ -20,6 +21,7 @@ export type RootOperationNode =
| CreateIndexNode
| CreateSchemaNode
| CreateViewNode
| RefreshMaterializedViewNode
| DropTableNode
| DropIndexNode
| DropSchemaNode
Expand Down
103 changes: 103 additions & 0 deletions src/schema/refresh-materialized-view-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { OperationNodeSource } from '../operation-node/operation-node-source.js'
import { CompiledQuery } from '../query-compiler/compiled-query.js'
import { Compilable } from '../util/compilable.js'
import { preventAwait } from '../util/prevent-await.js'
import { QueryExecutor } from '../query-executor/query-executor.js'
import { QueryId } from '../util/query-id.js'
import { freeze } from '../util/object-utils.js'
import { RefreshMaterializedViewNode } from '../operation-node/refresh-materialized-view-node.js'

export class RefreshMaterializedViewBuilder implements OperationNodeSource, Compilable {
readonly #props: RefreshMaterializedViewBuilderProps

constructor(props: RefreshMaterializedViewBuilderProps) {
this.#props = freeze(props)
}

/**
* Adds the "concurrently" modifier.
*
* Use this to refresh the view without locking out concurrent selects on the materialized view.
*
* WARNING!
* This cannot be used with the "with no data" modifier.
*/
concurrently(): RefreshMaterializedViewBuilder {
return new RefreshMaterializedViewBuilder({
...this.#props,
node: RefreshMaterializedViewNode.cloneWith(this.#props.node, {
concurrently: true,
withNoData: false,
}),
})
}

/**
* Adds the "with data" modifier.
*
* If specified (or defaults) the backing query is executed to provide the new data, and the materialized view is left in a scannable state
*/
withData(): RefreshMaterializedViewBuilder {
return new RefreshMaterializedViewBuilder({
...this.#props,
node: RefreshMaterializedViewNode.cloneWith(this.#props.node, {
withNoData: false,
}),
})
}

/**
* Adds the "with no data" modifier.
*
* If specified, no new data is generated and the materialized view is left in an unscannable state.
*
* WARNING!
* This cannot be used with the "concurrently" modifier.
*/
withNoData(): RefreshMaterializedViewBuilder {
return new RefreshMaterializedViewBuilder({
...this.#props,
node: RefreshMaterializedViewNode.cloneWith(this.#props.node, {
withNoData: true,
concurrently: false,
}),
})
}

/**
* Simply calls the provided function passing `this` as the only argument. `$call` returns
* what the provided function returns.
*/
$call<T>(func: (qb: this) => T): T {
return func(this)
}

toOperationNode(): RefreshMaterializedViewNode {
return this.#props.executor.transformQuery(
this.#props.node,
this.#props.queryId,
)
}

compile(): CompiledQuery {
return this.#props.executor.compileQuery(
this.toOperationNode(),
this.#props.queryId,
)
}

async execute(): Promise<void> {
await this.#props.executor.executeQuery(this.compile(), this.#props.queryId)
}
}

preventAwait(
RefreshMaterializedViewBuilder,
"don't await RefreshMaterializedViewBuilder instances directly. To execute the query you need to call `execute`",
)

export interface RefreshMaterializedViewBuilderProps {
readonly queryId: QueryId
readonly executor: QueryExecutor
readonly node: RefreshMaterializedViewNode
}
22 changes: 22 additions & 0 deletions src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { DropTypeBuilder } from './drop-type-builder.js'
import { CreateTypeNode } from '../operation-node/create-type-node.js'
import { DropTypeNode } from '../operation-node/drop-type-node.js'
import { parseSchemableIdentifier } from '../parser/identifier-parser.js'
import { RefreshMaterializedViewBuilder } from './refresh-materialized-view-builder.js'
import { RefreshMaterializedViewNode } from '../operation-node/refresh-materialized-view-node.js'

/**
* Provides methods for building database schema.
Expand Down Expand Up @@ -234,6 +236,26 @@ export class SchemaModule {
})
}

/**
* Refresh a materialized view.
*
* ### Examples
*
* ```ts
* await db.schema
* .refreshMaterializedView('my_view')
* .concurrently()
* .execute()
* ```
*/
refreshMaterializedView(viewName: string): RefreshMaterializedViewBuilder {
return new RefreshMaterializedViewBuilder({
queryId: createQueryId(),
executor: this.#executor,
node: RefreshMaterializedViewNode.create(viewName),
})
}

/**
* Drop a view.
*
Expand Down
Loading

0 comments on commit 32d17da

Please sign in to comment.