diff --git a/package.json b/package.json index 63ad7dfa2..df5a0afe9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kysely", - "version": "0.19.9", + "version": "0.19.10", "description": "Type safe SQL query builder", "repository": { "type": "git", diff --git a/src/operation-node/create-type-node.ts b/src/operation-node/create-type-node.ts new file mode 100644 index 000000000..d2d4970b4 --- /dev/null +++ b/src/operation-node/create-type-node.ts @@ -0,0 +1,38 @@ +import { freeze } from '../util/object-utils.js' +import { IdentifierNode } from './identifier-node.js' +import { OperationNode } from './operation-node.js' +import { ValueListNode } from './value-list-node.js' +import { ValueNode } from './value-node.js' + +export type CreateTypeNodeParams = Omit, 'kind'> + +export interface CreateTypeNode extends OperationNode { + readonly kind: 'CreateTypeNode' + readonly name: IdentifierNode + readonly enum?: ValueListNode +} + +/** + * @internal + */ +export const CreateTypeNode = freeze({ + is(node: OperationNode): node is CreateTypeNode { + return node.kind === 'CreateTypeNode' + }, + + create(name: string): CreateTypeNode { + return freeze({ + kind: 'CreateTypeNode', + name: IdentifierNode.create(name), + }) + }, + + cloneWithEnum(createType: CreateTypeNode, values: string[]): CreateTypeNode { + return freeze({ + ...createType, + enum: ValueListNode.create( + values.map((value) => ValueNode.createImmediate(value)) + ), + }) + }, +}) diff --git a/src/operation-node/drop-type-node.ts b/src/operation-node/drop-type-node.ts new file mode 100644 index 000000000..3e77bc972 --- /dev/null +++ b/src/operation-node/drop-type-node.ts @@ -0,0 +1,34 @@ +import { freeze } from '../util/object-utils.js' +import { IdentifierNode } from './identifier-node.js' +import { OperationNode } from './operation-node.js' + +export type DropTypeNodeParams = Omit, 'kind' | 'name'> + +export interface DropTypeNode extends OperationNode { + readonly kind: 'DropTypeNode' + readonly name: IdentifierNode + readonly ifExists?: boolean +} + +/** + * @internal + */ +export const DropTypeNode = freeze({ + is(node: OperationNode): node is DropTypeNode { + return node.kind === 'DropTypeNode' + }, + + create(name: string): DropTypeNode { + return freeze({ + kind: 'DropTypeNode', + name: IdentifierNode.create(name), + }) + }, + + cloneWith(dropType: DropTypeNode, params: DropTypeNodeParams): DropTypeNode { + return freeze({ + ...dropType, + ...params, + }) + }, +}) diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index a2ecf019e..b01693c98 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -68,6 +68,8 @@ import { DefaultValueNode } from './default-value-node.js' import { OnNode } from './on-node.js' import { ValuesNode } from './values-node.js' import { SelectModifierNode } from './select-modifier-node.js' +import { CreateTypeNode } from './create-type-node.js' +import { DropTypeNode } from './drop-type-node.js' /** * Transforms an operation node tree into another one. @@ -170,6 +172,8 @@ export class OperationNodeTransformer { OnNode: this.transformOn.bind(this), ValuesNode: this.transformValues.bind(this), SelectModifierNode: this.transformSelectModifier.bind(this), + CreateTypeNode: this.transformCreateType.bind(this), + DropTypeNode: this.transformDropType.bind(this), }) readonly transformNode = < @@ -773,6 +777,22 @@ export class OperationNodeTransformer { }) } + protected transformCreateType(node: CreateTypeNode): CreateTypeNode { + return requireAllProps({ + kind: 'CreateTypeNode', + name: this.transformNode(node.name), + enum: this.transformNode(node.enum), + }) + } + + protected transformDropType(node: DropTypeNode): DropTypeNode { + return requireAllProps({ + kind: 'DropTypeNode', + name: this.transformNode(node.name), + ifExists: node.ifExists, + }) + } + protected transformDataType(node: DataTypeNode): DataTypeNode { // An Object.freezed leaf node. No need to clone. return node diff --git a/src/operation-node/operation-node-visitor.ts b/src/operation-node/operation-node-visitor.ts index 8cbbb4f2b..02784f413 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -70,6 +70,8 @@ import { freeze } from '../util/object-utils.js' import { OnNode } from './on-node.js' import { ValuesNode } from './values-node.js' import { SelectModifierNode } from './select-modifier-node.js' +import { CreateTypeNode } from './create-type-node.js' +import { DropTypeNode } from './drop-type-node.js' export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] @@ -147,6 +149,8 @@ export abstract class OperationNodeVisitor { OnNode: this.visitOn.bind(this), ValuesNode: this.visitValues.bind(this), SelectModifierNode: this.visitSelectModifier.bind(this), + CreateTypeNode: this.visitCreateType.bind(this), + DropTypeNode: this.visitDropType.bind(this), }) protected readonly visitNode = (node: OperationNode): void => { @@ -230,4 +234,6 @@ export abstract class OperationNodeVisitor { protected abstract visitOn(node: OnNode): void protected abstract visitValues(node: ValuesNode): void protected abstract visitSelectModifier(node: SelectModifierNode): void + protected abstract visitCreateType(node: CreateTypeNode): void + protected abstract visitDropType(node: DropTypeNode): void } diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index 48ba3e495..0875df9b9 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -66,6 +66,8 @@ export type OperationNodeKind = | 'ValuesNode' | 'CommonTableExpressionNameNode' | 'SelectModifierNode' + | 'CreateTypeNode' + | 'DropTypeNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/plugin/with-schema/with-schema-transformer.ts b/src/plugin/with-schema/with-schema-transformer.ts index c2632c127..e1573d700 100644 --- a/src/plugin/with-schema/with-schema-transformer.ts +++ b/src/plugin/with-schema/with-schema-transformer.ts @@ -2,10 +2,12 @@ import { AliasNode } from '../../operation-node/alias-node.js' import { AlterTableNode } from '../../operation-node/alter-table-node.js' import { CreateIndexNode } from '../../operation-node/create-index-node.js' import { CreateTableNode } from '../../operation-node/create-table-node.js' +import { CreateTypeNode } from '../../operation-node/create-type-node.js' import { CreateViewNode } from '../../operation-node/create-view-node.js' import { DeleteQueryNode } from '../../operation-node/delete-query-node.js' import { DropIndexNode } from '../../operation-node/drop-index-node.js' import { DropTableNode } from '../../operation-node/drop-table-node.js' +import { DropTypeNode } from '../../operation-node/drop-type-node.js' import { DropViewNode } from '../../operation-node/drop-view-node.js' import { InsertQueryNode } from '../../operation-node/insert-query-node.js' import { JoinNode } from '../../operation-node/join-node.js' @@ -78,6 +80,14 @@ export class WithSchemaTransformer extends OperationNodeTransformer { return this.#transformRoot(node, (node) => super.transformDropView(node)) } + protected override transformCreateType(node: CreateTypeNode): CreateTypeNode { + return this.#transformRoot(node, (node) => super.transformCreateType(node)) + } + + protected override transformDropType(node: DropTypeNode): DropTypeNode { + return this.#transformRoot(node, (node) => super.transformDropType(node)) + } + protected override transformAlterTable(node: AlterTableNode): AlterTableNode { return this.#transformRoot(node, (node) => super.transformAlterTable(node)) } diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 2dadebcc2..5b92e365f 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -83,6 +83,8 @@ import { SelectModifier, SelectModifierNode, } from '../operation-node/select-modifier-node.js' +import { CreateTypeNode } from '../operation-node/create-type-node.js' +import { DropTypeNode } from '../operation-node/drop-type-node.js' export class DefaultQueryCompiler extends OperationNodeVisitor @@ -1098,6 +1100,26 @@ export class DefaultQueryCompiler } } + protected override visitCreateType(node: CreateTypeNode): void { + this.append('create type ') + this.visitNode(node.name) + + if (node.enum) { + this.append(' as enum ') + this.visitNode(node.enum) + } + } + + protected override visitDropType(node: DropTypeNode): void { + this.append('drop type ') + + if (node.ifExists) { + this.append('if exists ') + } + + this.visitNode(node.name) + } + protected append(str: string): void { this.#sql += str } diff --git a/src/query-compiler/query-compiler.ts b/src/query-compiler/query-compiler.ts index e3b373542..9aa5e90c0 100644 --- a/src/query-compiler/query-compiler.ts +++ b/src/query-compiler/query-compiler.ts @@ -2,10 +2,12 @@ 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 { 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 { 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' import { RawNode } from '../operation-node/raw-node.js' @@ -23,6 +25,8 @@ export type RootOperationNode = | DropViewNode | AlterTableNode | RawNode + | CreateTypeNode + | DropTypeNode /** * a `QueryCompiler` compiles a query expressed as a tree of `OperationNodes` into SQL. diff --git a/src/schema/create-type-builder.ts b/src/schema/create-type-builder.ts new file mode 100644 index 000000000..48c2ec669 --- /dev/null +++ b/src/schema/create-type-builder.ts @@ -0,0 +1,55 @@ +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 { CreateTypeNode } from '../operation-node/create-type-node.js' + +export class CreateTypeBuilder implements OperationNodeSource, Compilable { + readonly #props: CreateTypeBuilderProps + + constructor(props: CreateTypeBuilderProps) { + this.#props = freeze(props) + } + + toOperationNode(): CreateTypeNode { + return this.#props.executor.transformQuery( + this.#props.createTypeNode, + this.#props.queryId + ) + } + + asEnum(values: string[]): CreateTypeBuilder { + return new CreateTypeBuilder({ + ...this.#props, + createTypeNode: CreateTypeNode.cloneWithEnum( + this.#props.createTypeNode, + values + ), + }) + } + + 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( + CreateTypeBuilder, + "don't await CreateTypeBuilder instances directly. To execute the query you need to call `execute`" +) + +export interface CreateTypeBuilderProps { + readonly queryId: QueryId + readonly executor: QueryExecutor + readonly createTypeNode: CreateTypeNode +} diff --git a/src/schema/drop-type-builder.ts b/src/schema/drop-type-builder.ts new file mode 100644 index 000000000..c75185f7b --- /dev/null +++ b/src/schema/drop-type-builder.ts @@ -0,0 +1,54 @@ +import { DropTypeNode } from '../operation-node/drop-type-node.js' +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' + +export class DropTypeBuilder implements OperationNodeSource, Compilable { + readonly #props: DropTypeBuilderProps + + constructor(props: DropTypeBuilderProps) { + this.#props = freeze(props) + } + + ifExists(): DropTypeBuilder { + return new DropTypeBuilder({ + ...this.#props, + dropTypeNode: DropTypeNode.cloneWith(this.#props.dropTypeNode, { + ifExists: true, + }), + }) + } + + toOperationNode(): DropTypeNode { + return this.#props.executor.transformQuery( + this.#props.dropTypeNode, + 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( + DropTypeBuilder, + "don't await DropTypeBuilder instances directly. To execute the query you need to call `execute`" +) + +export interface DropTypeBuilderProps { + readonly queryId: QueryId + readonly executor: QueryExecutor + readonly dropTypeNode: DropTypeNode +} diff --git a/src/schema/schema.ts b/src/schema/schema.ts index 972be5614..c9d982c3d 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -21,6 +21,10 @@ import { CreateViewNode } from '../operation-node/create-view-node.js' import { DropViewBuilder } from './drop-view-builder.js' import { DropViewNode } from '../operation-node/drop-view-node.js' import { KyselyPlugin } from '../plugin/kysely-plugin.js' +import { CreateTypeBuilder } from './create-type-builder.js' +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' /** * Provides methods for building database schema. @@ -250,6 +254,50 @@ export class SchemaModule { }) } + /** + * Create a new type. + * + * Only some dialects like PostgreSQL have user-defined types. + * + * ### Examples + * + * ```ts + * await db.schema + * .createType('species') + * .asEnum(['dog', 'cat', 'frog']) + * .execute() + * ``` + */ + createType(viewName: string): CreateTypeBuilder { + return new CreateTypeBuilder({ + queryId: createQueryId(), + executor: this.#executor, + createTypeNode: CreateTypeNode.create(viewName), + }) + } + + /** + * Drop a type. + * + * Only some dialects like PostgreSQL have user-defined types. + * + * ### Examples + * + * ```ts + * await db.schema + * .dropType('species') + * .ifExists() + * .execute() + * ``` + */ + dropType(viewName: string): DropTypeBuilder { + return new DropTypeBuilder({ + queryId: createQueryId(), + executor: this.#executor, + dropTypeNode: DropTypeNode.create(viewName), + }) + } + /** * Returns a copy of this schema module with the given plugin installed. */ diff --git a/test/node/src/schema.test.ts b/test/node/src/schema.test.ts index 95098a56f..3ee279c9a 100644 --- a/test/node/src/schema.test.ts +++ b/test/node/src/schema.test.ts @@ -1112,7 +1112,7 @@ for (const dialect of BUILT_IN_DIALECTS) { beforeEach(cleanup) afterEach(cleanup) - it('should create a schema', async () => { + it('should drop a schema', async () => { await ctx.db.schema.createSchema('pets').execute() const builder = ctx.db.schema.dropSchema('pets') @@ -1156,6 +1156,77 @@ for (const dialect of BUILT_IN_DIALECTS) { } }) + describe('create type', () => { + if (dialect === 'postgres') { + beforeEach(cleanup) + afterEach(cleanup) + + it('should create an enum type', async () => { + const builder = ctx.db.schema + .createType('species') + .asEnum(['cat', 'dog', 'frog']) + + testSql(builder, dialect, { + postgres: { + sql: `create type "species" as enum ('cat', 'dog', 'frog')`, + parameters: [], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await builder.execute() + }) + } + + async function cleanup() { + await ctx.db.schema.dropType('species').ifExists().execute() + } + }) + + describe('drop type', () => { + if (dialect === 'postgres') { + beforeEach(cleanup) + afterEach(cleanup) + + it('should drop a type', async () => { + await ctx.db.schema.createType('species').execute() + + const builder = ctx.db.schema.dropType('species') + + testSql(builder, dialect, { + postgres: { + sql: `drop type "species"`, + parameters: [], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await builder.execute() + }) + + it('should drop a type if exists', async () => { + const builder = ctx.db.schema.dropType('species').ifExists() + + testSql(builder, dialect, { + postgres: { + sql: `drop type if exists "species"`, + parameters: [], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await builder.execute() + }) + } + + async function cleanup() { + await ctx.db.schema.dropType('species').ifExists().execute() + } + }) + describe('alter table', () => { beforeEach(async () => { await ctx.db.schema