Skip to content

Commit

Permalink
feat(mysql): support max_execution_time optimizer hint (#15341)
Browse files Browse the repository at this point in the history
Co-authored-by: Rik Smale <13023439+WikiRik@users.noreply.github.com>
  • Loading branch information
SeongJaeSong and WikiRik committed May 4, 2023
1 parent c3e1ae0 commit fc3d6aa
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 4 deletions.
6 changes: 6 additions & 0 deletions packages/core/src/dialects/abstract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ export type DialectSupports = {
dropTable: {
cascade: boolean,
},
maxExecutionTimeHint: {
select: boolean,
},
truncate: {
cascade: boolean,
},
Expand Down Expand Up @@ -338,6 +341,9 @@ export abstract class AbstractDialect {
dropTable: {
cascade: false,
},
maxExecutionTimeHint: {
select: false,
},
truncate: {
cascade: false,
},
Expand Down
16 changes: 15 additions & 1 deletion packages/core/src/dialects/abstract/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2172,10 +2172,24 @@ export class AbstractQueryGenerator extends AbstractQueryGeneratorTypeScript {
throw new sequelizeError.QueryError(message.replace(/ +/g, ' '));
}

_validateSelectOptions(options) {
if (options.maxExecutionTimeHintMs != null && !this.dialect.supports.maxExecutionTimeHint.select) {
throw new Error(`The maxExecutionTimeMs option is not supported by ${this.dialect.name}`);
}
}

_getBeforeSelectAttributesFragment(_options) {
return '';
}

selectFromTableFragment(options, model, attributes, tables, mainTableAs) {
this._throwOnEmptyAttributes(attributes, { modelName: model && model.name, as: mainTableAs });

let fragment = `SELECT ${attributes.join(', ')} FROM ${tables}`;
this._validateSelectOptions(options);

let fragment = 'SELECT';
fragment += this._getBeforeSelectAttributesFragment(options);
fragment += ` ${attributes.join(', ')} FROM ${tables}`;

if (mainTableAs) {
fragment += ` AS ${mainTableAs}`;
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/dialects/mariadb/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export class MariaDbQueryGenerator extends MariaDbQueryGeneratorTypeScript {
return ['MYSQL', 'INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'mysql', 'information_schema', 'performance_schema'];
}

_getBeforeSelectAttributesFragment(_options) {
return '';
}

addColumnQuery(table, key, dataType, options = {}) {
const ifNotExists = options.ifNotExists ? 'IF NOT EXISTS' : '';

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/dialects/mssql/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,11 @@ export class MsSqlQueryGenerator extends MsSqlQueryGeneratorTypeScript {
selectFromTableFragment(options, model, attributes, tables, mainTableAs, where) {
this._throwOnEmptyAttributes(attributes, { modelName: model && model.name, as: mainTableAs });

// mssql overwrite the abstract selectFromTableFragment function.
if (options.maxExecutionTimeHintMs != null) {
throw new Error(`The maxExecutionTimeMs option is not supported by ${this.dialect.name}`);
}

return joinSQLFragments([
'SELECT',
attributes.join(', '),
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/dialects/mysql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export class MysqlDialect extends AbstractDialect {
jsonOperations: true,
REGEXP: true,
globalTimeZoneConfig: true,
maxExecutionTimeHint: {
select: true,
},
},
);

Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/dialects/mysql/query-generator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

import { inspect } from 'node:util';
import { rejectInvalidOptions } from '../../utils/check';
import { addTicks } from '../../utils/dialect';
import { joinSQLFragments } from '../../utils/join-sql-fragments';
Expand Down Expand Up @@ -459,6 +460,25 @@ export class MySqlQueryGenerator extends MySqlQueryGeneratorTypeScript {
';',
]);
}

_getBeforeSelectAttributesFragment(options) {
let fragment = '';

const MINIMUM_EXECUTION_TIME_VALUE = 0;
const MAXIMUM_EXECUTION_TIME_VALUE = 4_294_967_295;

if (options.maxExecutionTimeHintMs != null) {
if (Number.isSafeInteger(options.maxExecutionTimeHintMs)
&& options.maxExecutionTimeHintMs >= MINIMUM_EXECUTION_TIME_VALUE
&& options.maxExecutionTimeHintMs <= MAXIMUM_EXECUTION_TIME_VALUE) {
fragment += ` /*+ MAX_EXECUTION_TIME(${options.maxExecutionTimeHintMs}) */`;
} else {
throw new Error(`maxExecutionTimeMs must be between ${MINIMUM_EXECUTION_TIME_VALUE} and ${MAXIMUM_EXECUTION_TIME_VALUE}, but it is ${inspect(options.maxExecutionTimeHintMs)}`);
}
}

return fragment;
}
}

/**
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/model.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,13 +776,21 @@ export interface IndexHintable {
indexHints?: IndexHint[];
}

export interface MaxExecutionTimeHintable {
/**
* This sets the max execution time for MySQL.
*/
maxExecutionTimeHintMs?: number;
}

/**
* Options that are passed to any model creating a SELECT query
*
* A hash of options to describe the scope of the search
*/
export interface FindOptions<TAttributes = any>
extends QueryOptions, Filterable<TAttributes>, Projectable, Paranoid, IndexHintable, SearchPathable {
extends
QueryOptions, Filterable<TAttributes>, Projectable, Paranoid, IndexHintable, SearchPathable, MaxExecutionTimeHintable {
/**
* A list of associations to eagerly load using a left join (a single association is also supported).
*
Expand Down Expand Up @@ -915,7 +923,7 @@ export interface NonNullFindByPkOptions<M extends Model> extends Omit<NonNullFin
* Options for Model.count method
*/
export interface CountOptions<TAttributes = any>
extends Logging, Transactionable, Filterable<TAttributes>, Projectable, Paranoid, Poolable {
extends Logging, Transactionable, Filterable<TAttributes>, Projectable, Paranoid, Poolable, MaxExecutionTimeHintable {
/**
* Include options. See `find` for details
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/core/test/types/max-execution-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { IndexHints } from '@sequelize/core';
import { User } from './models/user';

User.findAll({
maxExecutionTimeHintMs: 1000,
});
19 changes: 18 additions & 1 deletion packages/core/test/unit/query-generator/select-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai';
import type { InferAttributes, Model } from '@sequelize/core';
import { DataTypes, Op, or, sql as sqlTag } from '@sequelize/core';
import { _validateIncludedElements } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-internals.js';
import { expectsql, sequelize } from '../../support';
import { expectsql, getTestDialect, sequelize } from '../../support';

const { attribute, col, cast, where, fn, literal } = sqlTag;

Expand Down Expand Up @@ -659,4 +659,21 @@ Only named replacements (:name) are allowed in literal() because we cannot guara
});
});
});

describe('optimizer hints', () => {
const dialectName = getTestDialect();

it('max execution time hint', () => {
const notSupportedError = new Error(`The maxExecutionTimeMs option is not supported by ${dialectName}`);

expectsql(() => queryGenerator.selectQuery(User.tableName, {
model: User,
attributes: ['id'],
maxExecutionTimeHintMs: 1000,
}, User), {
default: notSupportedError,
mysql: 'SELECT /*+ MAX_EXECUTION_TIME(1000) */ `id` FROM `Users` AS `User`;',
});
});
});
});

0 comments on commit fc3d6aa

Please sign in to comment.