Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support refresh materialized view #990

Merged
merged 8 commits into from
Aug 2, 2024
Merged
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
12 changes: 12 additions & 0 deletions src/operation-node/data-type-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ const SIMPLE_COLUMN_DATA_TYPES = [
'jsonb',
'blob',
'varbinary',
'int4range',
'int4multirange',
'int8range',
'int8multirange',
'numrange',
'nummultirange',
'tsrange',
'tsmultirange',
'tstzrange',
'tstzmultirange',
'daterange',
'datemultirange',
] as const

const COLUMN_DATA_TYPE_REGEX = [
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
4 changes: 2 additions & 2 deletions src/raw-builder/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export interface Sql {
* select "first_name" from person
* ```
*
* The refefences can also include a table name:
* The references can also include a table name:
*
* ```ts
* const columnRef = 'person.first_name'
Expand All @@ -161,7 +161,7 @@ export interface Sql {
* select "person"."first_name" from person
* ```
*
* The refefences can also include a schema on supported databases:
* The references can also include a schema on supported databases:
*
* ```ts
* const columnRef = 'public.person.first_name'
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