diff --git a/src/kysely.ts b/src/kysely.ts index d260c1c90..65bf6f5c0 100644 --- a/src/kysely.ts +++ b/src/kysely.ts @@ -126,7 +126,7 @@ export class Kysely /** * Returns the {@link SchemaModule} module for building database schema. */ - get schema(): SchemaModule { + get schema(): SchemaModule { return new SchemaModule(this.#props.executor) } diff --git a/src/operation-node/create-trigger-node.ts b/src/operation-node/create-trigger-node.ts new file mode 100644 index 000000000..0f3c7b805 --- /dev/null +++ b/src/operation-node/create-trigger-node.ts @@ -0,0 +1,77 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' +import { TableNode } from './table-node.js' +import { IdentifierNode } from './identifier-node.js' +import { QueryNode } from './query-node.js' +import { TriggerEventNode } from './trigger-event-node.js' +import { TriggerOrderNode } from './trigger-order-node.js' +import { FunctionNode } from './function-node.js' + +export type TriggerTime = 'after' | 'before' | 'instead of' + +export type CreateTriggerNodeParams = Omit< + CreateTriggerNode, + 'kind' | 'name' | 'queries' +> + +export interface CreateTriggerNode extends OperationNode { + readonly kind: 'CreateTriggerNode' + readonly name: IdentifierNode + readonly queries?: ReadonlyArray + readonly function?: FunctionNode + readonly time?: TriggerTime + readonly events?: ReadonlyArray + readonly table?: TableNode + readonly orReplace?: boolean + readonly ifNotExists?: boolean + readonly when?: OperationNode + readonly temporary?: boolean + readonly forEach?: 'row' | 'statement' + readonly order?: TriggerOrderNode +} + +/** + * @internal + */ +export const CreateTriggerNode = freeze({ + is(node: OperationNode): node is CreateTriggerNode { + return node.kind === 'CreateTriggerNode' + }, + + create(name: IdentifierNode): CreateTriggerNode { + return freeze({ + kind: 'CreateTriggerNode', + name, + }) + }, + + cloneWithQuery( + createTrigger: CreateTriggerNode, + query: QueryNode + ): CreateTriggerNode { + return freeze({ + ...createTrigger, + queries: freeze([...(createTrigger.queries || []), query]), + }) + }, + + cloneWithEvent( + createTrigger: CreateTriggerNode, + event: TriggerEventNode + ): CreateTriggerNode { + return freeze({ + ...createTrigger, + events: freeze([...(createTrigger.events || []), event]), + }) + }, + + cloneWith( + createTrigger: CreateTriggerNode, + params: CreateTriggerNodeParams + ): CreateTriggerNode { + return freeze({ + ...createTrigger, + ...params, + }) + }, +}) diff --git a/src/operation-node/drop-trigger-node.ts b/src/operation-node/drop-trigger-node.ts new file mode 100644 index 000000000..37905a6d9 --- /dev/null +++ b/src/operation-node/drop-trigger-node.ts @@ -0,0 +1,40 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' +import { SchemableIdentifierNode } from './schemable-identifier-node.js' + +export type DropTriggerNodeParams = Omit< + Partial, + 'kind' | 'name' +> +export interface DropTriggerNode extends OperationNode { + readonly kind: 'DropTriggerNode' + readonly name: SchemableIdentifierNode + readonly ifExists?: boolean + readonly cascade?: boolean +} + +/** + * @internal + */ +export const DropTriggerNode = freeze({ + is(node: OperationNode): node is DropTriggerNode { + return node.kind === 'DropTriggerNode' + }, + + create(name: SchemableIdentifierNode): DropTriggerNode { + return freeze({ + kind: 'DropTriggerNode', + name, + }) + }, + + cloneWith( + dropTrigger: DropTriggerNode, + params: DropTriggerNodeParams + ): DropTriggerNode { + return freeze({ + ...dropTrigger, + ...params, + }) + }, +}) diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index 1adc18e79..c28752c35 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -88,6 +88,10 @@ import { JSONPathLegNode } from './json-path-leg-node.js' import { JSONOperatorChainNode } from './json-operator-chain-node.js' import { TupleNode } from './tuple-node.js' import { AddIndexNode } from './add-index-node.js' +import { CreateTriggerNode } from './create-trigger-node.js' +import { DropTriggerNode } from './drop-trigger-node.js' +import { TriggerEventNode } from './trigger-event-node.js' +import { TriggerOrderNode } from './trigger-order-node.js' /** * Transforms an operation node tree into another one. @@ -210,6 +214,10 @@ export class OperationNodeTransformer { JSONOperatorChainNode: this.transformJSONOperatorChain.bind(this), TupleNode: this.transformTuple.bind(this), AddIndexNode: this.transformAddIndex.bind(this), + CreateTriggerNode: this.transformCreateTrigger.bind(this), + TriggerEventNode: this.transformTriggerEvent.bind(this), + TriggerOrderNode: this.transformTriggerOrder.bind(this), + DropTriggerNode: this.transformDropTrigger.bind(this), }) transformNode(node: T): T { @@ -417,6 +425,40 @@ export class OperationNodeTransformer { }) } + protected transformCreateTrigger(node: CreateTriggerNode): CreateTriggerNode { + return requireAllProps({ + kind: 'CreateTriggerNode', + name: this.transformNode(node.name), + table: this.transformNode(node.table), + ifNotExists: node.ifNotExists, + time: node.time, + events: this.transformNodeList(node.events), + forEach: node.forEach, + orReplace: node.orReplace, + temporary: node.temporary, + queries: this.transformNodeList(node.queries), + function: this.transformNode(node.function), + when: this.transformNode(node.when), + order: this.transformNode(node.order), + }) + } + + protected transformTriggerEvent(node: TriggerEventNode): TriggerEventNode { + return requireAllProps({ + kind: 'TriggerEventNode', + event: node.event, + columns: this.transformNodeList(node.columns), + }) + } + + protected transformTriggerOrder(node: TriggerOrderNode): TriggerOrderNode { + return requireAllProps({ + kind: 'TriggerOrderNode', + order: node.order, + otherTriggerName: this.transformNode(node.otherTriggerName), + }) + } + protected transformColumnDefinition( node: ColumnDefinitionNode ): ColumnDefinitionNode { @@ -455,6 +497,15 @@ export class OperationNodeTransformer { }) } + protected transformDropTrigger(node: DropTriggerNode): DropTriggerNode { + return requireAllProps({ + kind: 'DropTriggerNode', + name: this.transformNode(node.name), + ifExists: node.ifExists, + cascade: node.cascade, + }) + } + protected transformOrderBy(node: OrderByNode): OrderByNode { return requireAllProps({ kind: 'OrderByNode', diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts index 4e48300bc..7214a1c7e 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -90,6 +90,10 @@ import { JSONPathLegNode } from './json-path-leg-node.js' import { JSONOperatorChainNode } from './json-operator-chain-node.js' import { TupleNode } from './tuple-node.js' import { AddIndexNode } from './add-index-node.js' +import { CreateTriggerNode } from './create-trigger-node.js' +import { DropTriggerNode } from './drop-trigger-node.js' +import { TriggerEventNode } from './trigger-event-node.js' +import { TriggerOrderNode } from './trigger-order-node.js' export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] @@ -187,6 +191,10 @@ export abstract class OperationNodeVisitor { JSONOperatorChainNode: this.visitJSONOperatorChain.bind(this), TupleNode: this.visitTuple.bind(this), AddIndexNode: this.visitAddIndex.bind(this), + CreateTriggerNode: this.visitCreateTrigger.bind(this), + TriggerEventNode: this.visitTriggerEvent.bind(this), + TriggerOrderNode: this.visitTriggerOrder.bind(this), + DropTriggerNode: this.visitDropTrigger.bind(this), }) protected readonly visitNode = (node: OperationNode): void => { @@ -213,9 +221,13 @@ export abstract class OperationNodeVisitor { protected abstract visitDeleteQuery(node: DeleteQueryNode): void protected abstract visitReturning(node: ReturningNode): void protected abstract visitCreateTable(node: CreateTableNode): void + protected abstract visitCreateTrigger(node: CreateTriggerNode): void + protected abstract visitTriggerEvent(node: TriggerEventNode): void + protected abstract visitTriggerOrder(node: TriggerOrderNode): void protected abstract visitAddColumn(node: AddColumnNode): void protected abstract visitColumnDefinition(node: ColumnDefinitionNode): void protected abstract visitDropTable(node: DropTableNode): void + protected abstract visitDropTrigger(node: DropTriggerNode): void protected abstract visitOrderBy(node: OrderByNode): void protected abstract visitOrderByItem(node: OrderByItemNode): void protected abstract visitGroupBy(node: GroupByNode): void diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index d40bb551c..2f3bbc898 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -86,6 +86,10 @@ export type OperationNodeKind = | 'JSONOperatorChainNode' | 'TupleNode' | 'AddIndexNode' + | 'CreateTriggerNode' + | 'TriggerEventNode' + | 'TriggerOrderNode' + | 'DropTriggerNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/operation-node/trigger-event-node.ts b/src/operation-node/trigger-event-node.ts new file mode 100644 index 000000000..c3f144bbe --- /dev/null +++ b/src/operation-node/trigger-event-node.ts @@ -0,0 +1,32 @@ +import { freeze } from '../util/object-utils.js' +import { OperationNode } from './operation-node.js' + +export type TriggerEvent = 'delete' | 'update' | 'insert' | 'truncate' + +export type TriggerEventNodeParams = Omit + +export interface TriggerEventNode extends OperationNode { + readonly kind: 'TriggerEventNode' + readonly event: TriggerEvent + readonly columns?: ReadonlyArray +} + +/** + * @internal + */ +export const TriggerEventNode = freeze({ + is(node: OperationNode): node is TriggerEventNode { + return node.kind === 'TriggerEventNode' + }, + + create( + event: TriggerEvent, + columns?: ReadonlyArray + ): TriggerEventNode { + return freeze({ + kind: 'TriggerEventNode', + event, + columns, + }) + }, +}) diff --git a/src/operation-node/trigger-order-node.ts b/src/operation-node/trigger-order-node.ts new file mode 100644 index 000000000..2e721aa7e --- /dev/null +++ b/src/operation-node/trigger-order-node.ts @@ -0,0 +1,33 @@ +import { freeze } from '../util/object-utils.js' +import { IdentifierNode } from './identifier-node.js' +import { OperationNode } from './operation-node.js' + +export type TriggerOrder = 'follows' | 'precedes' + +export type TriggerOrderNodeParams = Omit + +export interface TriggerOrderNode extends OperationNode { + readonly kind: 'TriggerOrderNode' + readonly order: TriggerOrder + readonly otherTriggerName: IdentifierNode +} + +/** + * @internal + */ +export const TriggerOrderNode = freeze({ + is(node: OperationNode): node is TriggerOrderNode { + return node.kind === 'TriggerOrderNode' + }, + + create( + order: TriggerOrder, + otherTriggerName: IdentifierNode + ): TriggerOrderNode { + return freeze({ + kind: 'TriggerOrderNode', + order, + otherTriggerName, + }) + }, +}) diff --git a/src/plugin/with-schema/with-schema-transformer.ts b/src/plugin/with-schema/with-schema-transformer.ts index a25aea9f7..bf540ffc7 100644 --- a/src/plugin/with-schema/with-schema-transformer.ts +++ b/src/plugin/with-schema/with-schema-transformer.ts @@ -21,12 +21,14 @@ const ROOT_OPERATION_NODES: Record = freeze({ CreateTableNode: true, CreateTypeNode: true, CreateViewNode: true, + CreateTriggerNode: true, DeleteQueryNode: true, DropIndexNode: true, DropSchemaNode: true, DropTableNode: true, DropTypeNode: true, DropViewNode: true, + DropTriggerNode: true, InsertQueryNode: true, RawNode: true, SelectQueryNode: true, diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 50f025709..94aedd351 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -105,6 +105,10 @@ import { JSONPathLegNode } from '../operation-node/json-path-leg-node.js' import { JSONOperatorChainNode } from '../operation-node/json-operator-chain-node.js' import { TupleNode } from '../operation-node/tuple-node.js' import { AddIndexNode } from '../operation-node/add-index-node.js' +import { CreateTriggerNode } from '../operation-node/create-trigger-node.js' +import { DropTriggerNode } from '../operation-node/drop-trigger-node.js' +import { TriggerEventNode } from '../operation-node/trigger-event-node.js' +import { TriggerOrderNode } from '../operation-node/trigger-order-node.js' export class DefaultQueryCompiler extends OperationNodeVisitor @@ -572,6 +576,83 @@ export class DefaultQueryCompiler } } + protected override visitCreateTrigger(node: CreateTriggerNode): void { + if (!node.time) throw new Error('Trigger time is required.') + if (!node.events) throw new Error('Trigger event is required.') + if (!node.table) throw new Error('Trigger table is required.') + + this.append('create ') + + if (node.temporary) { + this.append('temporary ') + } + + if (node.orReplace) { + this.append('or replace ') + } + + this.append('trigger ') + + if (node.ifNotExists) { + this.append('if not exists ') + } + + this.visitNode(node.name) + + this.append(` ${node.time} `) + + this.compileList(node.events, ' or ') + + this.append(' on ') + + this.visitNode(node.table) + + if (node.forEach) this.append(` for each ${node.forEach} `) + + if (node.when) { + this.append(' when ') + this.visitNode(node.when) + } + + if (node.order) this.visitNode(node.order) + + if (node.queries) { + this.append(' begin ') + + this.compileList(node.queries, '; ') + this.append('; ') + + this.append('end') + } else if (node.function) { + this.append(' execute function ') + this.visitNode(node.function) + } + } + + protected override visitTriggerEvent(node: TriggerEventNode): void { + this.append(`${node.event} `) + + if (node.event === 'update' && node.columns) { + this.append('of ') + this.compileList(node.columns, ', ') + } + } + + protected override visitTriggerOrder(node: TriggerOrderNode): void { + this.append(`${node.order} `) + this.visitNode(node.otherTriggerName) + } + + protected override visitDropTrigger(node: DropTriggerNode): void { + this.append('drop trigger ') + + if (node.ifExists) { + this.append('if exists ') + } + + this.visitNode(node.name) + } + protected override visitColumnDefinition(node: ColumnDefinitionNode): void { this.visitNode(node.column) diff --git a/src/query-compiler/query-compiler.ts b/src/query-compiler/query-compiler.ts index 9aa5e90c0..311bac5ce 100644 --- a/src/query-compiler/query-compiler.ts +++ b/src/query-compiler/query-compiler.ts @@ -2,11 +2,13 @@ import { AlterTableNode } from '../operation-node/alter-table-node.js' import { CreateIndexNode } from '../operation-node/create-index-node.js' import { CreateSchemaNode } from '../operation-node/create-schema-node.js' import { CreateTableNode } from '../operation-node/create-table-node.js' +import { CreateTriggerNode } from '../operation-node/create-trigger-node.js' import { CreateTypeNode } from '../operation-node/create-type-node.js' import { CreateViewNode } from '../operation-node/create-view-node.js' import { DropIndexNode } from '../operation-node/drop-index-node.js' import { DropSchemaNode } from '../operation-node/drop-schema-node.js' import { DropTableNode } from '../operation-node/drop-table-node.js' +import { DropTriggerNode } from '../operation-node/drop-trigger-node.js' import { DropTypeNode } from '../operation-node/drop-type-node.js' import { DropViewNode } from '../operation-node/drop-view-node.js' import { QueryNode } from '../operation-node/query-node.js' @@ -27,6 +29,8 @@ export type RootOperationNode = | RawNode | CreateTypeNode | DropTypeNode + | CreateTriggerNode + | DropTriggerNode /** * a `QueryCompiler` compiles a query expressed as a tree of `OperationNodes` into SQL. diff --git a/src/schema/create-trigger-builder.ts b/src/schema/create-trigger-builder.ts new file mode 100644 index 000000000..49f2c1050 --- /dev/null +++ b/src/schema/create-trigger-builder.ts @@ -0,0 +1,287 @@ +import { CreateTriggerNode } from '../operation-node/create-trigger-node.js' +import { OperationNodeSource } from '../operation-node/operation-node-source.js' +import { QueryNode } from '../operation-node/query-node.js' +import { + ComparisonOperatorExpression, + OperandValueExpressionOrList, + parseValueBinaryOperationOrExpression, +} from '../parser/binary-operation-parser.js' +import { + ReferenceExpression, + parseOrderedColumnName, + parseReferenceExpressionOrList, +} from '../parser/reference-parser.js' +import { ImmediateValueTransformer } from '../plugin/immediate-value/immediate-value-transformer.js' +import { CompiledQuery } from '../query-compiler/compiled-query.js' +import { QueryExecutor } from '../query-executor/query-executor.js' +import { Compilable } from '../util/compilable.js' +import { freeze } from '../util/object-utils.js' +import { QueryId } from '../util/query-id.js' +import { AnyColumn, AnyColumnWithTable, SqlBool } from '../util/type-utils.js' +import { preventAwait } from '../util/prevent-await.js' +import { IdentifierNode } from '../operation-node/identifier-node.js' +import { TriggerOrderNode } from '../operation-node/trigger-order-node.js' +import { + TriggerEvent, + TriggerEventNode, +} from '../operation-node/trigger-event-node.js' +import { FunctionNode } from '../operation-node/function-node.js' +import { ExpressionOrFactory } from '../parser/expression-parser.js' +import { TriggerQueryCreator } from '../trigger-query-creator.js' +import { TableNode } from '../operation-node/table-node.js' + +export type DatabaseWithOldNewTables = DB & { + old: DB[TB] + new: DB[TB] +} + +/** + * This builder can be used to create a `create table` query. + */ +export class CreateTriggerBuilder + implements OperationNodeSource, Compilable +{ + readonly #props: CreateTriggerBuilderProps + + constructor(props: CreateTriggerBuilderProps) { + this.#props = freeze(props) + } + + before(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + time: 'before', + }), + }) + } + + after(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + time: 'after', + }), + }) + } + + insteadOf(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + time: 'instead of', + }), + }) + } + + addEvent( + event: E, + columns?: E extends 'update' ? AnyColumn[] : never[] + ): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWithEvent( + this.#props.node, + TriggerEventNode.create(event, columns?.map(parseOrderedColumnName)) + ), + }) + } + + forEachRow(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + forEach: `row`, + }), + }) + } + + forEachStatement(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + forEach: `statement`, + }), + }) + } + + follows(otherTriggerName: string): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + order: TriggerOrderNode.create( + 'follows', + IdentifierNode.create(otherTriggerName) + ), + }), + }) + } + + precedes(otherTriggerName: string): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + order: TriggerOrderNode.create( + 'precedes', + IdentifierNode.create(otherTriggerName) + ), + }), + }) + } + + /** + * Specifies the table for the trigger. + */ + onTable( + table: TE, + schema?: string + ): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + table: schema + ? TableNode.createWithSchema(schema, table) + : TableNode.create(table), + }), + }) + } + + /** + * Adds the "temporary" modifier. + * + * Use this to create a temporary trigger. + */ + temporary(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + temporary: true, + }), + }) + } + + /** + * Adds the "if not exists" modifier. + * + * If the trigger already exists, no error is thrown if this method has been called. + */ + ifNotExists(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + ifNotExists: true, + }), + }) + } + + /** + * Only supported on PostgreSQL + */ + orReplace(): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + orReplace: true, + }), + }) + } + + /** + * Adds a query to the trigger. + */ + addQuery(build: QueryCreatorCallback): CreateTriggerBuilder { + const node = build( + new TriggerQueryCreator({ executor: this.#props.executor }) + ).toOperationNode() + + if (!QueryNode.is(node)) throw new Error('Must be a query node.') + + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWithQuery(this.#props.node, node), + }) + } + + function( + name: string, + args: ReadonlyArray> + ): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + function: FunctionNode.create( + name, + parseReferenceExpressionOrList(args) + ), + }), + }) + } + + when< + RE extends AnyColumnWithTable< + DatabaseWithOldNewTables, + 'old' | 'new' + > + >( + lhs: RE, + op: ComparisonOperatorExpression, + rhs: OperandValueExpressionOrList + ): CreateTriggerBuilder + when( + factory: ExpressionOrFactory< + DatabaseWithOldNewTables, + 'old' | 'new', + SqlBool + > + ): CreateTriggerBuilder + + when(...args: any[]): any { + const transformer = new ImmediateValueTransformer() + + return new CreateTriggerBuilder({ + ...this.#props, + node: CreateTriggerNode.cloneWith(this.#props.node, { + when: transformer.transformNode( + parseValueBinaryOperationOrExpression(args) + ), + }), + }) + } + + $call(func: (qb: this) => T): T { + return func(this) + } + + toOperationNode(): CreateTriggerNode { + 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 { + await this.#props.executor.executeQuery(this.compile(), this.#props.queryId) + } +} + +preventAwait( + CreateTriggerBuilder, + "don't await CreateTriggerBuilder instances directly. To execute the query you need to call `execute`" +) + +export interface CreateTriggerBuilderProps { + readonly queryId: QueryId + readonly executor: QueryExecutor + readonly node: CreateTriggerNode +} + +export type QueryCreatorCallback = ( + creator: TriggerQueryCreator +) => OperationNodeSource diff --git a/src/schema/drop-trigger-builder.ts b/src/schema/drop-trigger-builder.ts new file mode 100644 index 000000000..2670c060e --- /dev/null +++ b/src/schema/drop-trigger-builder.ts @@ -0,0 +1,71 @@ +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 { DropTriggerNode } from '../operation-node/drop-trigger-node.js' + +export class DropTriggerBuilder implements OperationNodeSource, Compilable { + readonly #props: DropTriggerBuilderProps + + constructor(props: DropTriggerBuilderProps) { + this.#props = freeze(props) + } + + ifExists(): DropTriggerBuilder { + return new DropTriggerBuilder({ + ...this.#props, + node: DropTriggerNode.cloneWith(this.#props.node, { + ifExists: true, + }), + }) + } + + cascade(): DropTriggerBuilder { + return new DropTriggerBuilder({ + ...this.#props, + node: DropTriggerNode.cloneWith(this.#props.node, { + cascade: true, + }), + }) + } + + /** + * Simply calls the provided function passing `this` as the only argument. `$call` returns + * what the provided function returns. + */ + $call(func: (qb: this) => T): T { + return func(this) + } + + toOperationNode(): DropTriggerNode { + 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 { + await this.#props.executor.executeQuery(this.compile(), this.#props.queryId) + } +} + +preventAwait( + DropTriggerBuilder, + "don't await DropTriggerBuilder instances directly. To execute the query you need to call `execute`" +) + +export interface DropTriggerBuilderProps { + readonly queryId: QueryId + readonly executor: QueryExecutor + readonly node: DropTriggerNode +} diff --git a/src/schema/schema.ts b/src/schema/schema.ts index ddf13d4d7..bca63b988 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -26,11 +26,16 @@ 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 { CreateTriggerBuilder } from './create-trigger-builder.js' +import { CreateTriggerNode } from '../operation-node/create-trigger-node.js' +import { IdentifierNode } from '../operation-node/identifier-node.js' +import { DropTriggerNode } from '../operation-node/drop-trigger-node.js' +import { DropTriggerBuilder } from './drop-trigger-builder.js' /** * Provides methods for building database schema. */ -export class SchemaModule { +export class SchemaModule { readonly #executor: QueryExecutor constructor(executor: QueryExecutor) { @@ -298,24 +303,40 @@ export class SchemaModule { }) } + createTrigger(name: string): CreateTriggerBuilder { + return new CreateTriggerBuilder({ + queryId: createQueryId(), + executor: this.#executor, + node: CreateTriggerNode.create(IdentifierNode.create(name)), + }) + } + + dropTrigger(triggerName: string): DropTriggerBuilder { + return new DropTriggerBuilder({ + queryId: createQueryId(), + executor: this.#executor, + node: DropTriggerNode.create(parseSchemableIdentifier(triggerName)), + }) + } + /** * Returns a copy of this schema module with the given plugin installed. */ - withPlugin(plugin: KyselyPlugin): SchemaModule { + withPlugin(plugin: KyselyPlugin): SchemaModule { return new SchemaModule(this.#executor.withPlugin(plugin)) } /** * Returns a copy of this schema module without any plugins. */ - withoutPlugins(): SchemaModule { + withoutPlugins(): SchemaModule { return new SchemaModule(this.#executor.withoutPlugins()) } /** * See {@link QueryCreator.withSchema} */ - withSchema(schema: string): SchemaModule { + withSchema(schema: string): SchemaModule { return new SchemaModule( this.#executor.withPluginAtFront(new WithSchemaPlugin(schema)) ) diff --git a/src/trigger-query-creator.ts b/src/trigger-query-creator.ts new file mode 100644 index 000000000..4157539a6 --- /dev/null +++ b/src/trigger-query-creator.ts @@ -0,0 +1,248 @@ +import { + SelectQueryBuilder, + createSelectQueryBuilder, +} from './query-builder/select-query-builder.js' +import { InsertQueryBuilder } from './query-builder/insert-query-builder.js' +import { DeleteQueryBuilder } from './query-builder/delete-query-builder.js' +import { UpdateQueryBuilder } from './query-builder/update-query-builder.js' +import { DeleteQueryNode } from './operation-node/delete-query-node.js' +import { InsertQueryNode } from './operation-node/insert-query-node.js' +import { SelectQueryNode } from './operation-node/select-query-node.js' +import { UpdateQueryNode } from './operation-node/update-query-node.js' +import { + parseTable, + parseTableExpression, + parseTableExpressionOrList, + TableExpression, + From, + TableExpressionOrList, + FromTables, + TableReference, + TableReferenceOrList, + ExtractTableAlias, + AnyAliasedTable, + PickTableWithAlias, +} from './parser/table-parser.js' +import { createQueryId } from './util/query-id.js' +import { freeze } from './util/object-utils.js' +import { InsertResult } from './query-builder/insert-result.js' +import { DeleteResult } from './query-builder/delete-result.js' +import { UpdateResult } from './query-builder/update-result.js' +import { KyselyPlugin } from './plugin/kysely-plugin.js' +import { + CallbackSelection, + SelectArg, + SelectCallback, + SelectExpression, + Selection, + parseSelectArg, +} from './parser/select-parser.js' +import { QueryCreatorProps } from './query-creator.js' +import { DatabaseWithOldNewTables } from './schema/create-trigger-builder.js' + +export class TriggerQueryCreator { + readonly #props: QueryCreatorProps + + constructor(props: QueryCreatorProps) { + this.#props = freeze(props) + } + + selectFrom( + from: TE[] + ): SelectQueryBuilder< + DatabaseWithOldNewTables, + ExtractTableAlias | 'new' | 'old', + {} + > + + selectFrom>( + from: TE[] + ): SelectQueryBuilder< + From, TE>, + FromTables | 'new' | 'old', + {} + > + + selectFrom( + from: TE + ): SelectQueryBuilder< + DatabaseWithOldNewTables, + ExtractTableAlias | 'new' | 'old', + {} + > + + selectFrom>( + from: TE + ): SelectQueryBuilder< + DatabaseWithOldNewTables & + PickTableWithAlias, TE>, + ExtractTableAlias | 'new' | 'old', + {} + > + + selectFrom>( + from: TE + ): SelectQueryBuilder< + From, TE>, + FromTables | 'new' | 'old', + {} + > + + selectFrom(from: TableExpressionOrList): any { + return createSelectQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: SelectQueryNode.createFrom( + parseTableExpressionOrList(from), + this.#props.withNode + ), + }) + } + + selectNoFrom>( + selections: ReadonlyArray + ): SelectQueryBuilder> + + selectNoFrom>( + callback: CB + ): SelectQueryBuilder> + + selectNoFrom>( + selection: SE + ): SelectQueryBuilder> + + selectNoFrom>( + selection: SelectArg + ): SelectQueryBuilder> { + return createSelectQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: SelectQueryNode.cloneWithSelections( + SelectQueryNode.create(this.#props.withNode), + parseSelectArg(selection as any) + ), + }) + } + + insertInto( + table: T + ): InsertQueryBuilder { + return new InsertQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: InsertQueryNode.create( + parseTable(table), + this.#props.withNode + ), + }) + } + + replaceInto( + table: T + ): InsertQueryBuilder { + return new InsertQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: InsertQueryNode.create( + parseTable(table), + this.#props.withNode, + true + ), + }) + } + + deleteFrom( + from: TR[] + ): DeleteQueryBuilder< + DatabaseWithOldNewTables, + ExtractTableAlias | 'new' | 'old', + DeleteResult + > + + deleteFrom>( + tables: TR[] + ): DeleteQueryBuilder< + From, TR>, + FromTables | 'new' | 'old', + DeleteResult + > + + deleteFrom( + from: TR + ): DeleteQueryBuilder< + DatabaseWithOldNewTables, + ExtractTableAlias | 'new' | 'old', + DeleteResult + > + + deleteFrom>( + table: TR + ): DeleteQueryBuilder< + From, TR>, + FromTables | 'new' | 'old', + DeleteResult + > + + deleteFrom(tables: TableReferenceOrList): any { + return new DeleteQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: DeleteQueryNode.create( + parseTableExpressionOrList(tables), + this.#props.withNode + ), + }) + } + + updateTable( + table: TR + ): UpdateQueryBuilder< + DB, + ExtractTableAlias, + ExtractTableAlias, + UpdateResult + > + + updateTable>( + table: TR + ): UpdateQueryBuilder< + DB & PickTableWithAlias, + ExtractTableAlias, + ExtractTableAlias, + UpdateResult + > + + updateTable>( + table: TR + ): UpdateQueryBuilder< + From, + FromTables, + FromTables, + UpdateResult + > + + updateTable>(table: TR): any { + return new UpdateQueryBuilder({ + queryId: createQueryId(), + executor: this.#props.executor, + queryNode: UpdateQueryNode.create( + parseTableExpression(table), + this.#props.withNode + ), + }) + } + + withPlugin(plugin: KyselyPlugin): TriggerQueryCreator { + return new TriggerQueryCreator({ + ...this.#props, + executor: this.#props.executor.withPlugin(plugin), + }) + } + + withoutPlugins(): TriggerQueryCreator { + return new TriggerQueryCreator({ + ...this.#props, + executor: this.#props.executor.withoutPlugins(), + }) + } +}