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
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-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-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-view-node.ts
koskimas marked this conversation as resolved.
Show resolved Hide resolved
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-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-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-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-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
81 changes: 81 additions & 0 deletions test/node/src/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1949,6 +1949,87 @@ for (const dialect of DIALECTS) {
}
})

describe('refresh materialized view', () => {
beforeEach(async () => {
await ctx.db.schema
.createView('materialized_dogs')
.materialized()
.as(ctx.db.selectFrom('pet').selectAll().where('species', '=', 'dog'))
.execute()
})

afterEach(async () => {
await ctx.db.schema
.dropView('materialized_dogs')
.materialized()
.ifExists()
.execute()
})

if (dialect === 'postgres') {
it('should refresh a materialized view', async () => {
const builder = ctx.db.schema
.refreshMaterializedView('materialized_dogs')

testSql(builder, dialect, {
postgres: {
sql: `refresh materialized view "materialized_dogs" with data`,
parameters: [],
},
mssql: NOT_SUPPORTED,
mysql: NOT_SUPPORTED,
sqlite: NOT_SUPPORTED,
})

await builder.execute()
})

it('should refresh a materialized view concurrently', async () => {
// concurrent refreshes require a unique index
await ctx.db.schema
.createIndex('materialized_dogs_index')
.unique()
.on('materialized_dogs')
.columns(['id'])
.execute()

const builder = ctx.db.schema
.refreshMaterializedView('materialized_dogs')
.concurrently()

testSql(builder, dialect, {
postgres: {
sql: `refresh materialized view concurrently "materialized_dogs" with data`,
parameters: [],
},
mssql: NOT_SUPPORTED,
mysql: NOT_SUPPORTED,
sqlite: NOT_SUPPORTED,
})

await builder.execute()
})

it('should refresh a materialized view with no data', async () => {
const builder = ctx.db.schema
.refreshMaterializedView('materialized_dogs')
.withNoData()

testSql(builder, dialect, {
postgres: {
sql: `refresh materialized view "materialized_dogs" with no data`,
parameters: [],
},
mssql: NOT_SUPPORTED,
mysql: NOT_SUPPORTED,
sqlite: NOT_SUPPORTED,
})

await builder.execute()
})
}
})

describe('drop view', () => {
beforeEach(async () => {
await ctx.db.schema
Expand Down
Loading