Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate bulkInsertQuery & insertQuery to ts #16988

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/dialects/abstract/data-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,10 @@ export class BIGINT extends BaseIntegerDataType {
protected _checkOptionSupport(dialect: AbstractDialect) {
super._checkOptionSupport(dialect);

if (!dialect.supports.dataTypes.BIGINT) {
ephys marked this conversation as resolved.
Show resolved Hide resolved
throwUnsupportedDataType(dialect, 'BIGINT');
}

if (this.options.unsigned && !this._supportsNativeUnsigned(dialect)) {
throwUnsupportedDataType(dialect, `${this.getDataTypeId()}.UNSIGNED`);
}
Expand Down
48 changes: 24 additions & 24 deletions packages/core/src/dialects/abstract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,12 @@ export interface SupportableExactDecimalOptions extends SupportableDecimalNumber
}

export type DialectSupports = {
'DEFAULT': boolean,
'DEFAULT VALUES': boolean,
'VALUES ()': boolean,
// TODO: rename to `update.limit`
'LIMIT ON UPDATE': boolean,
'ON DUPLICATE KEY': boolean,
'ORDER NULLS': boolean,
'UNION': boolean,
'UNION ALL': boolean,
'RIGHT JOIN': boolean,
EXCEPTION: boolean,

forShare?: 'LOCK IN SHARE MODE' | 'FOR SHARE' | undefined,
lock: boolean,
Expand All @@ -78,8 +73,6 @@ export type DialectSupports = {
/* does the dialect support updating autoincrement fields */
update: boolean,
},
/* Do we need to say DEFAULT for bulk insert */
bulkDefault: boolean,
/**
* Whether this dialect has native support for schemas.
* For the purposes of Sequelize, a Schema is considered to be a grouping of tables.
Expand All @@ -100,12 +93,20 @@ export type DialectSupports = {
},
migrations: boolean,
upserts: boolean,
inserts: {
ignoreDuplicates: string, /* dialect specific words for INSERT IGNORE or DO NOTHING */
updateOnDuplicate: boolean | string, /* whether dialect supports ON DUPLICATE KEY UPDATE */
onConflictDoNothing: string, /* dialect specific words for ON CONFLICT DO NOTHING */
onConflictWhere: boolean, /* whether dialect supports ON CONFLICT WHERE */
conflictFields: boolean, /* whether the dialect supports specifying conflict fields or not */
insert: {
/** Indicates if the dialect supports DEFAULT */
default: boolean,
/** Indicates if the dialect supports DEFAULT VALUES */
defaultValues: boolean,
exception: boolean,
/** Indicates if the dialect supports IGNORE */
ignore: boolean,
/** Indicates if the dialect supports ON CONFLICT */
onConflict: boolean,
/** Indicates if the dialect supports returning values */
returning: boolean,
/** Indicates if the dialect supports UPDATE ON DUPLICATE */
updateOnDuplicate: boolean,
},
constraints: {
restrict: boolean,
Expand Down Expand Up @@ -167,6 +168,8 @@ export type DialectSupports = {
DOUBLE: SupportableFloatOptions,
/** This dialect supports arbitrary precision numbers */
DECIMAL: false | SupportableExactDecimalOptions,
/** This dialect supports big integers */
BIGINT: boolean,
/**
* The dialect is considered to support JSON if it provides either:
* - A JSON data type.
Expand Down Expand Up @@ -270,16 +273,11 @@ export abstract class AbstractDialect {
* When changing a default, ensure the implementations still properly declare which feature they support.
*/
static readonly supports: DialectSupports = {
DEFAULT: true,
'DEFAULT VALUES': false,
'VALUES ()': false,
'LIMIT ON UPDATE': false,
'ON DUPLICATE KEY': true,
'ORDER NULLS': false,
UNION: true,
'UNION ALL': true,
'RIGHT JOIN': true,
EXCEPTION: false,
lock: false,
lockOf: false,
lockKey: false,
Expand All @@ -292,7 +290,6 @@ export abstract class AbstractDialect {
defaultValue: true,
update: true,
},
bulkDefault: false,
schemas: false,
multiDatabases: false,
transactions: true,
Expand All @@ -302,12 +299,14 @@ export abstract class AbstractDialect {
},
migrations: true,
upserts: true,
inserts: {
ignoreDuplicates: '',
insert: {
default: true,
defaultValues: false,
exception: false,
ignore: false,
onConflict: false,
returning: false,
updateOnDuplicate: false,
onConflictDoNothing: '',
onConflictWhere: false,
conflictFields: false,
},
constraints: {
restrict: true,
Expand Down Expand Up @@ -352,6 +351,7 @@ export abstract class AbstractDialect {
REAL: { NaN: false, infinity: false, zerofill: false, unsigned: false, scaleAndPrecision: false },
DOUBLE: { NaN: false, infinity: false, zerofill: false, unsigned: false, scaleAndPrecision: false },
DECIMAL: { constrained: true, unconstrained: false, NaN: false, infinity: false, zerofill: false, unsigned: false },
BIGINT: true,
CIDR: false,
MACADDR: false,
MACADDR8: false,
Expand Down
122 changes: 119 additions & 3 deletions packages/core/src/dialects/abstract/query-generator-internal.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { inspect } from 'node:util';
import { Deferrable } from '../../deferrable.js';
import type { AssociationPath } from '../../expression-builders/association-path.js';
import type { Attribute } from '../../expression-builders/attribute.js';
import { BaseSqlExpression } from '../../expression-builders/base-sql-expression.js';
import type { Cast } from '../../expression-builders/cast.js';
import type { Col } from '../../expression-builders/col.js';
import { Col } from '../../expression-builders/col.js';
import type { DialectAwareFn } from '../../expression-builders/dialect-aware-fn.js';
import type { Fn } from '../../expression-builders/fn.js';
import type { JsonPath } from '../../expression-builders/json-path.js';
import type { Literal } from '../../expression-builders/literal.js';
import { Literal } from '../../expression-builders/literal.js';
import type { AttributeOptions } from '../../model.js';
import type { Sequelize } from '../../sequelize.js';
import { joinSQLFragments } from '../../utils/join-sql-fragments.js';
import { EMPTY_ARRAY } from '../../utils/object.js';
import { injectReplacements } from '../../utils/sql.js';
import { attributeTypeToSql } from './data-types-utils.js';
import { VIRTUAL } from './data-types.js';
import type { EscapeOptions, TableNameOrModel } from './query-generator-typescript.js';
import type { AddLimitOffsetOptions, GetConstraintSnippetQueryOptions } from './query-generator.types.js';
import type {
AddLimitOffsetOptions,
GetConstraintSnippetQueryOptions,
GetReturnFieldsOptions,
InsertQueryOptions,
} from './query-generator.types.js';
import { WhereSqlBuilder, wrapAmbiguousWhere } from './where-sql-builder.js';
import type { AbstractDialect } from './index.js';

Expand Down Expand Up @@ -312,4 +321,111 @@ Only named replacements (:name) are allowed in literal() because we cannot guara
addLimitAndOffset(_options: AddLimitOffsetOptions): string {
throw new Error(`addLimitAndOffset has not been implemented in ${this.dialect.name}.`);
}

/**
* Creates a function that can be used to collect bind parameters.
*
* @param bind A mutable object to which bind parameters will be added.
*/
bindParam(bind: Record<string, unknown>): (value: unknown) => string {
let i = 0;

return (value: unknown): string => {
const bindName = `sequelize_${++i}`;

bind[bindName] = value;

return `$${bindName}`;
};
}

/**
* Returns the SQL fragment to handle returning the attributes from an insert/update query.
*
* @param options An object with options.
* @param modelAttributes A map with the model attributes.
*/
getReturnFields(options: GetReturnFieldsOptions, modelAttributes: Map<string, AttributeOptions>): string[] {
const returnFields: string[] = [];

if (Array.isArray(options.returning)) {
returnFields.push(...options.returning.map(field => {
if (typeof field === 'string') {
return this.queryGenerator.quoteIdentifier(field);
} else if (field instanceof Literal) {
return this.queryGenerator.formatSqlExpression(field);
} else if (field instanceof Col) {
return this.queryGenerator.formatSqlExpression(field);
}

throw new Error(`Unsupported value in "returning" option: ${inspect(field)}. This option only accepts true, false, or an array of strings, col() or literal().`);
}));
} else if (modelAttributes.size > 0) {
const attributes = [...modelAttributes.entries()]
.map(([name, attr]) => ({ ...attr, columnName: this.queryGenerator.quoteIdentifier(attr.columnName ?? name) }))
.filter(({ type }) => !(type instanceof VIRTUAL))
.map(({ columnName }) => columnName);

returnFields.push(...attributes);
}

if (returnFields.length === 0) {
returnFields.push('*');
}

return returnFields;
}

/**
* Generates an SQL fragment to handle the ON DUPLICATE KEY UPDATE clause of an insert query.
*
* @param options
* @param values
*/
generateUpdateOnDuplicateKeysFragment(options: InsertQueryOptions, values: Map<string, string> = new Map()): string {
const conflictFragments: string[] = [];
const updateOnDuplicateKeys = options.updateOnDuplicate ?? [];
if (this.dialect.supports.insert.onConflict) {
// If no conflict target columns were specified, use the primary key names from options.upsertKeys
const conflictKeys = options.upsertKeys?.map(attr => this.queryGenerator.quoteIdentifier(attr)) ?? [];
const updateKeys = updateOnDuplicateKeys.map(attr => `${this.queryGenerator.quoteIdentifier(attr)}=EXCLUDED.${this.queryGenerator.quoteIdentifier(attr)}`);
conflictFragments.push(`ON CONFLICT (${conflictKeys.join(',')})`);
if (options.conflictWhere) {
conflictFragments.push(this.queryGenerator.whereQuery(options.conflictWhere, options));
}

// if update keys are provided, then apply them here. if there are no updateKeys provided, then do not try to
// do an update. Instead, fall back to DO NOTHING.
if (updateKeys.length === 0) {
conflictFragments.push('DO NOTHING');
} else {
conflictFragments.push('DO UPDATE SET', updateKeys.join(','));
}
} else if (this.dialect.supports.insert.updateOnDuplicate) {
const valueKeys = updateOnDuplicateKeys.map(attr => {
const value = values.get(attr);

return value ? `${this.queryGenerator.quoteIdentifier(attr)}=${value}` : `${this.queryGenerator.quoteIdentifier(attr)}=VALUES(${this.queryGenerator.quoteIdentifier(attr)})`;
});
// the rough equivalent to ON CONFLICT DO NOTHING in mysql, etc is ON DUPLICATE KEY UPDATE id = id
// So, if no update values were provided, fall back to the identifier columns provided in the upsertKeys array.
// This will be the primary key in most cases, but it could be some other constraint.
if (valueKeys.length === 0 && options.upsertKeys) {
valueKeys.push(...options.upsertKeys.map(attr => `${this.queryGenerator.quoteIdentifier(attr)}=VALUES(${this.queryGenerator.quoteIdentifier(attr)})`));
}

// edge case... but if for some reason there were no valueKeys, and there were also no upsertKeys... then we
// can no longer build the requested query without a syntax error. Let's throw something more graceful here
// so the devs know what the problem is.
if (valueKeys.length === 0) {
throw new Error('No update values found for ON DUPLICATE KEY UPDATE clause, and no identifier fields could be found to use instead.');
}

conflictFragments.push(`ON DUPLICATE KEY UPDATE ${valueKeys.join(',')}`);
} else {
throw new Error(`Updating on duplicate keys is not supported by ${this.dialect.name}.`);
}

return joinSQLFragments(conflictFragments);
}
}