diff --git a/src/dialect/mysql/mysql-driver.ts b/src/dialect/mysql/mysql-driver.ts index 2ddfac56f..dda0bb4be 100644 --- a/src/dialect/mysql/mysql-driver.ts +++ b/src/dialect/mysql/mysql-driver.ts @@ -121,6 +121,11 @@ class MysqlConnection implements DatabaseConnection { if (isOkPacket(result)) { const { insertId, affectedRows } = result + const numAffectedRows = + affectedRows !== undefined && affectedRows !== null + ? BigInt(affectedRows) + : undefined + return { insertId: insertId !== undefined && @@ -128,10 +133,9 @@ class MysqlConnection implements DatabaseConnection { insertId.toString() !== '0' ? BigInt(insertId) : undefined, - numUpdatedOrDeletedRows: - affectedRows !== undefined && insertId !== null - ? BigInt(affectedRows) - : undefined, + // TODO: remove. + numUpdatedOrDeletedRows: numAffectedRows, + numAffectedRows, rows: [], } } else if (Array.isArray(result)) { diff --git a/src/dialect/postgres/postgres-driver.ts b/src/dialect/postgres/postgres-driver.ts index a3cc3ab98..ae070d1f6 100644 --- a/src/dialect/postgres/postgres-driver.ts +++ b/src/dialect/postgres/postgres-driver.ts @@ -106,9 +106,17 @@ class PostgresConnection implements DatabaseConnection { ...compiledQuery.parameters, ]) - if (result.command === 'UPDATE' || result.command === 'DELETE') { + if ( + result.command === 'INSERT' || + result.command === 'UPDATE' || + result.command === 'DELETE' + ) { + const numAffectedRows = BigInt(result.rowCount) + return { - numUpdatedOrDeletedRows: BigInt(result.rowCount), + // TODO: remove. + numUpdatedOrDeletedRows: numAffectedRows, + numAffectedRows, rows: result.rows ?? [], } } diff --git a/src/dialect/sqlite/sqlite-driver.ts b/src/dialect/sqlite/sqlite-driver.ts index 9db4092e9..510848a1c 100644 --- a/src/dialect/sqlite/sqlite-driver.ts +++ b/src/dialect/sqlite/sqlite-driver.ts @@ -76,11 +76,13 @@ class SqliteConnection implements DatabaseConnection { } else { const { changes, lastInsertRowid } = stmt.run(parameters) + const numAffectedRows = + changes !== undefined && changes !== null ? BigInt(changes) : undefined + return Promise.resolve({ - numUpdatedOrDeletedRows: - changes !== undefined && changes !== null - ? BigInt(changes) - : undefined, + // TODO: remove. + numUpdatedOrDeletedRows: numAffectedRows, + numAffectedRows, insertId: lastInsertRowid !== undefined && lastInsertRowid !== null ? BigInt(lastInsertRowid) diff --git a/src/dialect/sqlite/sqlite-introspector.ts b/src/dialect/sqlite/sqlite-introspector.ts index fcab2c859..f8a589a44 100644 --- a/src/dialect/sqlite/sqlite-introspector.ts +++ b/src/dialect/sqlite/sqlite-introspector.ts @@ -68,6 +68,7 @@ export class SqliteIntrospector implements DatabaseIntrospector { const autoIncrementCol = createSql[0]?.sql ?.split(/[\(\),]/) ?.find((it) => it.toLowerCase().includes('autoincrement')) + ?.trimStart() ?.split(/\s+/)?.[0] ?.replace(/["`]/g, '') diff --git a/src/driver/database-connection.ts b/src/driver/database-connection.ts index b7cad17d7..17a1c6646 100644 --- a/src/driver/database-connection.ts +++ b/src/driver/database-connection.ts @@ -15,11 +15,17 @@ export interface DatabaseConnection { export interface QueryResult { /** - * This is defined for update and delete queries and contains - * the number of rows the query updated/deleted. + * @deprecated use {@link QueryResult.numAffectedRows} instead. */ + // TODO: remove. readonly numUpdatedOrDeletedRows?: bigint + /** + * This is defined for insert, update and delete queries and contains + * the number of rows the query inserted/updated/deleted. + */ + readonly numAffectedRows?: bigint + /** * This is defined for insert queries on dialects that return * the auto incrementing primary key from an insert. diff --git a/src/index.ts b/src/index.ts index aa8cce749..c45d6fdbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,6 +45,7 @@ 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/drop-view-builder.js' +export * from './schema/alter-column-builder.js' export * from './dynamic/dynamic.js' @@ -189,6 +190,7 @@ export { UnknownRow, } from './util/type-utils.js' export * from './util/infer-result.js' +export { logOnce } from './util/log-once.js' export { SelectExpression, diff --git a/src/operation-node/alter-table-node.ts b/src/operation-node/alter-table-node.ts index c458cb7de..b7b2ddb98 100644 --- a/src/operation-node/alter-table-node.ts +++ b/src/operation-node/alter-table-node.ts @@ -10,18 +10,24 @@ import { AddConstraintNode } from './add-constraint-node.js' import { DropConstraintNode } from './drop-constraint-node.js' import { ModifyColumnNode } from './modify-column-node.js' -export type AlterTableNodeProps = Omit +export type AlterTableNodeTableProps = Pick< + AlterTableNode, + 'renameTo' | 'setSchema' | 'addConstraint' | 'dropConstraint' +> + +export type AlterTableColumnAlterationNode = + | RenameColumnNode + | AddColumnNode + | DropColumnNode + | AlterColumnNode + | ModifyColumnNode export interface AlterTableNode extends OperationNode { readonly kind: 'AlterTableNode' readonly table: TableNode readonly renameTo?: TableNode readonly setSchema?: IdentifierNode - readonly renameColumn?: RenameColumnNode - readonly addColumn?: AddColumnNode - readonly dropColumn?: DropColumnNode - readonly alterColumn?: AlterColumnNode - readonly modifyColumn?: ModifyColumnNode + readonly columnAlterations?: ReadonlyArray readonly addConstraint?: AddConstraintNode readonly dropConstraint?: DropConstraintNode } @@ -41,10 +47,25 @@ export const AlterTableNode = freeze({ }) }, - cloneWith(node: AlterTableNode, props: AlterTableNodeProps): AlterTableNode { + cloneWithTableProps( + node: AlterTableNode, + props: AlterTableNodeTableProps + ): AlterTableNode { return freeze({ ...node, ...props, }) }, + + cloneWithColumnAlteration( + node: AlterTableNode, + columnAlteration: AlterTableColumnAlterationNode + ): AlterTableNode { + return freeze({ + ...node, + columnAlterations: node.columnAlterations + ? [...node.columnAlterations, columnAlteration] + : [columnAlteration], + }) + }, }) diff --git a/src/operation-node/operation-node-transformer.ts b/src/operation-node/operation-node-transformer.ts index e5c791853..042eac25e 100644 --- a/src/operation-node/operation-node-transformer.ts +++ b/src/operation-node/operation-node-transformer.ts @@ -667,11 +667,7 @@ export class OperationNodeTransformer { table: this.transformNode(node.table), renameTo: this.transformNode(node.renameTo), setSchema: this.transformNode(node.setSchema), - renameColumn: this.transformNode(node.renameColumn), - addColumn: this.transformNode(node.addColumn), - dropColumn: this.transformNode(node.dropColumn), - alterColumn: this.transformNode(node.alterColumn), - modifyColumn: this.transformNode(node.modifyColumn), + columnAlterations: this.transformNodeList(node.columnAlterations), addConstraint: this.transformNode(node.addConstraint), dropConstraint: this.transformNode(node.dropConstraint), }) diff --git a/src/query-builder/delete-query-builder.ts b/src/query-builder/delete-query-builder.ts index 59a427d79..6f6b9e93e 100644 --- a/src/query-builder/delete-query-builder.ts +++ b/src/query-builder/delete-query-builder.ts @@ -608,9 +608,14 @@ export class DeleteQueryBuilder if (this.#props.executor.adapter.supportsReturning && query.returning) { return result.rows - } else { - return [new DeleteResult(result.numUpdatedOrDeletedRows!) as unknown as O] } + + return [ + new DeleteResult( + // TODO: remove numUpdatedOrDeletedRows. + (result.numAffectedRows ?? result.numUpdatedOrDeletedRows)! + ) as any, + ] } /** diff --git a/src/query-builder/insert-query-builder.ts b/src/query-builder/insert-query-builder.ts index 41aa6b6bc..92c4756cb 100644 --- a/src/query-builder/insert-query-builder.ts +++ b/src/query-builder/insert-query-builder.ts @@ -652,9 +652,15 @@ export class InsertQueryBuilder if (this.#props.executor.adapter.supportsReturning && query.returning) { return result.rows - } else { - return [new InsertResult(result.insertId) as unknown as O] } + + return [ + new InsertResult( + result.insertId, + // TODO: remove numUpdatedOrDeletedRows. + result.numAffectedRows ?? result.numUpdatedOrDeletedRows + ) as any, + ] } /** diff --git a/src/query-builder/insert-result.ts b/src/query-builder/insert-result.ts index e03d6c9cd..63fd31801 100644 --- a/src/query-builder/insert-result.ts +++ b/src/query-builder/insert-result.ts @@ -7,6 +7,9 @@ * need to use {@link ReturningInterface.returning} or {@link ReturningInterface.returningAll} * to get out the inserted id. * + * {@link numInsertedOrUpdatedRows} holds the number of (actually) inserted rows. + * On MySQL, updated rows are counted twice when using `on duplicate key update`. + * * ### Examples * * ```ts @@ -20,9 +23,14 @@ */ export class InsertResult { readonly #insertId: bigint | undefined + readonly #numInsertedOrUpdatedRows: bigint | undefined - constructor(insertId: bigint | undefined) { + constructor( + insertId: bigint | undefined, + numInsertedOrUpdatedRows: bigint | undefined + ) { this.#insertId = insertId + this.#numInsertedOrUpdatedRows = numInsertedOrUpdatedRows } /** @@ -31,4 +39,11 @@ export class InsertResult { get insertId(): bigint | undefined { return this.#insertId } + + /** + * Affected rows count. + */ + get numInsertedOrUpdatedRows(): bigint | undefined { + return this.#numInsertedOrUpdatedRows + } } diff --git a/src/query-builder/update-query-builder.ts b/src/query-builder/update-query-builder.ts index 751f1131d..17909c182 100644 --- a/src/query-builder/update-query-builder.ts +++ b/src/query-builder/update-query-builder.ts @@ -704,9 +704,14 @@ export class UpdateQueryBuilder if (this.#props.executor.adapter.supportsReturning && query.returning) { return result.rows - } else { - return [new UpdateResult(result.numUpdatedOrDeletedRows!) as unknown as O] } + + return [ + new UpdateResult( + // TODO: remove numUpdatedOrDeletedRows. + (result.numAffectedRows ?? result.numUpdatedOrDeletedRows)! + ) as any, + ] } /** diff --git a/src/query-compiler/default-query-compiler.ts b/src/query-compiler/default-query-compiler.ts index f25e15f6b..93024edd3 100644 --- a/src/query-compiler/default-query-compiler.ts +++ b/src/query-compiler/default-query-compiler.ts @@ -954,24 +954,8 @@ export class DefaultQueryCompiler this.visitNode(node.dropConstraint) } - if (node.renameColumn) { - this.visitNode(node.renameColumn) - } - - if (node.addColumn) { - this.visitNode(node.addColumn) - } - - if (node.dropColumn) { - this.visitNode(node.dropColumn) - } - - if (node.alterColumn) { - this.visitNode(node.alterColumn) - } - - if (node.modifyColumn) { - this.visitNode(node.modifyColumn) + if (node.columnAlterations) { + this.compileList(node.columnAlterations) } } diff --git a/src/query-executor/query-executor-base.ts b/src/query-executor/query-executor-base.ts index 2fd76642a..e51fed3a9 100644 --- a/src/query-executor/query-executor-base.ts +++ b/src/query-executor/query-executor-base.ts @@ -11,6 +11,7 @@ import { QueryId } from '../util/query-id.js' import { DialectAdapter } from '../dialect/dialect-adapter.js' import { QueryExecutor } from './query-executor.js' import { Deferred } from '../util/deferred.js' +import { logOnce } from '../util/log-once.js' const NO_PLUGINS: ReadonlyArray = freeze([]) @@ -65,7 +66,13 @@ export abstract class QueryExecutorBase implements QueryExecutor { ): Promise> { return await this.provideConnection(async (connection) => { const result = await connection.executeQuery(compiledQuery) - return this.#transformResult(result, queryId) + + const transformedResult = await this.#transformResult(result, queryId) + + // TODO: remove. + warnOfOutdatedDriverOrPlugins(result, transformedResult) + + return transformedResult as any }) } @@ -118,3 +125,24 @@ export abstract class QueryExecutorBase implements QueryExecutor { return result } } + +// TODO: remove. +function warnOfOutdatedDriverOrPlugins( + result: QueryResult, + transformedResult: QueryResult +): void { + const { numAffectedRows } = result + + if ( + (numAffectedRows === undefined && + result.numUpdatedOrDeletedRows === undefined) || + (numAffectedRows !== undefined && + transformedResult.numAffectedRows !== undefined) + ) { + return + } + + logOnce( + 'kysely:warning: outdated driver/plugin detected! QueryResult.numUpdatedOrDeletedRows is deprecated and will be removed in a future release.' + ) +} diff --git a/src/schema/alter-column-builder.ts b/src/schema/alter-column-builder.ts new file mode 100644 index 000000000..a2d699bb9 --- /dev/null +++ b/src/schema/alter-column-builder.ts @@ -0,0 +1,83 @@ +import { AlterColumnNode } from '../operation-node/alter-column-node.js' +import { + ColumnDataType, + DataTypeNode, +} from '../operation-node/data-type-node.js' +import { OperationNode } from '../operation-node/operation-node.js' +import { OperationNodeSource } from '../operation-node/operation-node-source.js' +import { + DefaultValueExpression, + parseDefaultValueExpression, +} from '../parser/default-value-parser.js' + +export class AlterColumnBuilder { + protected readonly alterColumnNode: AlterColumnNode + + constructor(alterColumnNode: AlterColumnNode) { + this.alterColumnNode = alterColumnNode + } + + setDataType(dataType: ColumnDataType): AlteredColumnBuilder { + return new AlteredColumnBuilder( + AlterColumnNode.cloneWith(this.alterColumnNode, { + dataType: DataTypeNode.create(dataType), + }) + ) + } + + setDefault(value: DefaultValueExpression): AlteredColumnBuilder { + return new AlteredColumnBuilder( + AlterColumnNode.cloneWith(this.alterColumnNode, { + setDefault: parseDefaultValueExpression(value), + }) + ) + } + + dropDefault(): AlteredColumnBuilder { + return new AlteredColumnBuilder( + AlterColumnNode.cloneWith(this.alterColumnNode, { + dropDefault: true, + }) + ) + } + + setNotNull(): AlteredColumnBuilder { + return new AlteredColumnBuilder( + AlterColumnNode.cloneWith(this.alterColumnNode, { + setNotNull: true, + }) + ) + } + + dropNotNull(): AlteredColumnBuilder { + return new AlteredColumnBuilder( + AlterColumnNode.cloneWith(this.alterColumnNode, { + dropNotNull: true, + }) + ) + } +} + +/** + * Allows us to force consumers to do something, anything, when altering a column. + * + * Basically, deny the following: + * + * ```ts + * db.schema.alterTable('person').alterColumn('age', (ac) => ac) + * ``` + * + * Which would now throw a compilation error, instead of a runtime error. + */ +export class AlteredColumnBuilder + extends AlterColumnBuilder + implements OperationNodeSource +{ + toOperationNode(): AlterColumnNode { + return this.alterColumnNode + } +} + +export type AlterColumnBuilderCallback = ( + builder: AlterColumnBuilder +) => AlteredColumnBuilder diff --git a/src/schema/alter-table-builder.ts b/src/schema/alter-table-builder.ts index 65a986b26..6fd59051b 100644 --- a/src/schema/alter-table-builder.ts +++ b/src/schema/alter-table-builder.ts @@ -2,10 +2,6 @@ import { AddColumnNode } from '../operation-node/add-column-node.js' import { AlterColumnNode } from '../operation-node/alter-column-node.js' import { AlterTableNode } from '../operation-node/alter-table-node.js' import { ColumnDefinitionNode } from '../operation-node/column-definition-node.js' -import { - ColumnDataType, - DataTypeNode, -} from '../operation-node/data-type-node.js' import { DropColumnNode } from '../operation-node/drop-column-node.js' import { IdentifierNode } from '../operation-node/identifier-node.js' import { OperationNodeSource } from '../operation-node/operation-node-source.js' @@ -17,7 +13,7 @@ import { freeze, noop } from '../util/object-utils.js' import { preventAwait } from '../util/prevent-await.js' import { ColumnDefinitionBuilder, - ColumnDefinitionBuilderInterface, + ColumnDefinitionBuilderCallback, } from './column-definition-builder.js' import { QueryId } from '../util/query-id.js' import { QueryExecutor } from '../query-executor/query-executor.js' @@ -35,18 +31,19 @@ import { UniqueConstraintNode } from '../operation-node/unique-constraint-node.j import { CheckConstraintNode } from '../operation-node/check-constraint-node.js' import { ForeignKeyConstraintNode } from '../operation-node/foreign-key-constraint-node.js' import { ColumnNode } from '../operation-node/column-node.js' -import { - DefaultValueExpression, - parseDefaultValueExpression, -} from '../parser/default-value-parser.js' +import { DefaultValueExpression } from '../parser/default-value-parser.js' import { parseTable } from '../parser/table-parser.js' import { DropConstraintNode } from '../operation-node/drop-constraint-node.js' import { Expression } from '../expression/expression.js' +import { + AlterColumnBuilder, + AlterColumnBuilderCallback, +} from './alter-column-builder.js' /** * This builder can be used to create a `alter table` query. */ -export class AlterTableBuilder { +export class AlterTableBuilder implements ColumnAlteringInterface { readonly #props: AlterTableBuilderProps constructor(props: AlterTableBuilderProps) { @@ -56,7 +53,7 @@ export class AlterTableBuilder { renameTo(newTableName: string): AlterTableExecutor { return new AlterTableExecutor({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { renameTo: parseTable(newTableName), }), }) @@ -65,74 +62,94 @@ export class AlterTableBuilder { setSchema(newSchema: string): AlterTableExecutor { return new AlterTableExecutor({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { setSchema: IdentifierNode.create(newSchema), }), }) } - alterColumn(column: string): AlterColumnBuilder { - return new AlterColumnBuilder({ + alterColumn( + column: string, + alteration: AlterColumnBuilderCallback + ): AlterTableColumnAlteringBuilder { + const builder = alteration( + new AlterColumnBuilder(AlterColumnNode.create(column)) + ) + + return new AlterTableColumnAlteringBuilder({ ...this.#props, - alterColumnNode: AlterColumnNode.create(column), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + builder.toOperationNode() + ), }) } - dropColumn(column: string): AlterTableExecutor { - return new AlterTableExecutor({ + dropColumn(column: string): AlterTableColumnAlteringBuilder { + return new AlterTableColumnAlteringBuilder({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - dropColumn: DropColumnNode.create(column), - }), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + DropColumnNode.create(column) + ), }) } - renameColumn(column: string, newColumn: string): AlterTableExecutor { - return new AlterTableExecutor({ + renameColumn( + column: string, + newColumn: string + ): AlterTableColumnAlteringBuilder { + return new AlterTableColumnAlteringBuilder({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - renameColumn: RenameColumnNode.create(column, newColumn), - }), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + RenameColumnNode.create(column, newColumn) + ), }) } - /** - * See {@link CreateTableBuilder.addColumn} - */ addColumn( columnName: string, dataType: DataTypeExpression, - build: AlterTableAddColumnBuilderCallback = noop - ): AlterTableAddColumnBuilder { - return build( - new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: new ColumnDefinitionBuilder( - ColumnDefinitionNode.create( - columnName, - parseDataTypeExpression(dataType) - ) - ), - }) + build: ColumnDefinitionBuilderCallback = noop + ): AlterTableColumnAlteringBuilder { + const builder = build( + new ColumnDefinitionBuilder( + ColumnDefinitionNode.create( + columnName, + parseDataTypeExpression(dataType) + ) + ) ) + + return new AlterTableColumnAlteringBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + AddColumnNode.create(builder.toOperationNode()) + ), + }) } - /** - * Creates an `alter table modify column` query. The `modify column` statement - * is only implemeted by MySQL and oracle AFAIK. On other databases you - * should use the `alterColumn` method. - */ modifyColumn( columnName: string, - dataType: DataTypeExpression - ): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: new ColumnDefinitionBuilder( + dataType: DataTypeExpression, + build: ColumnDefinitionBuilderCallback = noop + ): AlterTableColumnAlteringBuilder { + const builder = build( + new ColumnDefinitionBuilder( ColumnDefinitionNode.create( columnName, parseDataTypeExpression(dataType) ) + ) + ) + + return new AlterTableColumnAlteringBuilder({ + ...this.#props, + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + ModifyColumnNode.create(builder.toOperationNode()) ), }) } @@ -146,7 +163,7 @@ export class AlterTableBuilder { ): AlterTableExecutor { return new AlterTableExecutor({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { addConstraint: AddConstraintNode.create( UniqueConstraintNode.create(columns, constraintName) ), @@ -163,7 +180,7 @@ export class AlterTableBuilder { ): AlterTableExecutor { return new AlterTableExecutor({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { addConstraint: AddConstraintNode.create( CheckConstraintNode.create( checkExpression.toOperationNode(), @@ -203,86 +220,28 @@ export class AlterTableBuilder { dropConstraint(constraintName: string): AlterTableDropConstraintBuilder { return new AlterTableDropConstraintBuilder({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { dropConstraint: DropConstraintNode.create(constraintName), }), }) } + + /** + * Calls the given function passing `this` as the only argument. + * + * See {@link CreateTableBuilder.call} + */ + call(func: (qb: this) => T): T { + return func(this) + } } export interface AlterTableBuilderProps { readonly queryId: QueryId - readonly alterTableNode: AlterTableNode + readonly node: AlterTableNode readonly executor: QueryExecutor } -export class AlterColumnBuilder { - readonly #props: AlterColumnBuilderProps - - constructor(props: AlterColumnBuilderProps) { - this.#props = freeze(props) - } - - setDataType(dataType: ColumnDataType): AlterTableExecutor { - return new AlterTableExecutor({ - ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - alterColumn: AlterColumnNode.cloneWith(this.#props.alterColumnNode, { - dataType: DataTypeNode.create(dataType), - }), - }), - }) - } - - setDefault(value: DefaultValueExpression): AlterTableExecutor { - return new AlterTableExecutor({ - ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - alterColumn: AlterColumnNode.cloneWith(this.#props.alterColumnNode, { - setDefault: parseDefaultValueExpression(value), - }), - }), - }) - } - - dropDefault(): AlterTableExecutor { - return new AlterTableExecutor({ - ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - alterColumn: AlterColumnNode.cloneWith(this.#props.alterColumnNode, { - dropDefault: true, - }), - }), - }) - } - - setNotNull(): AlterTableExecutor { - return new AlterTableExecutor({ - ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - alterColumn: AlterColumnNode.cloneWith(this.#props.alterColumnNode, { - setNotNull: true, - }), - }), - }) - } - - dropNotNull(): AlterTableExecutor { - return new AlterTableExecutor({ - ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { - alterColumn: AlterColumnNode.cloneWith(this.#props.alterColumnNode, { - dropNotNull: true, - }), - }), - }) - } -} - -export interface AlterColumnBuilderProps extends AlterTableBuilderProps { - readonly alterColumnNode: AlterColumnNode -} - export class AlterTableExecutor implements OperationNodeSource, Compilable { readonly #props: AlterTableExecutorProps @@ -292,7 +251,7 @@ export class AlterTableExecutor implements OperationNodeSource, Compilable { toOperationNode(): AlterTableNode { return this.#props.executor.transformQuery( - this.#props.alterTableNode, + this.#props.node, this.#props.queryId ) } @@ -311,296 +270,139 @@ export class AlterTableExecutor implements OperationNodeSource, Compilable { export interface AlterTableExecutorProps extends AlterTableBuilderProps {} -export class AlterTableAddColumnBuilder - implements ColumnDefinitionBuilderInterface, OperationNodeSource, Compilable -{ - readonly #props: AlterTableAddColumnBuilderProps - - constructor(props: AlterTableAddColumnBuilderProps) { - this.#props = freeze(props) - } - - autoIncrement(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.autoIncrement(), - }) - } - - primaryKey(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.primaryKey(), - }) - } - - references(ref: string): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.references(ref), - }) - } - - onDelete(onDelete: OnModifyForeignAction): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.onDelete(onDelete), - }) - } - - onUpdate(onDelete: OnModifyForeignAction): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.onUpdate(onDelete), - }) - } - - unique(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.unique(), - }) - } - - notNull(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.notNull(), - }) - } - - unsigned(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.unsigned(), - }) - } - - defaultTo(value: DefaultValueExpression): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.defaultTo(value), - }) - } - - check(expression: Expression): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.check(expression), - }) - } - - generatedAlwaysAs(expression: Expression): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.generatedAlwaysAs(expression), - }) - } - - generatedAlwaysAsIdentity(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.generatedAlwaysAsIdentity(), - }) - } - - generatedByDefaultAsIdentity(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.generatedByDefaultAsIdentity(), - }) - } - - stored(): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.stored(), - }) - } - - modifyFront(modifier: Expression): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.modifyFront(modifier), - }) - } - - modifyEnd(modifier: Expression): AlterTableAddColumnBuilder { - return new AlterTableAddColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.modifyEnd(modifier), - }) - } +export interface ColumnAlteringInterface { + alterColumn( + column: string, + alteration: AlterColumnBuilderCallback + ): ColumnAlteringInterface - toOperationNode(): AlterTableNode { - return this.#props.executor.transformQuery( - AlterTableNode.cloneWith(this.#props.alterTableNode, { - addColumn: AddColumnNode.create( - this.#props.columnBuilder.toOperationNode() - ), - }), - this.#props.queryId - ) - } + dropColumn(column: string): ColumnAlteringInterface - compile(): CompiledQuery { - return this.#props.executor.compileQuery( - this.toOperationNode(), - this.#props.queryId - ) - } + renameColumn(column: string, newColumn: string): ColumnAlteringInterface - async execute(): Promise { - await this.#props.executor.executeQuery(this.compile(), this.#props.queryId) - } -} + /** + * See {@link CreateTableBuilder.addColumn} + */ + addColumn( + columnName: string, + dataType: DataTypeExpression, + build?: ColumnDefinitionBuilderCallback + ): ColumnAlteringInterface -export interface AlterTableAddColumnBuilderProps - extends AlterTableBuilderProps { - readonly columnBuilder: ColumnDefinitionBuilder + /** + * Creates an `alter table modify column` query. The `modify column` statement + * is only implemeted by MySQL and oracle AFAIK. On other databases you + * should use the `alterColumn` method. + */ + modifyColumn( + columnName: string, + dataType: DataTypeExpression, + build: ColumnDefinitionBuilderCallback + ): ColumnAlteringInterface } -export type AlterTableAddColumnBuilderCallback = ( - builder: AlterTableAddColumnBuilder -) => AlterTableAddColumnBuilder - -export class AlterTableModifyColumnBuilder - implements ColumnDefinitionBuilderInterface, OperationNodeSource, Compilable +export class AlterTableColumnAlteringBuilder + implements ColumnAlteringInterface, OperationNodeSource, Compilable { - readonly #props: AlterTableModifyColumnBuilderProps - - constructor(props: AlterTableModifyColumnBuilderProps) { - this.#props = freeze(props) - } - - autoIncrement(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.autoIncrement(), - }) - } - - primaryKey(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.primaryKey(), - }) - } + readonly #props: AlterTableColumnAlteringBuilderProps - references(ref: string): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.references(ref), - }) + constructor(props: AlterTableColumnAlteringBuilderProps) { + this.#props = props } - onDelete(onDelete: OnModifyForeignAction): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.onDelete(onDelete), - }) - } - - onUpdate(onUpdate: OnModifyForeignAction): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.onUpdate(onUpdate), - }) - } - - unique(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.unique(), - }) - } - - notNull(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.notNull(), - }) - } - - unsigned(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.unsigned(), - }) - } - - defaultTo(value: DefaultValueExpression): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.defaultTo(value), - }) - } + alterColumn( + column: string, + alteration: AlterColumnBuilderCallback + ): AlterTableColumnAlteringBuilder { + const builder = alteration( + new AlterColumnBuilder(AlterColumnNode.create(column)) + ) - check(expression: Expression): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ + return new AlterTableColumnAlteringBuilder({ ...this.#props, - columnBuilder: this.#props.columnBuilder.check(expression), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + builder.toOperationNode() + ), }) } - generatedAlwaysAs( - expression: Expression - ): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ + dropColumn(column: string): AlterTableColumnAlteringBuilder { + return new AlterTableColumnAlteringBuilder({ ...this.#props, - columnBuilder: this.#props.columnBuilder.generatedAlwaysAs(expression), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + DropColumnNode.create(column) + ), }) } - generatedAlwaysAsIdentity(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ + renameColumn( + column: string, + newColumn: string + ): AlterTableColumnAlteringBuilder { + return new AlterTableColumnAlteringBuilder({ ...this.#props, - columnBuilder: this.#props.columnBuilder.generatedAlwaysAsIdentity(), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + RenameColumnNode.create(column, newColumn) + ), }) } - generatedByDefaultAsIdentity(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.generatedByDefaultAsIdentity(), - }) - } + addColumn( + columnName: string, + dataType: DataTypeExpression, + build: ColumnDefinitionBuilderCallback = noop + ): AlterTableColumnAlteringBuilder { + const builder = build( + new ColumnDefinitionBuilder( + ColumnDefinitionNode.create( + columnName, + parseDataTypeExpression(dataType) + ) + ) + ) - stored(): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ + return new AlterTableColumnAlteringBuilder({ ...this.#props, - columnBuilder: this.#props.columnBuilder.stored(), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + AddColumnNode.create(builder.toOperationNode()) + ), }) } - modifyFront(modifier: Expression): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ - ...this.#props, - columnBuilder: this.#props.columnBuilder.modifyFront(modifier), - }) - } + modifyColumn( + columnName: string, + dataType: DataTypeExpression, + build: ColumnDefinitionBuilderCallback = noop + ): AlterTableColumnAlteringBuilder { + const builder = build( + new ColumnDefinitionBuilder( + ColumnDefinitionNode.create( + columnName, + parseDataTypeExpression(dataType) + ) + ) + ) - modifyEnd(modifier: Expression): AlterTableModifyColumnBuilder { - return new AlterTableModifyColumnBuilder({ + return new AlterTableColumnAlteringBuilder({ ...this.#props, - columnBuilder: this.#props.columnBuilder.modifyEnd(modifier), + node: AlterTableNode.cloneWithColumnAlteration( + this.#props.node, + ModifyColumnNode.create(builder.toOperationNode()) + ), }) } toOperationNode(): AlterTableNode { - return this.#props.executor.transformQuery( - AlterTableNode.cloneWith(this.#props.alterTableNode, { - modifyColumn: ModifyColumnNode.create( - this.#props.columnBuilder.toOperationNode() - ), - }), - this.#props.queryId - ) + return this.#props.node } compile(): CompiledQuery { return this.#props.executor.compileQuery( - this.toOperationNode(), + this.#props.node, this.#props.queryId ) } @@ -610,10 +412,8 @@ export class AlterTableModifyColumnBuilder } } -export interface AlterTableModifyColumnBuilderProps - extends AlterTableBuilderProps { - readonly columnBuilder: ColumnDefinitionBuilder -} +export interface AlterTableColumnAlteringBuilderProps + extends AlterTableBuilderProps {} export class AlterTableAddForeignKeyConstraintBuilder implements @@ -647,7 +447,7 @@ export class AlterTableAddForeignKeyConstraintBuilder toOperationNode(): AlterTableNode { return this.#props.executor.transformQuery( - AlterTableNode.cloneWith(this.#props.alterTableNode, { + AlterTableNode.cloneWithTableProps(this.#props.node, { addConstraint: AddConstraintNode.create( this.#props.constraintBuilder.toOperationNode() ), @@ -685,9 +485,9 @@ export class AlterTableDropConstraintBuilder ifExists(): AlterTableDropConstraintBuilder { return new AlterTableDropConstraintBuilder({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { dropConstraint: DropConstraintNode.cloneWith( - this.#props.alterTableNode.dropConstraint!, + this.#props.node.dropConstraint!, { ifExists: true, } @@ -699,9 +499,9 @@ export class AlterTableDropConstraintBuilder cascade(): AlterTableDropConstraintBuilder { return new AlterTableDropConstraintBuilder({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { dropConstraint: DropConstraintNode.cloneWith( - this.#props.alterTableNode.dropConstraint!, + this.#props.node.dropConstraint!, { modifier: 'cascade', } @@ -713,9 +513,9 @@ export class AlterTableDropConstraintBuilder restrict(): AlterTableDropConstraintBuilder { return new AlterTableDropConstraintBuilder({ ...this.#props, - alterTableNode: AlterTableNode.cloneWith(this.#props.alterTableNode, { + node: AlterTableNode.cloneWithTableProps(this.#props.node, { dropConstraint: DropConstraintNode.cloneWith( - this.#props.alterTableNode.dropConstraint!, + this.#props.node.dropConstraint!, { modifier: 'restrict', } @@ -726,7 +526,7 @@ export class AlterTableDropConstraintBuilder toOperationNode(): AlterTableNode { return this.#props.executor.transformQuery( - this.#props.alterTableNode, + this.#props.node, this.#props.queryId ) } @@ -755,13 +555,8 @@ preventAwait( ) preventAwait( - AlterTableAddColumnBuilder, - "don't await AlterTableAddColumnBuilder instances directly. To execute the query you need to call `execute`" -) - -preventAwait( - AlterTableModifyColumnBuilder, - "don't await AlterTableModifyColumnBuilder instances directly. To execute the query you need to call `execute`" + AlterTableColumnAlteringBuilder, + "don't await AlterTableColumnAlteringBuilder instances directly. To execute the query you need to call `execute`" ) preventAwait( diff --git a/src/schema/column-definition-builder.ts b/src/schema/column-definition-builder.ts index 7bb419bd5..8f546c989 100644 --- a/src/schema/column-definition-builder.ts +++ b/src/schema/column-definition-builder.ts @@ -18,7 +18,13 @@ import { DefaultValueNode } from '../operation-node/default-value-node.js' import { parseOnModifyForeignAction } from '../parser/on-modify-action-parser.js' import { Expression } from '../expression/expression.js' -export interface ColumnDefinitionBuilderInterface { +export class ColumnDefinitionBuilder implements OperationNodeSource { + readonly #node: ColumnDefinitionNode + + constructor(node: ColumnDefinitionNode) { + this.#node = node + } + /** * Adds `auto_increment` or `autoincrement` to the column definition * depending on the dialect. @@ -26,7 +32,11 @@ export interface ColumnDefinitionBuilderInterface { * Some dialects like PostgreSQL don't support this. On PostgreSQL * you can use the `serial` or `bigserial` data type instead. */ - autoIncrement(): ColumnDefinitionBuilderInterface + autoIncrement(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { autoIncrement: true }) + ) + } /** * Makes the column the primary key. @@ -34,7 +44,11 @@ export interface ColumnDefinitionBuilderInterface { * If you want to specify a composite primary key use the * {@link CreateTableBuilder.addPrimaryKeyConstraint} method. */ - primaryKey(): ColumnDefinitionBuilderInterface + primaryKey(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { primaryKey: true }) + ) + } /** * Adds a foreign key constraint for the column. @@ -49,7 +63,23 @@ export interface ColumnDefinitionBuilderInterface { * col.references('person.id') * ``` */ - references(ref: string): ColumnDefinitionBuilderInterface + references(ref: string): ColumnDefinitionBuilder { + const references = parseStringReference(ref) + + if (!ReferenceNode.is(references) || SelectAllNode.is(references.column)) { + throw new Error( + `invalid call references('${ref}'). The reference must have format table.column or schema.table.column` + ) + } + + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + references: ReferencesNode.create(references.table, [ + references.column, + ]), + }) + ) + } /** * Adds an `on delete` constraint for the foreign key column. @@ -64,7 +94,20 @@ export interface ColumnDefinitionBuilderInterface { * col.references('person.id').onDelete('cascade') * ``` */ - onDelete(onDelete: OnModifyForeignAction): ColumnDefinitionBuilderInterface + onDelete(onDelete: OnModifyForeignAction): ColumnDefinitionBuilder { + if (!this.#node.references) { + throw new Error('on delete constraint can only be added for foreign keys') + } + + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + references: ReferencesNode.cloneWithOnDelete( + this.#node.references, + parseOnModifyForeignAction(onDelete) + ), + }) + ) + } /** * Adds an `on update` constraint for the foreign key column. @@ -75,24 +118,49 @@ export interface ColumnDefinitionBuilderInterface { * col.references('person.id').onUpdate('cascade') * ``` */ - onUpdate(onUpdate: OnModifyForeignAction): ColumnDefinitionBuilderInterface + onUpdate(onUpdate: OnModifyForeignAction): ColumnDefinitionBuilder { + if (!this.#node.references) { + throw new Error('on update constraint can only be added for foreign keys') + } + + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + references: ReferencesNode.cloneWithOnUpdate( + this.#node.references, + parseOnModifyForeignAction(onUpdate) + ), + }) + ) + } /** * Adds a unique constraint for the column. */ - unique(): ColumnDefinitionBuilderInterface + unique(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { unique: true }) + ) + } /** * Adds a `not null` constraint for the column. */ - notNull(): ColumnDefinitionBuilderInterface + notNull(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { notNull: true }) + ) + } /** * Adds a `unsigned` modifier for the column. * * This only works on some dialects like MySQL. */ - unsigned(): ColumnDefinitionBuilderInterface + unsigned(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { unsigned: true }) + ) + } /** * Adds a default value constraint for the column. @@ -122,7 +190,13 @@ export interface ColumnDefinitionBuilderInterface { * .execute() * ``` */ - defaultTo(value: DefaultValueExpression): ColumnDefinitionBuilderInterface + defaultTo(value: DefaultValueExpression): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + defaultTo: DefaultValueNode.create(parseDefaultValueExpression(value)), + }) + ) + } /** * Adds a check constraint for the column. @@ -140,7 +214,13 @@ export interface ColumnDefinitionBuilderInterface { * .execute() * ``` */ - check(expression: Expression): ColumnDefinitionBuilderInterface + check(expression: Expression): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + check: CheckConstraintNode.create(expression.toOperationNode()), + }) + ) + } /** * Makes the column a generated column using a `generated always as` statement. @@ -158,19 +238,37 @@ export interface ColumnDefinitionBuilderInterface { * .execute() * ``` */ - generatedAlwaysAs( - expression: Expression - ): ColumnDefinitionBuilderInterface + generatedAlwaysAs(expression: Expression): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + generated: GeneratedNode.createWithExpression( + expression.toOperationNode() + ), + }) + ) + } /** * Adds the `generated always as identity` specifier on supported dialects. */ - generatedAlwaysAsIdentity(): ColumnDefinitionBuilderInterface + generatedAlwaysAsIdentity(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + generated: GeneratedNode.create({ identity: true, always: true }), + }) + ) + } /** * Adds the `generated by default as identity` specifier on supported dialects. */ - generatedByDefaultAsIdentity(): ColumnDefinitionBuilderInterface + generatedByDefaultAsIdentity(): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + generated: GeneratedNode.create({ identity: true, byDefault: true }), + }) + ) + } /** * Makes a generated column stored instead of virtual. This method can only @@ -188,7 +286,19 @@ export interface ColumnDefinitionBuilderInterface { * .execute() * ``` */ - stored(): ColumnDefinitionBuilderInterface + stored(): ColumnDefinitionBuilder { + if (!this.#node.generated) { + throw new Error('stored() can only be called after generatedAlwaysAs') + } + + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWith(this.#node, { + generated: GeneratedNode.cloneWith(this.#node.generated, { + stored: true, + }), + }) + ) + } /** * This can be used to add any additional SQL right after the column's data type. @@ -211,7 +321,14 @@ export interface ColumnDefinitionBuilderInterface { * ) * ``` */ - modifyFront(modifier: Expression): ColumnDefinitionBuilderInterface + modifyFront(modifier: Expression): ColumnDefinitionBuilder { + return new ColumnDefinitionBuilder( + ColumnDefinitionNode.cloneWithFrontModifier( + this.#node, + modifier.toOperationNode() + ) + ) + } /** * This can be used to add any additional SQL to the end of the column definition. @@ -234,161 +351,6 @@ export interface ColumnDefinitionBuilderInterface { * ) * ``` */ - modifyEnd(modifier: Expression): ColumnDefinitionBuilderInterface -} - -export class ColumnDefinitionBuilder - implements ColumnDefinitionBuilderInterface, OperationNodeSource -{ - readonly #node: ColumnDefinitionNode - - constructor(node: ColumnDefinitionNode) { - this.#node = node - } - - autoIncrement(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { autoIncrement: true }) - ) - } - - primaryKey(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { primaryKey: true }) - ) - } - - references(ref: string): ColumnDefinitionBuilder { - const references = parseStringReference(ref) - - if (!ReferenceNode.is(references) || SelectAllNode.is(references.column)) { - throw new Error( - `invalid call references('${ref}'). The reference must have format table.column or schema.table.column` - ) - } - - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - references: ReferencesNode.create(references.table, [ - references.column, - ]), - }) - ) - } - - onDelete(onDelete: OnModifyForeignAction): ColumnDefinitionBuilder { - if (!this.#node.references) { - throw new Error('on delete constraint can only be added for foreign keys') - } - - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - references: ReferencesNode.cloneWithOnDelete( - this.#node.references, - parseOnModifyForeignAction(onDelete) - ), - }) - ) - } - - onUpdate(onUpdate: OnModifyForeignAction): ColumnDefinitionBuilder { - if (!this.#node.references) { - throw new Error('on update constraint can only be added for foreign keys') - } - - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - references: ReferencesNode.cloneWithOnUpdate( - this.#node.references, - parseOnModifyForeignAction(onUpdate) - ), - }) - ) - } - - unique(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { unique: true }) - ) - } - - notNull(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { notNull: true }) - ) - } - - unsigned(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { unsigned: true }) - ) - } - - defaultTo(value: DefaultValueExpression): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - defaultTo: DefaultValueNode.create(parseDefaultValueExpression(value)), - }) - ) - } - - check(expression: Expression): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - check: CheckConstraintNode.create(expression.toOperationNode()), - }) - ) - } - - generatedAlwaysAs(expression: Expression): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - generated: GeneratedNode.createWithExpression( - expression.toOperationNode() - ), - }) - ) - } - - generatedAlwaysAsIdentity(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - generated: GeneratedNode.create({ identity: true, always: true }), - }) - ) - } - - generatedByDefaultAsIdentity(): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - generated: GeneratedNode.create({ identity: true, byDefault: true }), - }) - ) - } - - stored(): ColumnDefinitionBuilder { - if (!this.#node.generated) { - throw new Error('stored() can only be called after generatedAlwaysAs') - } - - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWith(this.#node, { - generated: GeneratedNode.cloneWith(this.#node.generated, { - stored: true, - }), - }) - ) - } - - modifyFront(modifier: Expression): ColumnDefinitionBuilder { - return new ColumnDefinitionBuilder( - ColumnDefinitionNode.cloneWithFrontModifier( - this.#node, - modifier.toOperationNode() - ) - ) - } - modifyEnd(modifier: Expression): ColumnDefinitionBuilder { return new ColumnDefinitionBuilder( ColumnDefinitionNode.cloneWithEndModifier( @@ -407,3 +369,7 @@ preventAwait( ColumnDefinitionBuilder, "don't await ColumnDefinitionBuilder instances directly." ) + +export type ColumnDefinitionBuilderCallback = ( + builder: ColumnDefinitionBuilder +) => ColumnDefinitionBuilder diff --git a/src/schema/create-table-builder.ts b/src/schema/create-table-builder.ts index b7d928323..35e50f4a9 100644 --- a/src/schema/create-table-builder.ts +++ b/src/schema/create-table-builder.ts @@ -355,6 +355,42 @@ export class CreateTableBuilder }) } + /** + * Calls the given function passing `this` as the only argument. + * + * ### Examples + * + * ```ts + * db.schema + * .createTable('test') + * .call((builder) => builder.addColumn('id', 'integer')) + * .execute() + * ``` + * + * ```ts + * const addDefaultColumns = ( + * builder: CreateTableBuilder + * ) => { + * return builder + * .addColumn('id', 'integer', (col) => col.notNull()) + * .addColumn('created_at', 'date', (col) => + * col.notNull().defaultTo(sql`now()`) + * ) + * .addColumn('updated_at', 'date', (col) => + * col.notNull().defaultTo(sql`now()`) + * ) + * } + * + * db.schema + * .createTable('test') + * .call(addDefaultColumns) + * .execute() + * ``` + */ + call(func: (qb: this) => T): T { + return func(this) + } + toOperationNode(): CreateTableNode { return this.#props.executor.transformQuery( this.#props.createTableNode, diff --git a/src/schema/schema.ts b/src/schema/schema.ts index 124d69c86..20b3d337b 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -210,7 +210,7 @@ export class SchemaModule { return new AlterTableBuilder({ queryId: createQueryId(), executor: this.#executor, - alterTableNode: AlterTableNode.create(table), + node: AlterTableNode.create(table), }) } diff --git a/src/util/log-once.ts b/src/util/log-once.ts new file mode 100644 index 000000000..9c85ef3f2 --- /dev/null +++ b/src/util/log-once.ts @@ -0,0 +1,14 @@ +const LOGGED_MESSAGES: Set = new Set() + +/** + * Use for system-level logging, such as deprecation messages. + * Logs a message and ensures it won't be logged again. + */ +export function logOnce(message: string): void { + if (LOGGED_MESSAGES.has(message)) { + return + } + + LOGGED_MESSAGES.add(message) + console.log(message) +} diff --git a/test/node/src/delete.test.ts b/test/node/src/delete.test.ts index 1b7b53fb0..3e59293e0 100644 --- a/test/node/src/delete.test.ts +++ b/test/node/src/delete.test.ts @@ -104,7 +104,10 @@ for (const dialect of BUILT_IN_DIALECTS) { sqlite: NOT_SUPPORTED, }) - await query.execute() + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(DeleteResult) + expect(result.numDeletedRows).to.equal(2n) }) } diff --git a/test/node/src/insert.test.ts b/test/node/src/insert.test.ts index 474949a45..90e3db62a 100644 --- a/test/node/src/insert.test.ts +++ b/test/node/src/insert.test.ts @@ -58,9 +58,10 @@ for (const dialect of BUILT_IN_DIALECTS) { const result = await query.executeTakeFirst() expect(result).to.be.instanceOf(InsertResult) + expect(result.numInsertedOrUpdatedRows).to.equal(1n) if (dialect === 'postgres') { - expect(result.insertId).to.equal(undefined) + expect(result.insertId).to.be.undefined } else { expect(result.insertId).to.be.a('bigint') } @@ -100,6 +101,7 @@ for (const dialect of BUILT_IN_DIALECTS) { const result = await query.executeTakeFirst() expect(result).to.be.instanceOf(InsertResult) + expect(result.numInsertedOrUpdatedRows).to.equal(1n) expect(await getNewestPerson(ctx.db)).to.eql({ first_name: 'Hammo', @@ -130,7 +132,15 @@ for (const dialect of BUILT_IN_DIALECTS) { }, }) - await query.execute() + const result = await query.executeTakeFirst() + expect(result).to.be.instanceOf(InsertResult) + + const { pet_count } = await ctx.db + .selectFrom('pet') + .select(sql`count(*)`.as('pet_count')) + .executeTakeFirstOrThrow() + + expect(result.numInsertedOrUpdatedRows).to.equal(BigInt(pet_count)) const persons = await ctx.db .selectFrom('person') @@ -177,8 +187,13 @@ for (const dialect of BUILT_IN_DIALECTS) { sqlite: NOT_SUPPORTED, }) - const res = await query.execute() - expect(res).to.have.length(2) + const result = await query.execute() + + expect(result).to.have.length(2) + expect(result).to.deep.equal([ + { first_name: '1', gender: 'foo' }, + { first_name: '2', gender: 'bar' }, + ]) }) } @@ -205,7 +220,16 @@ for (const dialect of BUILT_IN_DIALECTS) { }, }) - await query.execute() + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(InsertResult) + expect(result.numInsertedOrUpdatedRows).to.equal(1n) + + if (dialect === 'postgres') { + expect(result.insertId).to.be.undefined + } else { + expect(result.insertId).to.be.a('bigint') + } }) if (dialect === 'mysql') { @@ -234,7 +258,8 @@ for (const dialect of BUILT_IN_DIALECTS) { const result = await query.executeTakeFirst() expect(result).to.be.instanceOf(InsertResult) - expect(result.insertId).to.equal(undefined) + expect(result.insertId).to.be.undefined + expect(result.numInsertedOrUpdatedRows).to.equal(0n) }) } @@ -272,13 +297,15 @@ for (const dialect of BUILT_IN_DIALECTS) { }) const result = await query.executeTakeFirst() + expect(result).to.be.instanceOf(InsertResult) + expect(result.numInsertedOrUpdatedRows).to.equal(0n) if (dialect === 'sqlite') { // SQLite seems to return the last inserted id even if nothing got inserted. expect(result.insertId! > 0n).to.be.equal(true) } else { - expect(result.insertId).to.equal(undefined) + expect(result.insertId).to.be.undefined } }) } @@ -310,8 +337,10 @@ for (const dialect of BUILT_IN_DIALECTS) { }) const result = await query.executeTakeFirst() + expect(result).to.be.instanceOf(InsertResult) - expect(result.insertId).to.equal(undefined) + expect(result.insertId).to.be.undefined + expect(result.numInsertedOrUpdatedRows).to.equal(0n) }) } @@ -342,7 +371,11 @@ for (const dialect of BUILT_IN_DIALECTS) { sqlite: NOT_SUPPORTED, }) - await query.execute() + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(InsertResult) + expect(result.insertId).to.equal(BigInt(id)) + expect(result.numInsertedOrUpdatedRows).to.equal(2n) const updatedPet = await ctx.db .selectFrom('pet') @@ -394,7 +427,16 @@ for (const dialect of BUILT_IN_DIALECTS) { }, }) - await query.execute() + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(InsertResult) + expect(result.numInsertedOrUpdatedRows).to.equal(1n) + + if (dialect === 'postgres') { + expect(result.insertId).to.be.undefined + } else { + expect(result.insertId).to.be.a('bigint') + } const updatedPet = await ctx.db .selectFrom('pet') @@ -485,12 +527,71 @@ for (const dialect of BUILT_IN_DIALECTS) { sqlite: NOT_SUPPORTED, }) - await query.execute() + const result = await query.execute() + + expect(result).to.have.length(1) + expect(result[0]).to.containSubset({ + species: 'hamster', + name: 'Catto', + }) }) } + it('should insert multiple rows', async () => { + const query = ctx.db.insertInto('person').values([ + { + first_name: 'Foo', + last_name: 'Bar', + gender: 'other', + }, + { + first_name: 'Baz', + last_name: 'Spam', + gender: 'other', + }, + ]) + + testSql(query, dialect, { + postgres: { + sql: 'insert into "person" ("first_name", "last_name", "gender") values ($1, $2, $3), ($4, $5, $6)', + parameters: ['Foo', 'Bar', 'other', 'Baz', 'Spam', 'other'], + }, + mysql: { + sql: 'insert into `person` (`first_name`, `last_name`, `gender`) values (?, ?, ?), (?, ?, ?)', + parameters: ['Foo', 'Bar', 'other', 'Baz', 'Spam', 'other'], + }, + sqlite: { + sql: 'insert into "person" ("first_name", "last_name", "gender") values (?, ?, ?), (?, ?, ?)', + parameters: ['Foo', 'Bar', 'other', 'Baz', 'Spam', 'other'], + }, + }) + + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(InsertResult) + expect(result.numInsertedOrUpdatedRows).to.equal(2n) + + if (dialect === 'postgres') { + expect(result.insertId).to.be.undefined + } else { + expect(result.insertId).to.be.a('bigint') + } + + const inserted = await ctx.db + .selectFrom('person') + .selectAll() + .orderBy('id', 'desc') + .limit(2) + .execute() + + expect(inserted).to.containSubset([ + { first_name: 'Foo', last_name: 'Bar', gender: 'other' }, + { first_name: 'Baz', last_name: 'Spam', gender: 'other' }, + ]) + }) + if (dialect === 'postgres' || dialect === 'sqlite') { - it('should insert multiple rows', async () => { + it('should insert multiple rows while falling back to default values in partial rows', async () => { const query = ctx.db .insertInto('person') .values([ @@ -522,10 +623,15 @@ for (const dialect of BUILT_IN_DIALECTS) { }) const result = await query.execute() + expect(result).to.have.length(2) + expect(result).to.containSubset([ + { first_name: 'Foo', last_name: null, gender: 'other' }, + { first_name: 'Baz', last_name: 'Spam', gender: 'other' }, + ]) }) - it('should return data using `returning`', async () => { + it('should insert a row and return data using `returning`', async () => { const result = await ctx.db .insertInto('person') .values({ @@ -552,7 +658,7 @@ for (const dialect of BUILT_IN_DIALECTS) { last_name: 'Barson', }) - it('conditional returning statement should add optional fields', async () => { + it('should insert a row, returning some fields of inserted row and conditionally returning additional fields', async () => { const condition = true const query = ctx.db @@ -566,11 +672,12 @@ for (const dialect of BUILT_IN_DIALECTS) { .if(condition, (qb) => qb.returning('last_name')) const result = await query.executeTakeFirstOrThrow() + expect(result.last_name).to.equal('Barson') }) }) - it('should return data using `returningAll`', async () => { + it('should insert a row and return data using `returningAll`', async () => { const result = await ctx.db .insertInto('person') .values({ diff --git a/test/node/src/log-once.test.ts b/test/node/src/log-once.test.ts new file mode 100644 index 000000000..e2d7c61c7 --- /dev/null +++ b/test/node/src/log-once.test.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai' +import { createSandbox, SinonSpy } from 'sinon' +import { logOnce } from '../../..' + +describe('logOnce', () => { + let logSpy: SinonSpy + const sandbox = createSandbox() + + before(() => { + logSpy = sandbox.spy(console, 'log') + }) + + it('should log each message once.', () => { + const message = 'Kysely is awesome!' + const message2 = 'Type-safety is everything!' + + logOnce(message) + logOnce(message) + logOnce(message2) + logOnce(message2) + logOnce(message) + + expect(logSpy.calledTwice).to.be.true + expect(logSpy.getCall(0).args[0]).to.equal(message) + expect(logSpy.getCall(1).args[0]).to.equal(message2) + }) +}) diff --git a/test/node/src/schema.test.ts b/test/node/src/schema.test.ts index 59c851307..82843f460 100644 --- a/test/node/src/schema.test.ts +++ b/test/node/src/schema.test.ts @@ -786,6 +786,46 @@ for (const dialect of BUILT_IN_DIALECTS) { await builder.execute() }) } + + it('should create a table calling query builder functions', async () => { + const builder = ctx.db.schema + .createTable('test') + .addColumn('id', 'integer', (col) => col.notNull()) + .call((builder) => + builder.addColumn('call_me', 'varchar(10)', (col) => + col.defaultTo('maybe') + ) + ) + + testSql(builder, dialect, { + postgres: { + sql: [ + 'create table "test"', + '("id" integer not null,', + `"call_me" varchar(10) default 'maybe')`, + ], + parameters: [], + }, + mysql: { + sql: [ + 'create table `test`', + '(`id` integer not null,', + "`call_me` varchar(10) default 'maybe')", + ], + parameters: [], + }, + sqlite: { + sql: [ + 'create table "test"', + '("id" integer not null,', + `"call_me" varchar(10) default 'maybe')`, + ], + parameters: [], + }, + }) + + await builder.execute() + }) }) describe('drop table', () => { @@ -1541,6 +1581,109 @@ for (const dialect of BUILT_IN_DIALECTS) { .execute() }) + describe('add column', () => { + it('should add a column', async () => { + const builder = ctx.db.schema + .alterTable('test') + .addColumn('bool_col', 'boolean', (cb) => cb.notNull()) + + testSql(builder, dialect, { + postgres: { + sql: 'alter table "test" add column "bool_col" boolean not null', + parameters: [], + }, + mysql: { + sql: 'alter table `test` add column `bool_col` boolean not null', + parameters: [], + }, + sqlite: { + sql: 'alter table "test" add column "bool_col" boolean not null', + parameters: [], + }, + }) + + await builder.execute() + + expect(await getColumnMeta('test.bool_col')).to.containSubset({ + name: 'bool_col', + isNullable: false, + dataType: + dialect === 'postgres' + ? 'bool' + : dialect === 'sqlite' + ? 'boolean' + : 'tinyint', + }) + }) + + if (dialect !== 'sqlite') { + it('should add a unique column', async () => { + const builder = ctx.db.schema + .alterTable('test') + .addColumn('bool_col', 'boolean', (cb) => cb.notNull().unique()) + + testSql(builder, dialect, { + postgres: { + sql: 'alter table "test" add column "bool_col" boolean not null unique', + parameters: [], + }, + mysql: { + sql: 'alter table `test` add column `bool_col` boolean not null unique', + parameters: [], + }, + sqlite: { + sql: 'alter table "test" add column "bool_col" boolean not null unique', + parameters: [], + }, + }) + + await builder.execute() + + expect(await getColumnMeta('test.bool_col')).to.containSubset({ + name: 'bool_col', + isNullable: false, + dataType: dialect === 'postgres' ? 'bool' : 'tinyint', + }) + }) + + it('should add multiple columns', async () => { + const builder = ctx.db.schema + .alterTable('test') + .addColumn('another_col', 'text') + .addColumn('yet_another_col', 'integer') + + testSql(builder, dialect, { + postgres: { + sql: [ + 'alter table "test"', + 'add column "another_col" text,', + 'add column "yet_another_col" integer', + ], + parameters: [], + }, + mysql: { + sql: [ + 'alter table `test`', + 'add column `another_col` text,', + 'add column `yet_another_col` integer', + ], + parameters: [], + }, + sqlite: { + sql: [ + 'alter table "test"', + 'add column "another_col" text,', + 'add column "yet_another_col" integer', + ], + parameters: [], + }, + }) + + await builder.execute() + }) + } + }) + if (dialect === 'mysql') { describe('modify column', () => { it('should set column data type', async () => { @@ -1563,8 +1706,7 @@ for (const dialect of BUILT_IN_DIALECTS) { it('should add not null constraint for column', async () => { const builder = ctx.db.schema .alterTable('test') - .modifyColumn('varchar_col', 'varchar(255)') - .notNull() + .modifyColumn('varchar_col', 'varchar(255)', (cb) => cb.notNull()) testSql(builder, dialect, { mysql: { @@ -1579,14 +1721,9 @@ for (const dialect of BUILT_IN_DIALECTS) { }) it('should drop not null constraint for column', async () => { - expect( - (await getColumnMeta('test.varchar_col')).isNullable - ).to.equal(true) - await ctx.db.schema .alterTable('test') - .modifyColumn('varchar_col', 'varchar(255)') - .notNull() + .modifyColumn('varchar_col', 'varchar(255)', (cb) => cb.notNull()) .execute() expect( @@ -1612,6 +1749,28 @@ for (const dialect of BUILT_IN_DIALECTS) { (await getColumnMeta('test.varchar_col')).isNullable ).to.equal(true) }) + + it('should modify multiple columns', async () => { + const builder = ctx.db.schema + .alterTable('test') + .modifyColumn('varchar_col', 'varchar(255)') + .modifyColumn('integer_col', 'bigint') + + testSql(builder, dialect, { + mysql: { + sql: [ + 'alter table `test`', + 'modify column `varchar_col` varchar(255),', + 'modify column `integer_col` bigint', + ], + parameters: [], + }, + postgres: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await builder.execute() + }) }) } @@ -1620,8 +1779,7 @@ for (const dialect of BUILT_IN_DIALECTS) { it('should set default value', async () => { const builder = ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .setDefault('foo') + .alterColumn('varchar_col', (ac) => ac.setDefault('foo')) testSql(builder, dialect, { postgres: { @@ -1639,16 +1797,16 @@ for (const dialect of BUILT_IN_DIALECTS) { }) it('should drop default value', async () => { + const subject = 'varchar_col' + await ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .setDefault('foo') + .alterColumn(subject, (ac) => ac.setDefault('foo')) .execute() const builder = ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .dropDefault() + .alterColumn(subject, (ac) => ac.dropDefault()) testSql(builder, dialect, { postgres: { @@ -1661,7 +1819,6 @@ for (const dialect of BUILT_IN_DIALECTS) { }, sqlite: NOT_SUPPORTED, }) - await builder.execute() }) @@ -1669,8 +1826,7 @@ for (const dialect of BUILT_IN_DIALECTS) { it('should set column data type', async () => { const builder = ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .setDataType('text') + .alterColumn('varchar_col', (ac) => ac.setDataType('text')) testSql(builder, dialect, { postgres: { @@ -1690,8 +1846,7 @@ for (const dialect of BUILT_IN_DIALECTS) { it('should add not null constraint for column', async () => { const builder = ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .setNotNull() + .alterColumn('varchar_col', (ac) => ac.setNotNull()) testSql(builder, dialect, { postgres: { @@ -1711,14 +1866,12 @@ for (const dialect of BUILT_IN_DIALECTS) { it('should drop not null constraint for column', async () => { await ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .setNotNull() + .alterColumn('varchar_col', (ac) => ac.setNotNull()) .execute() const builder = ctx.db.schema .alterTable('test') - .alterColumn('varchar_col') - .dropNotNull() + .alterColumn('varchar_col', (ac) => ac.dropNotNull()) testSql(builder, dialect, { postgres: { @@ -1735,6 +1888,35 @@ for (const dialect of BUILT_IN_DIALECTS) { await builder.execute() }) } + + it('should alter multiple columns', async () => { + const builder = ctx.db.schema + .alterTable('test') + .alterColumn('varchar_col', (ac) => ac.setDefault('foo')) + .alterColumn('integer_col', (ac) => ac.setDefault(5)) + + testSql(builder, dialect, { + postgres: { + sql: [ + `alter table "test"`, + `alter column "varchar_col" set default 'foo',`, + `alter column "integer_col" set default 5`, + ], + parameters: [], + }, + mysql: { + sql: [ + 'alter table `test`', + "alter column `varchar_col` set default 'foo',", + 'alter column `integer_col` set default 5', + ], + parameters: [], + }, + sqlite: NOT_SUPPORTED, + }) + + await builder.execute() + }) }) } @@ -1761,6 +1943,49 @@ for (const dialect of BUILT_IN_DIALECTS) { await builder.execute() }) + + if (dialect !== 'sqlite') { + it('should drop multiple columns', async () => { + await ctx.db.schema + .alterTable('test') + .addColumn('text_col', 'text') + .execute() + + const builder = ctx.db.schema + .alterTable('test') + .dropColumn('varchar_col') + .dropColumn('text_col') + + testSql(builder, dialect, { + postgres: { + sql: [ + 'alter table "test"', + 'drop column "varchar_col",', + 'drop column "text_col"', + ], + parameters: [], + }, + mysql: { + sql: [ + 'alter table `test`', + 'drop column `varchar_col`,', + 'drop column `text_col`', + ], + parameters: [], + }, + sqlite: { + sql: [ + 'alter table "test"', + 'drop column "varchar_col",', + 'drop column "text_col"', + ], + parameters: [], + }, + }) + + await builder.execute() + }) + } }) describe('rename', () => { @@ -1828,108 +2053,106 @@ for (const dialect of BUILT_IN_DIALECTS) { await builder.execute() }) - }) - - describe('add column', () => { - it('should add a column', async () => { - const builder = ctx.db.schema - .alterTable('test') - .addColumn('bool_col', 'boolean') - .notNull() - - testSql(builder, dialect, { - postgres: { - sql: 'alter table "test" add column "bool_col" boolean not null', - parameters: [], - }, - mysql: { - sql: 'alter table `test` add column `bool_col` boolean not null', - parameters: [], - }, - sqlite: { - sql: 'alter table "test" add column "bool_col" boolean not null', - parameters: [], - }, - }) - - await builder.execute() - - expect(await getColumnMeta('test.bool_col')).to.containSubset({ - name: 'bool_col', - isNullable: false, - dataType: - dialect === 'postgres' - ? 'bool' - : dialect === 'sqlite' - ? 'boolean' - : 'tinyint', - }) - }) - - it('should add a column using a callback', async () => { - const builder = ctx.db.schema - .alterTable('test') - .addColumn('bool_col', 'boolean', (col) => col.notNull()) - - testSql(builder, dialect, { - postgres: { - sql: 'alter table "test" add column "bool_col" boolean not null', - parameters: [], - }, - mysql: { - sql: 'alter table `test` add column `bool_col` boolean not null', - parameters: [], - }, - sqlite: { - sql: 'alter table "test" add column "bool_col" boolean not null', - parameters: [], - }, - }) - - await builder.execute() - - expect(await getColumnMeta('test.bool_col')).to.containSubset({ - name: 'bool_col', - isNullable: false, - dataType: - dialect === 'postgres' - ? 'bool' - : dialect === 'sqlite' - ? 'boolean' - : 'tinyint', - }) - }) - if (dialect !== 'sqlite') { - it('should add a unique column', async () => { + if (dialect === 'mysql') { + it('should rename multiple columns', async () => { const builder = ctx.db.schema .alterTable('test') - .addColumn('bool_col', 'boolean') - .notNull() - .unique() + .renameColumn('varchar_col', 'text_col') + .renameColumn('integer_col', 'number_col') testSql(builder, dialect, { postgres: { - sql: 'alter table "test" add column "bool_col" boolean not null unique', + sql: [ + 'alter table "test"', + 'rename column "varchar_col" to "text_col",', + 'rename column "integer_col" to "number_col"', + ], parameters: [], }, mysql: { - sql: 'alter table `test` add column `bool_col` boolean not null unique', + sql: [ + 'alter table `test`', + 'rename column `varchar_col` to `text_col`,', + 'rename column `integer_col` to `number_col`', + ], parameters: [], }, sqlite: { - sql: 'alter table "test" add column "bool_col" boolean not null unique', + sql: [ + 'alter table "test"', + 'rename column "varchar_col" to "text_col",', + 'rename column "integer_col" to "number_col"', + ], parameters: [], }, }) await builder.execute() + }) + } + }) - expect(await getColumnMeta('test.bool_col')).to.containSubset({ - name: 'bool_col', - isNullable: false, - dataType: dialect === 'postgres' ? 'bool' : 'tinyint', + describe('mixed column alterations', () => { + if (dialect === 'postgres') { + it('should alter multiple columns in various ways', async () => { + const builder = ctx.db.schema + .alterTable('test') + .addColumn('another_varchar_col', 'varchar(255)') + .alterColumn('varchar_col', (ac) => ac.setDefault('foo')) + .dropColumn('integer_col') + + testSql(builder, dialect, { + postgres: { + sql: [ + `alter table "test"`, + `add column "another_varchar_col" varchar(255),`, + `alter column "varchar_col" set default 'foo',`, + `drop column "integer_col"`, + ], + parameters: [], + }, + mysql: NOT_SUPPORTED, + sqlite: NOT_SUPPORTED, + }) + + await builder.execute() + }) + } + + if (dialect === 'mysql') { + it('should alter multiple columns in various ways', async () => { + await ctx.db.schema + .alterTable('test') + .addColumn('rename_me', 'text') + .addColumn('modify_me', 'boolean') + .execute() + + const builder = ctx.db.schema + .alterTable('test') + .addColumn('another_varchar_col', 'varchar(255)') + .alterColumn('varchar_col', (ac) => ac.setDefault('foo')) + .dropColumn('integer_col') + .renameColumn('rename_me', 'text_col') + .modifyColumn('modify_me', 'bigint') + + testSql(builder, dialect, { + postgres: NOT_SUPPORTED, + mysql: { + sql: [ + 'alter table `test`', + 'add column `another_varchar_col` varchar(255),', + "alter column `varchar_col` set default 'foo',", + 'drop column `integer_col`,', + 'rename column `rename_me` to `text_col`,', + 'modify column `modify_me` bigint', + ], + parameters: [], + }, + sqlite: NOT_SUPPORTED, }) + + await builder.execute() }) } }) @@ -2106,6 +2329,31 @@ for (const dialect of BUILT_IN_DIALECTS) { }) }) } + + it('should alter a table calling query builder functions', async () => { + const builder = ctx.db.schema + .alterTable('test') + .call((builder) => + builder.addColumn('abc', 'integer', (col) => col.defaultTo('42')) + ) + + testSql(builder, dialect, { + postgres: { + sql: [`alter table "test" add column "abc" integer default '42'`], + parameters: [], + }, + mysql: { + sql: ["alter table `test` add column `abc` integer default '42'"], + parameters: [], + }, + sqlite: { + sql: [`alter table "test" add column "abc" integer default '42'`], + parameters: [], + }, + }) + + await builder.execute() + }) }) async function dropTestTables(): Promise { diff --git a/test/node/src/update.test.ts b/test/node/src/update.test.ts index 1dba94f7a..5531aaaaa 100644 --- a/test/node/src/update.test.ts +++ b/test/node/src/update.test.ts @@ -113,7 +113,7 @@ for (const dialect of BUILT_IN_DIALECTS) { expect(person.last_name).to.equal('Catto') }) - it('should update update using a raw expression', async () => { + it('should update one row using a raw expression', async () => { const query = ctx.db .updateTable('person') .set({ @@ -136,10 +136,21 @@ for (const dialect of BUILT_IN_DIALECTS) { }, }) - await query.execute() + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(UpdateResult) + expect(result.numUpdatedRows).to.equal(1n) + + const jennifer = await ctx.db + .selectFrom('person') + .where('first_name', '=', 'Jennifer') + .select('last_name') + .executeTakeFirstOrThrow() + + expect(jennifer.last_name).to.equal('Jennifer') }) - it('undefined values should be ignored', async () => { + it('should update one row while ignoring undefined values', async () => { const query = ctx.db .updateTable('person') .set({ id: undefined, first_name: 'Foo', last_name: 'Barson' }) @@ -160,11 +171,25 @@ for (const dialect of BUILT_IN_DIALECTS) { }, }) - await query.execute() + const result = await query.executeTakeFirst() + + expect(result).to.be.instanceOf(UpdateResult) + expect(result.numUpdatedRows).to.equal(1n) + + const female = await ctx.db + .selectFrom('person') + .where('gender', '=', 'female') + .select(['first_name', 'last_name']) + .executeTakeFirstOrThrow() + + expect(female).to.deep.equal({ + first_name: 'Foo', + last_name: 'Barson', + }) }) if (dialect === 'postgres' || dialect === 'sqlite') { - it('should return updated rows when `returning` is used', async () => { + it('should update some rows and return updated rows when `returning` is used', async () => { const query = ctx.db .updateTable('person') .set({ last_name: 'Barson' }) @@ -196,7 +221,7 @@ for (const dialect of BUILT_IN_DIALECTS) { ]) }) - it('conditional returning statement should add optional fields', async () => { + it('should update all rows, returning some fields of updated rows, and conditionally returning additional fields', async () => { const condition = true const query = ctx.db @@ -206,10 +231,11 @@ for (const dialect of BUILT_IN_DIALECTS) { .if(condition, (qb) => qb.returning('last_name')) const result = await query.executeTakeFirstOrThrow() + expect(result.last_name).to.equal('Barson') }) - it('should join a table when `from` is called', async () => { + it('should update some rows and join a table when `from` is called', async () => { const query = ctx.db .updateTable('person') .from('pet') @@ -233,6 +259,7 @@ for (const dialect of BUILT_IN_DIALECTS) { }) const result = await query.execute() + expect(result[0].first_name).to.equal('Doggo') }) }