Skip to content

Commit

Permalink
feat: migrate insert & bulkInsert to ts
Browse files Browse the repository at this point in the history
  • Loading branch information
lohart13 committed Feb 8, 2024
1 parent 117d0f3 commit 17fa3fd
Show file tree
Hide file tree
Showing 10 changed files with 104 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface Escapeable extends Bindable {

export interface QueryWithBindParams {
query: string;
bind?: BindOrReplacements | undefined;
bind?: Record<string, unknown> | undefined;
}

// keep CREATE_DATABASE_QUERY_SUPPORTABLE_OPTIONS updated when modifying this
Expand Down
89 changes: 89 additions & 0 deletions packages/core/src/dialects/abstract/query-interface-typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import isEmpty from 'lodash/isEmpty';
import { Deferrable } from '../../deferrable';
import type { ConstraintChecking } from '../../deferrable';
import { BaseError } from '../../errors';
import { Model } from '../../model';
import type { AttributeOptions, ModelStatic } from '../../model';
import { setTransactionFromCls } from '../../model-internals.js';
import { QueryTypes } from '../../query-types';
import type { QueryRawOptions, QueryRawOptionsWithType, Sequelize } from '../../sequelize';
Expand All @@ -13,6 +15,7 @@ import {
showAllToListSchemas,
showAllToListTables,
} from '../../utils/deprecations';
import { assertNoReservedBind, combineBinds } from '../../utils/sql';
import type { RequiredBy } from '../../utils/types';
import type { Connection } from './connection-manager.js';
import type { AbstractQueryGenerator } from './query-generator';
Expand All @@ -32,9 +35,11 @@ import type {
DropSchemaOptions,
FetchDatabaseVersionOptions,
ListDatabasesOptions,
QiBulkInsertOptions,
QiDropAllSchemasOptions,
QiDropAllTablesOptions,
QiDropTableOptions,
QiInsertOptions,
QiListSchemasOptions,
QiListTablesOptions,
QiTruncateTableOptions,
Expand Down Expand Up @@ -737,4 +742,88 @@ export class AbstractQueryInterfaceTypeScript {

return this.sequelize.queryRaw(sql, { ...bulkDeleteOptions, raw: true, type: QueryTypes.DELETE });
}

/**
* Insert multiple records into a table
*
* @example
* queryInterface.bulkInsert('roles', [{
* label: 'user',
* createdAt: new Date(),
* updatedAt: new Date()
* }, {
* label: 'admin',
* createdAt: new Date(),
* updatedAt: new Date()
* }]);
*
* @param tableName Table name to insert record to
* @param values List of records to insert
* @param options Various options, please see Model.bulkCreate options
* @param attributeHash Various attributes mapped by field name
*/
async bulkInsert(
tableName: TableNameOrModel,
values: Array<Record<string, unknown>>,
options?: QiBulkInsertOptions,
attributeHash?: Record<string, AttributeOptions>,
): Promise<Array<Record<string, unknown>>> {
const bulkInsertOptions = { ...options };
const sql = this.queryGenerator.bulkInsertQuery(tableName, values, bulkInsertOptions, attributeHash);

// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete bulkInsertOptions.replacements;

const results = await this.sequelize.queryRaw<Array<Record<string, unknown>>>(sql, {
...bulkInsertOptions,
type: QueryTypes.INSERT,
});

return results[0];
}

/**
* Inserts a new record
*
* @param tableName
* @param values
* @param options
* @param instanceOrAttributeHash
*/
async insert<M extends Model>(
tableName: TableNameOrModel,
values: Record<string, unknown>,
options?: QiInsertOptions,
instanceOrAttributeHash?: M | Record<string, AttributeOptions>,
): Promise<[M | Record<string, unknown>, number]> {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

let attributeHash: Record<string, AttributeOptions> | undefined;
const insertOptions = { ...options };
if (instanceOrAttributeHash instanceof Model) {
const model = (instanceOrAttributeHash.constructor as ModelStatic<M>);
insertOptions.model = model;
insertOptions.instance = instanceOrAttributeHash;
insertOptions.hasTrigger = model.modelDefinition.options.hasTrigger ?? false;
} else {
attributeHash = instanceOrAttributeHash;
}

const { query, bind } = this.queryGenerator.insertQuery(tableName, values, options, attributeHash);

// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete insertOptions.replacements;
insertOptions.bind = combineBinds(insertOptions.bind ?? {}, bind ?? {});

if (instanceOrAttributeHash instanceof Model) {
const results = await this.sequelize.queryRaw<M>(query, { ...insertOptions, type: QueryTypes.INSERT });
results[0].isNewRecord = false;

return results;
}

return this.sequelize.queryRaw(query, { ...insertOptions, type: QueryTypes.INSERT });
}
}
22 changes: 1 addition & 21 deletions packages/core/src/dialects/abstract/query-interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type { RemoveIndexQueryOptions, TableNameOrModel } from './query-generato
import type { AbstractQueryGenerator, AddColumnQueryOptions } from './query-generator.js';
import type { AddLimitOffsetOptions } from './query-generator.types.js';
import { AbstractQueryInterfaceTypeScript } from './query-interface-typescript';
import type { ColumnsDescription, QiBulkInsertOptions, QiInsertOptions } from './query-interface.types.js';
import type { ColumnsDescription } from './query-interface.types.js';
import type { WhereOptions } from './where-sql-builder-types.js';

interface Replaceable {
Expand Down Expand Up @@ -327,26 +327,6 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript {
*/
nameIndexes(indexes: string[], rawTablename: string): Promise<void>;

/**
* Inserts multiple records at once
*/
bulkInsert(
tableName: TableNameOrModel,
values: Array<Record<string, unknown>>,
options?: QiBulkInsertOptions,
attributeHash?: Record<string, AttributeOptions>,
): Promise<object[]>;

/**
* Inserts a new record
*/
insert(
instance: Model | null,
tableName: TableNameOrModel,
values: Record<string, unknown>,
options?: QiInsertOptions,
): Promise<object>;

/**
* Inserts or Updates a record in the database
*/
Expand Down
64 changes: 0 additions & 64 deletions packages/core/src/dialects/abstract/query-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,36 +340,6 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript {
return await this.sequelize.queryRaw(sql, options);
}

async insert(instance, tableName, values, options) {
if (options?.bind) {
assertNoReservedBind(options.bind);
}

options = cloneDeep(options) ?? {};
const modelDefinition = instance?.constructor.modelDefinition;

options.hasTrigger = modelDefinition?.options.hasTrigger;
const { query, bind } = this.queryGenerator.insertQuery(
tableName,
values,
options,
);

options.type = QueryTypes.INSERT;
options.instance = instance;

// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete options.replacements;
options.bind = combineBinds(options.bind, bind);

const results = await this.sequelize.queryRaw(query, options);
if (instance) {
results[0].isNewRecord = false;
}

return results;
}

/**
* Upsert
*
Expand Down Expand Up @@ -444,40 +414,6 @@ export class AbstractQueryInterface extends AbstractQueryInterfaceTypeScript {
return await this.sequelize.queryRaw(query, options);
}

/**
* Insert multiple records into a table
*
* @example
* queryInterface.bulkInsert('roles', [{
* label: 'user',
* createdAt: new Date(),
* updatedAt: new Date()
* }, {
* label: 'admin',
* createdAt: new Date(),
* updatedAt: new Date()
* }]);
*
* @param {string} tableName Table name to insert record to
* @param {Array} records List of records to insert
* @param {object} options Various options, please see Model.bulkCreate options
* @param {object} attributes Various attributes mapped by field name
*
* @returns {Promise}
*/
async bulkInsert(tableName, records, options, attributes) {
options = { ...options, type: QueryTypes.INSERT };

const sql = this.queryGenerator.bulkInsertQuery(tableName, records, options, attributes);

// unlike bind, replacements are handled by QueryGenerator, not QueryRaw
delete options.replacements;

const results = await this.sequelize.queryRaw(sql, options);

return results[0];
}

async update(instance, tableName, values, where, options) {
if (options?.bind) {
assertNoReservedBind(options.bind);
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -3748,7 +3748,7 @@ Instead of specifying a Model, either:

if (this.isNewRecord) {
query = 'insert';
args = [this, this.constructor, values, options];
args = [this.constructor, values, options, this];
}

const [result, rowsUpdated] = await this.constructor.queryInterface[query](...args);
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/sequelize.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,7 +951,7 @@ export class Sequelize extends SequelizeTypeScript {
/* eslint-disable max-len -- these signatures are more readable if they are all aligned */
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.UPDATE>): Promise<[undefined, number]>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.BULKUPDATE>): Promise<number>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.INSERT>): Promise<[number, number]>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.INSERT>): Promise<[Record<string, unknown>, number]>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.UPSERT>): Promise<number>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.DELETE>): Promise<number>;
query(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.DESCRIBE>): Promise<ColumnsDescription>;
Expand All @@ -960,6 +960,7 @@ export class Sequelize extends SequelizeTypeScript {
query<M extends Model>(sql: string | BaseSqlExpression, options: QueryOptionsWithModel<M>): Promise<M[]>;
query<T extends object>(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.SELECT> & { plain: true }): Promise<T | null>;
query<T extends object>(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.SELECT>): Promise<T[]>;
query<T extends object>(sql: string | BaseSqlExpression, options: QueryOptionsWithType<QueryTypes.INSERT>): Promise<[T, number]>;
query(sql: string | BaseSqlExpression, options: (QueryOptions | QueryOptionsWithType<QueryTypes.RAW>) & { plain: true }): Promise<{ [key: string]: unknown } | null>;
query(sql: string | BaseSqlExpression, options?: QueryOptions | QueryOptionsWithType<QueryTypes.RAW>): Promise<[unknown[], unknown]>;

Expand All @@ -971,7 +972,7 @@ export class Sequelize extends SequelizeTypeScript {
*/
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.UPDATE>): Promise<[undefined, number]>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.BULKUPDATE>): Promise<number>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.INSERT>): Promise<[number, number]>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.INSERT>): Promise<[Record<string, unknown>, number]>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.UPSERT>): Promise<number>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.DELETE>): Promise<number>;
queryRaw(sql: string, options: QueryRawOptionsWithType<QueryTypes.DESCRIBE>): Promise<ColumnsDescription>;
Expand All @@ -980,6 +981,7 @@ export class Sequelize extends SequelizeTypeScript {
queryRaw<M extends Model>(sql: string, options: QueryRawOptionsWithModel<M>): Promise<M[]>;
queryRaw<T extends object>(sql: string, options: QueryRawOptionsWithType<QueryTypes.SELECT> & { plain: true }): Promise<T | null>;
queryRaw<T extends object>(sql: string, options: QueryRawOptionsWithType<QueryTypes.SELECT>): Promise<T[]>;
queryRaw<T extends object>(sql: string, options: QueryRawOptionsWithType<QueryTypes.INSERT>): Promise<[T, number]>;
queryRaw(sql: string, options: (QueryRawOptions | QueryRawOptionsWithType<QueryTypes.RAW>) & { plain: true }): Promise<{ [key: string]: unknown } | null>;
queryRaw(sql: string, options?: QueryRawOptions | QueryRawOptionsWithType<QueryTypes.RAW>): Promise<[unknown[], unknown]>;
/* eslint-enable max-len */
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/types/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ async function test() {

expectTypeOf(
await sequelize.query('INSERT into test set test=1', { type: QueryTypes.INSERT }),
).toEqualTypeOf<[number, number]>();
).toEqualTypeOf<[Record<string, unknown>, number]>();
}

// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- false positive :/
Expand Down
3 changes: 2 additions & 1 deletion packages/core/test/types/query-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,6 @@ async function test() {

await queryInterface.upsert('test', { a: 1 }, { b: 2 }, { c: 3 }, { model: TestModel });

await queryInterface.insert(null, 'test', {});
await queryInterface.insert('test', {});
await queryInterface.insert(TestModel, {});
}
2 changes: 1 addition & 1 deletion packages/core/test/unit/instance/set.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe(Support.getTestDialectTeaser('Instance'), () => {

describe('custom setter', () => {
before(function () {
this.stubCreate = sinon.stub(current.queryInterface, 'insert').callsFake(async instance => [instance, 1]);
this.stubCreate = sinon.stub(current.queryInterface, 'insert').callsFake(async (_table, _values, _options, instance) => [instance, 1]);
});

after(function () {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/test/unit/query-interface/insert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('QueryInterface#insert', () => {
it('does not parse replacements outside of raw sql', async () => {
const stub = sinon.stub(sequelize, 'queryRaw');

await sequelize.queryInterface.insert(null, User, {
await sequelize.queryInterface.insert(User, {
firstName: 'Zoe',
}, {
returning: sequelize.dialect.supports.insert.returning ? [':data'] : undefined,
Expand All @@ -42,7 +42,7 @@ describe('QueryInterface#insert', () => {
it('throws if a bind parameter name starts with the reserved "sequelize_" prefix', async () => {
sinon.stub(sequelize, 'queryRaw');

await expect(sequelize.queryInterface.insert(null, User, {
await expect(sequelize.queryInterface.insert(User, {
firstName: literal('$sequelize_test'),
}, {
bind: {
Expand All @@ -54,7 +54,7 @@ describe('QueryInterface#insert', () => {
it('merges user-provided bind parameters with sequelize-generated bind parameters (object bind)', async () => {
const stub = sinon.stub(sequelize, 'queryRaw');

await sequelize.queryInterface.insert(null, User, {
await sequelize.queryInterface.insert(User, {
firstName: literal('$firstName'),
lastName: 'Doe',
}, {
Expand All @@ -78,7 +78,7 @@ describe('QueryInterface#insert', () => {
it('merges user-provided bind parameters with sequelize-generated bind parameters (array bind)', async () => {
const stub = sinon.stub(sequelize, 'queryRaw');

await sequelize.queryInterface.insert(null, User, {
await sequelize.queryInterface.insert(User, {
firstName: literal('$1'),
lastName: 'Doe',
}, {
Expand Down

0 comments on commit 17fa3fd

Please sign in to comment.