diff --git a/src/operation-node/add-index-node.ts b/src/operation-node/add-index-node.ts new file mode 100644 index 000000000..ff95e2579 --- /dev/null +++ b/src/operation-node/add-index-node.ts @@ -0,0 +1,45 @@ +import { freeze } from '../util/object-utils.js' +import { IdentifierNode } from './identifier-node.js' +import { OperationNode } from './operation-node.js' +import { RawNode } from './raw-node.js' + +export type AddIndexNodeProps = Omit + +export interface AddIndexNode extends OperationNode { + readonly kind: 'AddIndexNode' + readonly name: IdentifierNode + readonly columns?: OperationNode[] + readonly unique?: boolean + readonly using?: RawNode + readonly ifNotExists?: boolean +} + +/** + * @internal + */ +export const AddIndexNode = freeze({ + is(node: OperationNode): node is AddIndexNode { + return node.kind === 'AddIndexNode' + }, + + create(name: string): AddIndexNode { + return freeze({ + kind: 'AddIndexNode', + name: IdentifierNode.create(name) + }) + }, + + cloneWith(node: AddIndexNode, props: AddIndexNodeProps): AddIndexNode { + return freeze({ + ...node, + ...props, + }) + }, + + cloneWithColumns(node: AddIndexNode, columns: OperationNode[]): AddIndexNode { + return freeze({ + ...node, + columns: [...(node.columns || []), ...columns], + }) + }, +}) \ No newline at end of file diff --git a/src/operation-node/alter-table-node.ts b/src/operation-node/alter-table-node.ts index fdb090eef..6573cabf7 100644 --- a/src/operation-node/alter-table-node.ts +++ b/src/operation-node/alter-table-node.ts @@ -9,10 +9,12 @@ import { AlterColumnNode } from './alter-column-node.js' import { AddConstraintNode } from './add-constraint-node.js' import { DropConstraintNode } from './drop-constraint-node.js' import { ModifyColumnNode } from './modify-column-node.js' +import { DropIndexNode } from './drop-index-node.js' +import { AddIndexNode } from './add-index-node.js' export type AlterTableNodeTableProps = Pick< AlterTableNode, - 'renameTo' | 'setSchema' | 'addConstraint' | 'dropConstraint' + 'renameTo' | 'setSchema' | 'addConstraint' | 'dropConstraint' | 'addIndex' | 'dropIndex' > export type AlterTableColumnAlterationNode = @@ -30,6 +32,8 @@ export interface AlterTableNode extends OperationNode { readonly columnAlterations?: ReadonlyArray readonly addConstraint?: AddConstraintNode readonly dropConstraint?: DropConstraintNode + readonly addIndex?: AddIndexNode + readonly dropIndex?: DropIndexNode } /** diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index bec227d06..1adc18e79 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -87,6 +87,7 @@ import { JSONPathNode } from './json-path-node.js' 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' /** * Transforms an operation node tree into another one. @@ -208,6 +209,7 @@ export class OperationNodeTransformer { JSONPathLegNode: this.transformJSONPathLeg.bind(this), JSONOperatorChainNode: this.transformJSONOperatorChain.bind(this), TupleNode: this.transformTuple.bind(this), + AddIndexNode: this.transformAddIndex.bind(this), }) transformNode(node: T): T { @@ -696,6 +698,8 @@ export class OperationNodeTransformer { columnAlterations: this.transformNodeList(node.columnAlterations), addConstraint: this.transformNode(node.addConstraint), dropConstraint: this.transformNode(node.dropConstraint), + addIndex: this.transformNode(node.addIndex), + dropIndex: this.transformNode(node.dropIndex), }) } @@ -976,6 +980,17 @@ export class OperationNodeTransformer { }) } + protected transformAddIndex(node: AddIndexNode): AddIndexNode { + return requireAllProps({ + kind: 'AddIndexNode', + name: this.transformNode(node.name), + columns: this.transformNodeList(node.columns), + unique: node.unique, + using: this.transformNode(node.using), + ifNotExists: node.ifNotExists, + }) + } + 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 23ec54091..4e48300bc 100644 --- a/src/operation-node/operation-node-visitor.ts +++ b/src/operation-node/operation-node-visitor.ts @@ -89,6 +89,7 @@ import { JSONPathNode } from './json-path-node.js' 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' export abstract class OperationNodeVisitor { protected readonly nodeStack: OperationNode[] = [] @@ -185,6 +186,7 @@ export abstract class OperationNodeVisitor { JSONPathLegNode: this.visitJSONPathLeg.bind(this), JSONOperatorChainNode: this.visitJSONOperatorChain.bind(this), TupleNode: this.visitTuple.bind(this), + AddIndexNode: this.visitAddIndex.bind(this), }) protected readonly visitNode = (node: OperationNode): void => { @@ -289,4 +291,5 @@ export abstract class OperationNodeVisitor { protected abstract visitJSONPathLeg(node: JSONPathLegNode): void protected abstract visitJSONOperatorChain(node: JSONOperatorChainNode): void protected abstract visitTuple(node: TupleNode): void + protected abstract visitAddIndex(node: AddIndexNode): void } diff --git a/src/operation-node/operation-node.ts b/src/operation-node/operation-node.ts index d9511bf41..d40bb551c 100644 --- a/src/operation-node/operation-node.ts +++ b/src/operation-node/operation-node.ts @@ -85,6 +85,7 @@ export type OperationNodeKind = | 'JSONPathLegNode' | 'JSONOperatorChainNode' | 'TupleNode' + | 'AddIndexNode' export interface OperationNode { readonly kind: OperationNodeKind diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index 991e6609f..50f025709 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -104,6 +104,7 @@ import { JSONPathNode } from '../operation-node/json-path-node.js' 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' export class DefaultQueryCompiler extends OperationNodeVisitor @@ -1023,6 +1024,14 @@ export class DefaultQueryCompiler if (node.columnAlterations) { this.compileColumnAlterations(node.columnAlterations) } + + if (node.addIndex) { + this.visitNode(node.addIndex) + } + + if (node.dropIndex) { + this.visitNode(node.dropIndex) + } } protected override visitAddColumn(node: AddColumnNode): void { @@ -1424,6 +1433,29 @@ export class DefaultQueryCompiler } } + protected override visitAddIndex(node: AddIndexNode): void { + this.append('add ') + + if (node.unique) { + this.append('unique ') + } + + this.append('index ') + + this.visitNode(node.name) + + if (node.columns) { + this.append(' (') + this.compileList(node.columns) + this.append(')') + } + + if (node.using) { + this.append(' using ') + this.visitNode(node.using) + } + } + protected append(str: string): void { this.#sql += str } diff --git a/src/schema/alter-table-add-index-builder.ts b/src/schema/alter-table-add-index-builder.ts new file mode 100644 index 000000000..96207b1fc --- /dev/null +++ b/src/schema/alter-table-add-index-builder.ts @@ -0,0 +1,205 @@ +import { Expression } from '../expression/expression.js' +import { AddIndexNode } from '../operation-node/add-index-node.js' +import { AlterTableNode } from '../operation-node/alter-table-node.js' +import { IndexType } from '../operation-node/create-index-node.js' +import { OperationNodeSource } from '../operation-node/operation-node-source.js' +import { RawNode } from '../operation-node/raw-node.js' +import { OrderedColumnName, parseOrderedColumnName } from '../parser/reference-parser.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 { preventAwait } from '../util/prevent-await.js' +import { QueryId } from '../util/query-id.js' + +export class AlterTableAddIndexBuilder + implements + OperationNodeSource, + Compilable +{ + readonly #props: AlterTableAddIndexBuilderProps + + constructor(props: AlterTableAddIndexBuilderProps) { + this.#props = freeze(props) + } + + /** + * Makes the index unique. + */ + unique(): AlterTableAddIndexBuilder { + return new AlterTableAddIndexBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + addIndex: AddIndexNode.cloneWith( + this.#props.node.addIndex!, + { + unique: true, + } + ), + }), + }) + } + + /** + * Adds a column to the index. + * + * Also see {@link columns} for adding multiple columns at once or {@link expression} + * for specifying an arbitrary expression. + * + * ### Examples + * + * ```ts + * await db.schema + * .alterTable('person') + * .createIndex('person_first_name_and_age_index') + * .column('first_name') + * .column('age desc') + * .execute() + * ``` + * + * The generated SQL (MySQL): + * + * ```sql + * alter table `person` add index `person_first_name_and_age_index` (`first_name`, `age` desc) + * ``` + */ + column( + column: OrderedColumnName + ): AlterTableAddIndexBuilder { + return new AlterTableAddIndexBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + addIndex: AddIndexNode.cloneWithColumns( + this.#props.node.addIndex!, + [parseOrderedColumnName(column)], + ), + }), + }) + } + + /** + * Specifies a list of columns for the index. + * + * Also see {@link column} for adding a single column or {@link expression} for + * specifying an arbitrary expression. + * + * ### Examples + * + * ```ts + * await db.schema + * .alterTable('person') + * .addIndex('person_first_name_and_age_index') + * .columns(['first_name', 'age desc']) + * .execute() + * ``` + * + * The generated SQL (MySQL): + * + * ```sql + * alter table `person` add index `person_first_name_and_age_index` (`first_name`, `age` desc) + * ``` + */ + columns( + columns: OrderedColumnName[] + ): AlterTableAddIndexBuilder { + return new AlterTableAddIndexBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + addIndex: AddIndexNode.cloneWithColumns( + this.#props.node.addIndex!, + columns.map(parseOrderedColumnName), + ), + }), + }) + } + + /** + * Specifies an arbitrary expression for the index. + * + * ### Examples + * + * ```ts + * import { sql } from 'kysely' + * + * await db.schema + * .alterTable('person') + * .addIndex('person_first_name_index') + * .expression(sql`(first_name < 'Sami')`) + * .execute() + * ``` + * + * The generated SQL (MySQL): + * + * ```sql + * alter table `person` add index `person_first_name_index` ((first_name < 'Sami')) + * ``` + */ + expression(expression: Expression): AlterTableAddIndexBuilder { + return new AlterTableAddIndexBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + addIndex: AddIndexNode.cloneWithColumns( + this.#props.node.addIndex!, + [expression.toOperationNode()], + ), + }), + }) + } + + /** + * Specifies the index type. + */ + using(indexType: IndexType): AlterTableAddIndexBuilder + using(indexType: string): AlterTableAddIndexBuilder + using(indexType: string): AlterTableAddIndexBuilder { + return new AlterTableAddIndexBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + addIndex: AddIndexNode.cloneWith( + this.#props.node.addIndex!, + { + using: RawNode.createWithSql(indexType), + } + ), + }), + + }) + } + + /** + * 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(): AlterTableNode { + 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) + } +} + +export interface AlterTableAddIndexBuilderProps { + readonly queryId: QueryId + readonly executor: QueryExecutor + readonly node: AlterTableNode +} + +preventAwait( + AlterTableAddIndexBuilder, + "don't await AlterTableAddIndexBuilder instances directly. To execute the query you need to call `execute`" +) \ No newline at end of file diff --git a/src/schema/alter-table-builder.ts b/src/schema/alter-table-builder.ts index c8cce3678..edcf362b4 100644 --- a/src/schema/alter-table-builder.ts +++ b/src/schema/alter-table-builder.ts @@ -37,6 +37,9 @@ import { AlterTableExecutor } from './alter-table-executor.js' import { AlterTableAddForeignKeyConstraintBuilder } from './alter-table-add-foreign-key-constraint-builder.js' import { AlterTableDropConstraintBuilder } from './alter-table-drop-constraint-builder.js' import { PrimaryConstraintNode } from '../operation-node/primary-constraint-node.js' +import { DropIndexNode } from '../operation-node/drop-index-node.js' +import { AddIndexNode } from '../operation-node/add-index-node.js' +import { AlterTableAddIndexBuilder } from './alter-table-add-index-builder.js' import { UniqueConstraintNodeBuilder, UniqueConstraintNodeBuilderCallback, @@ -250,6 +253,60 @@ export class AlterTableBuilder implements ColumnAlteringInterface { }) } + /** + * This can be used to add index to table. + * + * ### Examples + * + * ```ts + * db.schema.alterTable('person') + * .addIndex('person_email_index') + * .column('email') + * .unique() + * .execute() + * ``` + * + * The generated SQL (MySQL): + * + * ```sql + * alter table `person` add unique index `person_email_index` (`email`) + * ``` + */ + addIndex(indexName: string): AlterTableAddIndexBuilder { + return new AlterTableAddIndexBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + addIndex: AddIndexNode.create(indexName), + }), + }) + } + + /** + * This can be used to drop index from table. + * + * ### Examples + * + * ```ts + * db.schema.alterTable('person') + * .dropIndex('person_email_index') + * .execute() + * ``` + * + * The generated SQL (MySQL): + * + * ```sql + * alter table `person` drop index `test_first_name_index` + * ``` + */ + dropIndex(indexName: string): AlterTableExecutor { + return new AlterTableExecutor({ + ...this.#props, + node: AlterTableNode.cloneWithTableProps(this.#props.node, { + dropIndex: DropIndexNode.create(indexName), + }), + }) + } + /** * Calls the given function passing `this` as the only argument. * diff --git a/test/node/src/schema.test.ts b/test/node/src/schema.test.ts index d0183252e..2130d49a7 100644 --- a/test/node/src/schema.test.ts +++ b/test/node/src/schema.test.ts @@ -3305,6 +3305,156 @@ for (const dialect of DIALECTS) { await builder.execute() }) + + if (dialect === 'mysql') { + describe('add index', () => { + it('should add an index', async () => { + const query = ctx.db.schema + .alterTable('test') + .addIndex('test_integer_col_index') + .column('integer_col') + + testSql(query, dialect, { + mysql: { + sql: 'alter table `test` add index `test_integer_col_index` (`integer_col`)', + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED + }) + + await query.execute() + }) + + it('should add a unique index', async () => { + const query = ctx.db.schema + .alterTable('test') + .addIndex('test_integer_col_index') + .unique() + .column('integer_col') + + testSql(query, dialect, { + mysql: { + sql: 'alter table `test` add unique index `test_integer_col_index` (`integer_col`)', + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should add an index for multiple columns', async () => { + const query = ctx.db.schema + .alterTable('test') + .addIndex('test_integer_varchar_col_index') + .unique() + .columns(['integer_col', 'varchar_col']) + + testSql(query, dialect, { + mysql: { + sql: 'alter table `test` add unique index `test_integer_varchar_col_index` (`integer_col`, `varchar_col`)', + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should add an index for an expression', async () => { + const query = ctx.db.schema + .alterTable('test') + .addIndex('test_varchar_col_index') + .expression(sql`(varchar_col < 'Sami')`) + + testSql(query, dialect, { + mysql: { + sql: "alter table `test` add index `test_varchar_col_index` ((varchar_col < 'Sami'))", + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should add a sorted index, single column', async () => { + const query = ctx.db.schema + .alterTable('test') + .addIndex('test_integer_col_index') + .column('integer_col desc') + + testSql(query, dialect, { + mysql: { + sql: 'alter table `test` add index `test_integer_col_index` (`integer_col` desc)', + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + + it('should add a sorted index, multi-column', async () => { + const query = ctx.db.schema + .alterTable('test') + .addIndex('test_integer_varchar_col_index') + .columns(['integer_col desc', 'varchar_col desc']) + + testSql(query, dialect, { + mysql: { + sql: 'alter table `test` add index `test_integer_varchar_col_index` (`integer_col` desc, `varchar_col` desc)', + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + }) + } + + if (dialect === 'mysql') { + describe('drop index', () => { + beforeEach(async () => { + await ctx.db.schema + .alterTable('test') + .addIndex('test_integer_col_index') + .column('integer_col') + .execute() + }) + + it('should drop an index', async () => { + const query = ctx.db.schema + .alterTable('test') + .dropIndex('test_integer_col_index') + + testSql(query, dialect, { + mysql: { + sql: 'alter table `test` drop index `test_integer_col_index`', + parameters: [], + }, + postgres: NOT_SUPPORTED, + mssql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await query.execute() + }) + }) + } }) async function dropTestTables(): Promise {