@sequelize/core
Version:
Sequelize is a promise-based Node.js ORM tool for Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server, Amazon Redshift, Snowflake’s Data Cloud, Db2, and IBM i. It features solid transaction support, relations, eager and lazy loading, read replication a
1,178 lines (1,177 loc) • 74.4 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var query_generator_exports = {};
__export(query_generator_exports, {
ADD_COLUMN_QUERY_SUPPORTABLE_OPTIONS: () => ADD_COLUMN_QUERY_SUPPORTABLE_OPTIONS,
AbstractQueryGenerator: () => AbstractQueryGenerator,
CREATE_TABLE_QUERY_SUPPORTABLE_OPTIONS: () => CREATE_TABLE_QUERY_SUPPORTABLE_OPTIONS
});
module.exports = __toCommonJS(query_generator_exports);
var import_compact = __toESM(require("lodash/compact"));
var import_defaults = __toESM(require("lodash/defaults"));
var import_each = __toESM(require("lodash/each"));
var import_forOwn = __toESM(require("lodash/forOwn"));
var import_get = __toESM(require("lodash/get"));
var import_isEmpty = __toESM(require("lodash/isEmpty"));
var import_isObject = __toESM(require("lodash/isObject"));
var import_isPlainObject = __toESM(require("lodash/isPlainObject"));
var import_pick = __toESM(require("lodash/pick"));
var import_reduce = __toESM(require("lodash/reduce"));
var import_uniq = __toESM(require("lodash/uniq"));
var import_node_util = __toESM(require("node:util"));
var import_base = require("../associations/base");
var import_belongs_to = require("../associations/belongs-to");
var import_belongs_to_many = require("../associations/belongs-to-many");
var import_has_many = require("../associations/has-many");
var import_base_sql_expression = require("../expression-builders/base-sql-expression.js");
var import_col = require("../expression-builders/col.js");
var import_literal = require("../expression-builders/literal.js");
var import_model_internals = require("../model-internals");
var import_sequelize = require("../sequelize");
var import_format = require("../utils/format");
var import_join_sql_fragments = require("../utils/join-sql-fragments");
var import_model_utils = require("../utils/model-utils");
var import_string = require("../utils/string");
var import_data_types_utils = require("./data-types-utils");
var import_query_generator_internal = require("./query-generator-internal.js");
var import_query_generator_typescript = require("./query-generator-typescript");
var import_where_sql_builder = require("./where-sql-builder");
const util = require("node:util");
const crypto = require("node:crypto");
const DataTypes = require("../data-types");
const { Op } = require("../operators");
const sequelizeError = require("../errors");
const { _validateIncludedElements } = require("../model-internals");
const CREATE_TABLE_QUERY_SUPPORTABLE_OPTIONS = /* @__PURE__ */ new Set([
"collate",
"charset",
"engine",
"rowFormat",
"comment",
"initialAutoIncrement",
"uniqueKeys"
]);
const ADD_COLUMN_QUERY_SUPPORTABLE_OPTIONS = /* @__PURE__ */ new Set(["ifNotExists"]);
class AbstractQueryGenerator extends import_query_generator_typescript.AbstractQueryGeneratorTypeScript {
#internals;
constructor(dialect, internals = new import_query_generator_internal.AbstractQueryGeneratorInternal(dialect)) {
super(dialect, internals);
this.#internals = internals;
}
/**
* Returns an insert into command
*
* @param {string} table
* @param {object} valueHash attribute value pairs
* @param {object} modelAttributes
* @param {object} [options]
*
* @private
*/
insertQuery(table, valueHash, modelAttributes, options) {
options ||= {};
(0, import_defaults.default)(options, this.options);
const modelAttributeMap = {};
const bind = /* @__PURE__ */ Object.create(null);
const fields = [];
const returningModelAttributes = [];
const values = /* @__PURE__ */ Object.create(null);
const quotedTable = this.quoteTable(table);
let bindParam = options.bindParam === void 0 ? this.bindParam(bind) : options.bindParam;
let query;
let valueQuery = "";
let emptyQuery = "";
let outputFragment = "";
let returningFragment = "";
let identityWrapperRequired = false;
let tmpTable = "";
if (modelAttributes) {
(0, import_each.default)(modelAttributes, (attribute, key) => {
modelAttributeMap[key] = attribute;
if (attribute.field) {
modelAttributeMap[attribute.field] = attribute;
}
});
}
if (this.dialect.supports["DEFAULT VALUES"]) {
emptyQuery += " DEFAULT VALUES";
} else if (this.dialect.supports["VALUES ()"]) {
emptyQuery += " VALUES ()";
}
if (this.dialect.supports.returnValues && options.returning) {
const returnValues = this.generateReturnValues(modelAttributes, options);
returningModelAttributes.push(...returnValues.returnFields);
returningFragment = returnValues.returningFragment;
tmpTable = returnValues.tmpTable || "";
outputFragment = returnValues.outputFragment || "";
}
if ((0, import_get.default)(this, ["sequelize", "options", "prependSearchPath"]) || options.searchPath) {
bindParam = void 0;
}
if (this.dialect.supports.EXCEPTION && options.exception) {
bindParam = void 0;
}
valueHash = (0, import_format.removeNullishValuesFromHash)(valueHash, this.options.omitNull);
for (const key in valueHash) {
if (Object.hasOwn(valueHash, key)) {
const value = valueHash[key] ?? null;
fields.push(this.quoteIdentifier(key));
if (modelAttributeMap[key] && modelAttributeMap[key].autoIncrement === true && value == null) {
if (!this.dialect.supports.autoIncrement.defaultValue) {
fields.splice(-1, 1);
} else if (this.dialect.supports.DEFAULT) {
values[key] = "DEFAULT";
} else {
values[key] = this.escape(null);
}
} else {
if (modelAttributeMap[key] && modelAttributeMap[key].autoIncrement === true) {
identityWrapperRequired = true;
}
values[key] = this.escape(value, {
model: options.model,
type: modelAttributeMap[key]?.type,
replacements: options.replacements,
bindParam
});
}
}
}
let onDuplicateKeyUpdate = "";
if (!(0, import_isEmpty.default)(options.conflictWhere) && !this.dialect.supports.inserts.onConflictWhere) {
throw new Error("missing dialect support for conflictWhere option");
}
if (this.dialect.supports.inserts.updateOnDuplicate && options.updateOnDuplicate) {
if (this.dialect.supports.inserts.updateOnDuplicate === " ON CONFLICT DO UPDATE SET") {
const conflictKeys = options.upsertKeys.map((attr) => this.quoteIdentifier(attr));
const updateKeys = options.updateOnDuplicate.map(
(attr) => `${this.quoteIdentifier(attr)}=EXCLUDED.${this.quoteIdentifier(attr)}`
);
const fragments = ["ON CONFLICT", "(", conflictKeys.join(","), ")"];
if (!(0, import_isEmpty.default)(options.conflictWhere)) {
fragments.push(this.whereQuery(options.conflictWhere, options));
}
if ((0, import_isEmpty.default)(updateKeys)) {
fragments.push("DO NOTHING");
} else {
fragments.push("DO UPDATE SET", updateKeys.join(","));
}
onDuplicateKeyUpdate = ` ${(0, import_join_sql_fragments.joinSQLFragments)(fragments)}`;
} else {
const valueKeys = options.updateOnDuplicate.map(
(attr) => `${this.quoteIdentifier(attr)}=${values[attr]}`
);
if ((0, import_isEmpty.default)(valueKeys) && options.upsertKeys) {
valueKeys.push(
...options.upsertKeys.map(
(attr) => `${this.quoteIdentifier(attr)}=${this.quoteIdentifier(attr)}`
)
);
}
if ((0, import_isEmpty.default)(valueKeys)) {
throw new Error(
"No update values found for ON DUPLICATE KEY UPDATE clause, and no identifier fields could be found to use instead."
);
}
onDuplicateKeyUpdate += `${this.dialect.supports.inserts.updateOnDuplicate} ${valueKeys.join(",")}`;
}
}
const replacements = {
ignoreDuplicates: options.ignoreDuplicates ? this.dialect.supports.inserts.ignoreDuplicates : "",
onConflictDoNothing: options.ignoreDuplicates ? this.dialect.supports.inserts.onConflictDoNothing : "",
attributes: fields.join(","),
output: outputFragment,
values: Object.values(values).join(","),
tmpTable
};
valueQuery = `${tmpTable}INSERT${replacements.ignoreDuplicates} INTO ${quotedTable} (${replacements.attributes})${replacements.output} VALUES (${replacements.values})${onDuplicateKeyUpdate}${replacements.onConflictDoNothing}${valueQuery}`;
emptyQuery = `${tmpTable}INSERT${replacements.ignoreDuplicates} INTO ${quotedTable}${replacements.output}${onDuplicateKeyUpdate}${replacements.onConflictDoNothing}${emptyQuery}`;
if (this.dialect.supports.EXCEPTION && options.exception) {
const dropFunction = "DROP FUNCTION IF EXISTS pg_temp.testfunc()";
if (returningModelAttributes.length === 0) {
returningModelAttributes.push("*");
}
const delimiter = `$func_${crypto.randomUUID().replaceAll("-", "")}$`;
const selectQuery = `SELECT (testfunc.response).${returningModelAttributes.join(", (testfunc.response).")}, testfunc.sequelize_caught_exception FROM pg_temp.testfunc();`;
options.exception = "WHEN unique_violation THEN GET STACKED DIAGNOSTICS sequelize_caught_exception = PG_EXCEPTION_DETAIL;";
valueQuery = `CREATE OR REPLACE FUNCTION pg_temp.testfunc(OUT response ${quotedTable}, OUT sequelize_caught_exception text) RETURNS RECORD AS ${delimiter} BEGIN ${valueQuery} RETURNING * INTO response; EXCEPTION ${options.exception} END ${delimiter} LANGUAGE plpgsql; ${selectQuery} ${dropFunction}`;
} else {
valueQuery += returningFragment;
emptyQuery += returningFragment;
}
query = `${`${replacements.attributes.length > 0 ? valueQuery : emptyQuery}`.trim()};`;
if (this.dialect.supports.finalTable) {
query = `SELECT * FROM FINAL TABLE (${replacements.attributes.length > 0 ? valueQuery : emptyQuery});`;
}
if (identityWrapperRequired && this.dialect.supports.autoIncrement.identityInsert) {
query = `SET IDENTITY_INSERT ${quotedTable} ON; ${query} SET IDENTITY_INSERT ${quotedTable} OFF;`;
}
const result = { query };
if (options.bindParam !== false) {
result.bind = bind;
}
return result;
}
/**
* Returns an insert into command for multiple values.
*
* @param {string} tableName
* @param {object} fieldValueHashes
* @param {object} options
* @param {object} fieldMappedAttributes
*
* @private
*/
bulkInsertQuery(tableName, fieldValueHashes, options, fieldMappedAttributes) {
options ||= {};
fieldMappedAttributes ||= {};
const tuples = [];
const serials = {};
const allAttributes = [];
let onDuplicateKeyUpdate = "";
for (const fieldValueHash of fieldValueHashes) {
(0, import_forOwn.default)(fieldValueHash, (value, key) => {
if (!allAttributes.includes(key)) {
allAttributes.push(key);
}
if (fieldMappedAttributes[key] && fieldMappedAttributes[key].autoIncrement === true) {
serials[key] = true;
}
});
}
for (const fieldValueHash of fieldValueHashes) {
const values = allAttributes.map((key) => {
if (this.dialect.supports.bulkDefault && serials[key] === true) {
return fieldValueHash[key] != null ? fieldValueHash[key] : "DEFAULT";
}
return this.escape(fieldValueHash[key] ?? null, {
// model // TODO: make bulkInsertQuery accept model instead of fieldValueHashes
// bindParam // TODO: support bind params
type: fieldMappedAttributes[key]?.type,
replacements: options.replacements
});
});
tuples.push(`(${values.join(",")})`);
}
if (this.dialect.supports.inserts.updateOnDuplicate && options.updateOnDuplicate) {
if (this.dialect.supports.inserts.updateOnDuplicate === " ON CONFLICT DO UPDATE SET") {
const conflictKeys = options.upsertKeys.map((attr) => this.quoteIdentifier(attr));
const updateKeys = options.updateOnDuplicate.map(
(attr) => `${this.quoteIdentifier(attr)}=EXCLUDED.${this.quoteIdentifier(attr)}`
);
let whereClause = false;
if (options.conflictWhere) {
if (!this.dialect.supports.inserts.onConflictWhere) {
throw new Error(`conflictWhere not supported for dialect ${this.dialect.name}`);
}
whereClause = this.whereQuery(options.conflictWhere, options);
}
onDuplicateKeyUpdate = [
"ON CONFLICT",
"(",
conflictKeys.join(","),
")",
whereClause,
"DO UPDATE SET",
updateKeys.join(",")
];
} else {
if (options.conflictWhere) {
throw new Error(`conflictWhere not supported for dialect ${this.dialect.name}`);
}
const valueKeys = options.updateOnDuplicate.map(
(attr) => `${this.quoteIdentifier(attr)}=VALUES(${this.quoteIdentifier(attr)})`
);
onDuplicateKeyUpdate = `${this.dialect.supports.inserts.updateOnDuplicate} ${valueKeys.join(",")}`;
}
}
const ignoreDuplicates = options.ignoreDuplicates ? this.dialect.supports.inserts.ignoreDuplicates : "";
const attributes = allAttributes.map((attr) => this.quoteIdentifier(attr)).join(",");
const onConflictDoNothing = options.ignoreDuplicates ? this.dialect.supports.inserts.onConflictDoNothing : "";
let returning = "";
if (this.dialect.supports.returnValues && options.returning) {
const returnValues = this.generateReturnValues(fieldMappedAttributes, options);
returning += returnValues.returningFragment;
}
return (0, import_join_sql_fragments.joinSQLFragments)([
"INSERT",
ignoreDuplicates,
"INTO",
this.quoteTable(tableName),
`(${attributes})`,
"VALUES",
tuples.join(","),
onDuplicateKeyUpdate,
onConflictDoNothing,
returning,
";"
]);
}
/**
* Returns an update query
*
* @param {string} tableName
* @param {object} attrValueHash
* @param {object} where A hash with conditions (e.g. {name: 'foo'}) OR an ID as integer
* @param {object} options
* @param {object} columnDefinitions
*
* @private
*/
updateQuery(tableName, attrValueHash, where, options, columnDefinitions) {
options ||= {};
(0, import_defaults.default)(options, this.options);
attrValueHash = (0, import_format.removeNullishValuesFromHash)(attrValueHash, options.omitNull, options);
const values = [];
const bind = /* @__PURE__ */ Object.create(null);
const modelAttributeMap = {};
let outputFragment = "";
let tmpTable = "";
let suffix = "";
if ((0, import_get.default)(this, ["sequelize", "options", "prependSearchPath"]) || options.searchPath) {
options.bindParam = false;
}
const bindParam = options.bindParam === void 0 ? this.bindParam(bind) : options.bindParam;
if (this.dialect.supports["LIMIT ON UPDATE"] && options.limit && this.dialect.name !== "mssql" && this.dialect.name !== "db2") {
suffix = ` LIMIT ${this.escape(options.limit, options)} `;
}
if (this.dialect.supports.returnValues && options.returning) {
const returnValues = this.generateReturnValues(columnDefinitions, options);
suffix += returnValues.returningFragment;
tmpTable = returnValues.tmpTable || "";
outputFragment = returnValues.outputFragment || "";
if (this.dialect.supports.returnValues !== "output" && options.returning) {
options.mapToModel = true;
}
}
if (columnDefinitions) {
(0, import_each.default)(columnDefinitions, (attribute, key) => {
modelAttributeMap[key] = attribute;
if (attribute.field) {
modelAttributeMap[attribute.field] = attribute;
}
});
}
for (const key in attrValueHash) {
if (modelAttributeMap && modelAttributeMap[key] && modelAttributeMap[key].autoIncrement === true && !this.dialect.supports.autoIncrement.update) {
continue;
}
const value = attrValueHash[key] ?? null;
values.push(
`${this.quoteIdentifier(key)}=${this.escape(value, {
// model // TODO: receive modelDefinition instead of columnDefinitions
type: modelAttributeMap?.[key]?.type,
replacements: options.replacements,
bindParam
})}`
);
}
const whereOptions = { ...options, bindParam };
if (values.length === 0) {
return { query: "" };
}
const query = `${tmpTable}UPDATE ${this.quoteTable(tableName)} SET ${values.join(",")}${outputFragment} ${this.whereQuery(where, whereOptions)}${suffix}`.trim();
const result = { query };
if (options.bindParam !== false) {
result.bind = bind;
}
return result;
}
/**
* Returns an update query using arithmetic operator
*
* @param {string} operator String with the arithmetic operator (e.g. '+' or '-')
* @param {string} tableName Name of the table
* @param {object} where A plain-object with conditions (e.g. {name: 'foo'}) OR an ID as integer
* @param {object} incrementAmountsByAttribute A plain-object with attribute-value-pairs
* @param {object} extraAttributesToBeUpdated A plain-object with attribute-value-pairs
* @param {object} options
*
* @private
*/
arithmeticQuery(operator, tableName, where, incrementAmountsByAttribute, extraAttributesToBeUpdated, options) {
options ||= {};
(0, import_defaults.default)(options, { returning: true });
const { model } = options;
const escapeOptions = (0, import_pick.default)(options, ["replacements", "model"]);
extraAttributesToBeUpdated = (0, import_format.removeNullishValuesFromHash)(
extraAttributesToBeUpdated,
this.options.omitNull
);
let outputFragment = "";
let returningFragment = "";
if (this.dialect.supports.returnValues && options.returning) {
const returnValues = this.generateReturnValues(null, options);
outputFragment = returnValues.outputFragment;
returningFragment = returnValues.returningFragment;
}
const updateSetSqlFragments = [];
for (const attributeName in incrementAmountsByAttribute) {
const columnName = model ? model.modelDefinition.getColumnNameLoose(attributeName) : attributeName;
const incrementAmount = incrementAmountsByAttribute[columnName];
const quotedField = this.quoteIdentifier(columnName);
const escapedAmount = this.escape(incrementAmount, escapeOptions);
updateSetSqlFragments.push(`${quotedField}=${quotedField}${operator} ${escapedAmount}`);
}
for (const attributeName in extraAttributesToBeUpdated) {
const columnName = model ? model.modelDefinition.getColumnNameLoose(attributeName) : attributeName;
const newValue = extraAttributesToBeUpdated[columnName];
const quotedField = this.quoteIdentifier(columnName);
const escapedValue = this.escape(newValue, escapeOptions);
updateSetSqlFragments.push(`${quotedField}=${escapedValue}`);
}
return (0, import_join_sql_fragments.joinSQLFragments)([
"UPDATE",
this.quoteTable(tableName),
"SET",
updateSetSqlFragments.join(","),
outputFragment,
this.whereQuery(where, escapeOptions),
returningFragment
]);
}
/*
Returns an add index query.
Parameters:
- tableName -> Name of an existing table, possibly with schema.
- options:
- type: UNIQUE|FULLTEXT|SPATIAL
- name: The name of the index. Default is <table>_<attr1>_<attr2>
- fields: An array of attributes as string or as hash.
If the attribute is a hash, it must have the following content:
- name: The name of the attribute/column
- length: An integer. Optional
- order: 'ASC' or 'DESC'. Optional
- parser
- using
- operator
- concurrently: Pass CONCURRENT so other operations run while the index is created
- include
- rawTablename, the name of the table, without schema. Used to create the name of the index
@private
*/
addIndexQuery(tableName, attributes, options, rawTablename) {
options ||= {};
if (!Array.isArray(attributes)) {
options = attributes;
attributes = void 0;
} else {
options.fields = attributes;
}
options.prefix = options.prefix || rawTablename || tableName;
if (options.prefix && typeof options.prefix === "string") {
options.prefix = options.prefix.replaceAll(".", "_");
}
const fieldsSql = options.fields.map((field) => {
if (field instanceof import_base_sql_expression.BaseSqlExpression) {
return this.formatSqlExpression(field);
}
if (typeof field === "string") {
field = {
name: field
};
}
let result = "";
if (field.attribute) {
field.name = field.attribute;
}
if (!field.name) {
throw new Error(`The following index field has no name: ${util.inspect(field)}`);
}
result += this.quoteIdentifier(field.name);
if (this.dialect.supports.index.collate && field.collate) {
result += ` COLLATE ${this.quoteIdentifier(field.collate)}`;
}
if (this.dialect.supports.index.operator) {
const operator = field.operator || options.operator;
if (operator) {
result += ` ${operator}`;
}
}
if (this.dialect.supports.index.length > 0 && field.length > 0) {
result += `(${field.length})`;
}
if (field.order) {
result += ` ${field.order}`;
}
return result;
});
let includeSql;
if (options.include) {
if (!this.dialect.supports.index.include) {
throw new Error(
`The include attribute for indexes is not supported by ${this.dialect.name} dialect`
);
}
if (options.include instanceof import_literal.Literal) {
includeSql = `INCLUDE ${options.include.val}`;
} else if (Array.isArray(options.include)) {
includeSql = `INCLUDE (${options.include.map((field) => field instanceof import_literal.Literal ? field.val : this.quoteIdentifier(field)).join(", ")})`;
} else {
throw new TypeError("The include attribute for indexes must be an array or a literal.");
}
}
if (!options.name) {
options = (0, import_string.nameIndex)(options, options.prefix);
}
options = (0, import_model_internals.conformIndex)(options);
if (!this.dialect.supports.index.type) {
delete options.type;
}
if (options.where) {
options.where = this.whereQuery(options.where);
}
const escapedTableName = this.quoteTable(tableName);
const concurrently = this.dialect.supports.index.concurrently && options.concurrently ? "CONCURRENTLY" : void 0;
let ind;
if (this.dialect.supports.indexViaAlter) {
ind = ["ALTER TABLE", escapedTableName, concurrently, "ADD"];
} else {
ind = ["CREATE"];
}
const escapedIndexName = tableName.schema && this.dialect.name === "db2" ? (
// 'quoteTable' isn't the best name: it quotes any identifier.
// in this case, the goal is to produce '"schema_name"."index_name"' to scope the index in this schema
this.quoteTable({
schema: tableName.schema,
tableName: options.name
})
) : this.quoteIdentifiers(options.name);
ind = ind.concat(
options.unique ? "UNIQUE" : "",
options.type,
"INDEX",
!this.dialect.supports.indexViaAlter ? concurrently : void 0,
escapedIndexName,
this.dialect.supports.index.using === 1 && options.using ? `USING ${options.using}` : "",
!this.dialect.supports.indexViaAlter ? `ON ${escapedTableName}` : void 0,
this.dialect.supports.index.using === 2 && options.using ? `USING ${options.using}` : "",
`(${fieldsSql.join(", ")})`,
this.dialect.supports.index.parser && options.parser ? `WITH PARSER ${options.parser}` : void 0,
this.dialect.supports.index.include && options.include ? includeSql : void 0,
this.dialect.supports.index.where && options.where ? options.where : void 0
);
return (0, import_compact.default)(ind).join(" ");
}
/*
Quote an object based on its type. This is a more general version of quoteIdentifiers
Strings: should proxy to quoteIdentifiers
Arrays:
* Expects array in the form: [<model> (optional), <model> (optional),... String, String (optional)]
Each <model> can be a model, or an object {model: Model, as: String}, matching include, or an
association object, or the name of an association.
* Zero or more models can be included in the array and are used to trace a path through the tree of
included nested associations. This produces the correct table name for the ORDER BY/GROUP BY SQL
and quotes it.
* If a single string is appended to end of array, it is quoted.
If two strings appended, the 1st string is quoted, the 2nd string unquoted.
Objects:
* If raw is set, that value should be returned verbatim, without quoting
* If fn is set, the string should start with the value of fn, starting paren, followed by
the values of cols (which is assumed to be an array), quoted and joined with ', ',
unless they are themselves objects
* If direction is set, should be prepended
Currently this function is only used for ordering / grouping columns and Sequelize.col(), but it could
potentially also be used for other places where we want to be able to call SQL functions (e.g. as default values)
@private
*/
quote(collection, parent, connector = ".", options) {
const validOrderOptions = [
"ASC",
"DESC",
"ASC NULLS LAST",
"DESC NULLS LAST",
"ASC NULLS FIRST",
"DESC NULLS FIRST",
"NULLS FIRST",
"NULLS LAST"
];
if (typeof collection === "string") {
return this.quoteIdentifiers(collection);
}
if (Array.isArray(collection)) {
collection.forEach((item2, index) => {
const previous = collection[index - 1];
let previousAssociation;
let previousModel;
if (!previous && parent !== void 0) {
previousModel = parent;
} else if (previous && previous instanceof import_base.Association) {
previousAssociation = previous;
previousModel = previous.target;
}
if ((0, import_model_utils.isModelStatic)(previousModel)) {
let model;
let as;
if ((0, import_model_utils.isModelStatic)(item2)) {
model = item2;
} else if ((0, import_isPlainObject.default)(item2) && item2.model && (0, import_model_utils.isModelStatic)(item2.model)) {
model = item2.model;
as = item2.as;
}
if (model) {
if (!as && previousAssociation && previousAssociation instanceof import_base.Association && previousAssociation.through?.model === model) {
item2 = previousAssociation.fromSourceToThroughOne;
} else {
item2 = previousModel.getAssociationWithModel(model, as);
}
if (!(item2 instanceof import_base.Association)) {
throw new TypeError(
`Unable to find a valid association between models "${previousModel.name}" and "${model.name}"`
);
}
}
}
if (typeof item2 === "string") {
const orderIndex = validOrderOptions.indexOf(item2.toUpperCase());
if (index > 0 && orderIndex !== -1) {
item2 = new import_literal.Literal(` ${validOrderOptions[orderIndex]}`);
} else if ((0, import_model_utils.isModelStatic)(previousModel)) {
const { modelDefinition: previousModelDefinition } = previousModel;
if (previousModel.associations?.[item2]) {
item2 = previousModel.associations[item2];
} else if (previousModelDefinition.attributes.has(item2)) {
item2 = previousModelDefinition.attributes.get(item2).columnName;
} else if (item2.includes(".")) {
const itemSplit = item2.split(".");
const jsonAttribute = previousModelDefinition.attributes.get(itemSplit[0]);
if (jsonAttribute.type instanceof DataTypes.JSON) {
const identifier = this.quoteIdentifiers(
`${previousModel.name}.${jsonAttribute.columnName}`
);
const path = itemSplit.slice(1);
item2 = this.jsonPathExtractionQuery(identifier, path);
item2 = new import_literal.Literal(item2);
}
}
}
}
collection[index] = item2;
});
const collectionLength = collection.length;
const tableNames = [];
let item;
let i = 0;
for (i = 0; i < collectionLength - 1; i++) {
item = collection[i];
if (typeof item === "string" || item._modelAttribute || item instanceof import_base_sql_expression.BaseSqlExpression) {
break;
} else if (item instanceof import_base.Association) {
const previousAssociation = collection[i - 1];
if (previousAssociation instanceof import_belongs_to_many.BelongsToManyAssociation && item === previousAssociation.fromSourceToThroughOne) {
tableNames[i] = previousAssociation.throughModel.name;
} else {
tableNames[i] = item.as;
}
}
}
let sql = "";
if (i > 0) {
sql += `${this.quoteIdentifier(tableNames.join(connector))}.`;
} else if (typeof collection[0] === "string" && parent) {
sql += `${this.quoteIdentifier(parent.name)}.`;
}
for (const collectionItem of collection.slice(i)) {
sql += this.quote(collectionItem, parent, connector, options);
}
return sql;
}
if (collection._modelAttribute) {
return `${this.quoteTable(collection.Model.name)}.${this.quoteIdentifier(collection.fieldName)}`;
}
if (collection instanceof import_base_sql_expression.BaseSqlExpression) {
return this.formatSqlExpression(collection, options);
}
if ((0, import_isPlainObject.default)(collection) && collection.raw) {
throw new Error(
'The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.'
);
}
throw new Error(`Unknown structure passed to order / group: ${util.inspect(collection)}`);
}
/**
* Split a list of identifiers by "." and quote each part.
*
* ⚠️ You almost certainly want to use `quoteIdentifier` instead!
* This method splits the identifier by "." into multiple identifiers, and has special meaning for "*".
* This behavior should never be the default and should be explicitly opted into by using {@link sql.col}.
*
* @param {string} identifiers
*
* @returns {string}
*/
quoteIdentifiers(identifiers) {
if (identifiers.includes(".")) {
identifiers = identifiers.split(".");
const head = identifiers.slice(0, -1).join("->");
const tail = identifiers.at(-1);
return `${this.quoteIdentifier(head)}.${tail === "*" ? "*" : this.quoteIdentifier(tail)}`;
}
if (identifiers === "*") {
return "*";
}
return this.quoteIdentifier(identifiers);
}
bindParam(bind) {
let i = 0;
return (value) => {
const bindName = `sequelize_${++i}`;
bind[bindName] = value;
return `$${bindName}`;
};
}
/*
Returns a query for selecting elements in the table <tableName>.
Options:
- attributes -> An array of attributes (e.g. ['name', 'birthday']). Default: *
- where -> A hash with conditions (e.g. {name: 'foo'})
OR an ID as integer
- order -> e.g. 'id DESC'
- group
- limit -> The maximum count you want to get.
- offset -> An offset value to start from. Only useable with limit!
@private
*/
selectQuery(tableName, options, model) {
options ||= {};
const limit = options.limit;
const mainQueryItems = [];
const subQueryItems = [];
const subQuery = options.subQuery === void 0 ? limit && options.hasMultiAssociation : options.subQuery;
const attributes = {
main: options.attributes && [...options.attributes],
subQuery: null
};
const mainTable = {
name: tableName,
quotedName: null,
as: null,
quotedAs: null,
model
};
const topLevelInfo = {
names: mainTable,
options,
subQuery
};
let mainJoinQueries = [];
let subJoinQueries = [];
let query;
if (options.minifyAliases && !options.aliasesMapping) {
options.aliasesMapping = /* @__PURE__ */ new Map();
options.aliasesByTable = {};
options.includeAliases = /* @__PURE__ */ new Map();
}
if (options.tableAs) {
mainTable.as = options.tableAs;
} else if (!Array.isArray(mainTable.name) && mainTable.model) {
mainTable.as = mainTable.model.name;
}
mainTable.quotedAs = mainTable.as && this.quoteIdentifier(mainTable.as);
mainTable.quotedName = !Array.isArray(mainTable.name) ? this.quoteTable(mainTable.name, { ...options, alias: mainTable.as ?? false }) : tableName.map((t) => {
return Array.isArray(t) ? this.quoteTable(t[0], { ...options, alias: t[1] }) : this.quoteTable(t, { ...options, alias: true });
}).join(", ");
const mainModelDefinition = mainTable.model?.modelDefinition;
const mainModelAttributes = mainModelDefinition?.attributes;
if (subQuery && attributes.main) {
for (const pkAttrName of mainModelDefinition.primaryKeysAttributeNames) {
if (!attributes.main.some(
(attr) => pkAttrName === attr || pkAttrName === attr[0] || pkAttrName === attr[1]
)) {
const attribute = mainModelAttributes.get(pkAttrName);
attributes.main.push(
attribute.columnName !== pkAttrName ? [pkAttrName, attribute.columnName] : pkAttrName
);
}
}
}
attributes.main = this.escapeAttributes(attributes.main, options, mainTable.as);
attributes.main = attributes.main || (options.include ? [`${mainTable.quotedAs}.*`] : ["*"]);
if (subQuery || options.groupedLimit) {
attributes.subQuery = attributes.main;
attributes.main = [`${mainTable.quotedAs || mainTable.quotedName}.*`];
}
if (options.include) {
for (const include of options.include) {
if (include.separate) {
continue;
}
const joinQueries = this.generateInclude(
include,
{ externalAs: mainTable.as, internalAs: mainTable.as },
topLevelInfo,
{ replacements: options.replacements, minifyAliases: options.minifyAliases }
);
subJoinQueries = subJoinQueries.concat(joinQueries.subQuery);
mainJoinQueries = mainJoinQueries.concat(joinQueries.mainQuery);
if (joinQueries.attributes.main.length > 0) {
attributes.main = (0, import_uniq.default)(attributes.main.concat(joinQueries.attributes.main));
}
if (joinQueries.attributes.subQuery.length > 0) {
attributes.subQuery = (0, import_uniq.default)(attributes.subQuery.concat(joinQueries.attributes.subQuery));
}
}
}
if (subQuery) {
subQueryItems.push(
this.selectFromTableFragment(
options,
mainTable.model,
attributes.subQuery,
mainTable.quotedName,
mainTable.quotedAs
),
subJoinQueries.join("")
);
} else {
if (options.groupedLimit) {
if (!mainTable.quotedAs) {
mainTable.quotedAs = mainTable.quotedName;
}
if (!mainTable.as) {
mainTable.as = mainTable.name;
}
let where = { ...options.where };
let groupedLimitOrder;
let whereKey;
let include;
let groupedTableName = mainTable.as;
if (typeof options.groupedLimit.on === "string") {
whereKey = options.groupedLimit.on;
} else if (options.groupedLimit.on instanceof import_has_many.HasManyAssociation) {
whereKey = options.groupedLimit.on.identifierField;
}
const placeholder = '"$PLACEHOLDER$" = true';
if (options.groupedLimit.on instanceof import_belongs_to_many.BelongsToManyAssociation) {
groupedTableName = options.groupedLimit.on.throughModel.name;
const groupedLimitOptions = _validateIncludedElements({
include: [
{
as: options.groupedLimit.on.throughModel.name,
association: options.groupedLimit.on.fromSourceToThrough,
duplicating: false,
// The UNION'ed query may contain duplicates, but each sub-query cannot
required: true,
where: (0, import_sequelize.and)(new import_literal.Literal(placeholder), options.groupedLimit.through?.where)
}
],
model
});
options.hasJoin = true;
options.hasMultiAssociation = true;
options.includeMap = Object.assign(groupedLimitOptions.includeMap, options.includeMap);
options.includeNames = groupedLimitOptions.includeNames.concat(
options.includeNames || []
);
include = groupedLimitOptions.include;
if (Array.isArray(options.order)) {
options.order.forEach((order, i) => {
if (Array.isArray(order)) {
order = order[0];
}
let alias = `subquery_order_${i}`;
options.attributes.push([order, alias]);
alias = new import_literal.Literal(this.quote(alias, void 0, void 0, options));
if (Array.isArray(options.order[i])) {
options.order[i][0] = alias;
} else {
options.order[i] = alias;
}
});
groupedLimitOrder = options.order;
}
} else {
groupedLimitOrder = options.order;
delete options.order;
where = (0, import_sequelize.and)(new import_literal.Literal(placeholder), where);
}
const baseQuery = `SELECT * FROM (${this.selectQuery(
tableName,
{
attributes: options.attributes,
offset: options.offset,
limit: options.groupedLimit.limit,
order: groupedLimitOrder,
minifyAliases: options.minifyAliases,
aliasesMapping: options.aliasesMapping,
aliasesByTable: options.aliasesByTable,
where,
include,
model
},
model
).replace(/;$/, "")}) AS sub`;
const splicePos = baseQuery.indexOf(placeholder);
mainQueryItems.push(
this.selectFromTableFragment(
options,
mainTable.model,
attributes.main,
`(${options.groupedLimit.values.map((value) => {
let groupWhere;
if (whereKey) {
groupWhere = {
[whereKey]: value
};
}
if (include) {
groupWhere = {
[options.groupedLimit.on.foreignIdentifierField]: value
};
}
return (0, import_string.spliceStr)(
baseQuery,
splicePos,
placeholder.length,
this.whereItemsQuery(groupWhere, { ...options, mainAlias: groupedTableName })
);
}).join(this.dialect.supports["UNION ALL"] ? " UNION ALL " : " UNION ")})`,
mainTable.quotedAs
)
);
} else {
mainQueryItems.push(
this.selectFromTableFragment(
options,
mainTable.model,
attributes.main,
mainTable.quotedName,
mainTable.quotedAs
)
);
}
mainQueryItems.push(mainJoinQueries.join(""));
}
if (Object.hasOwn(options, "where") && !options.groupedLimit) {
options.where = this.whereItemsQuery(options.where, {
...options,
model,
mainAlias: mainTable.as || tableName
});
if (options.where) {
if (subQuery) {
subQueryItems.push(` WHERE ${options.where}`);
} else {
mainQueryItems.push(` WHERE ${options.where}`);
for (const [key, value] of mainQueryItems.entries()) {
if (value.startsWith("SELECT")) {
mainQueryItems[key] = this.selectFromTableFragment(
options,
model,
attributes.main,
mainTable.quotedName,
mainTable.quotedAs,
options.where
);
}
}
}
}
}
if (options.group) {
options.group = Array.isArray(options.group) ? options.group.map((t) => this.aliasGrouping(t, model, mainTable.as, options)).join(", ") : this.aliasGrouping(options.group, model, mainTable.as, options);
if (subQuery && options.group) {
subQueryItems.push(` GROUP BY ${options.group}`);
} else if (options.group) {
mainQueryItems.push(` GROUP BY ${options.group}`);
}
}
if (Object.hasOwn(options, "having")) {
options.having = this.whereItemsQuery(options.having, {
...options,
model,
mainAlias: mainTable.as || tableName
});
if (options.having) {
if (subQuery) {
subQueryItems.push(` HAVING ${options.having}`);
} else {
mainQueryItems.push(` HAVING ${options.having}`);
}
}
}
if (options.order) {
const orders = this.getQueryOrders(options, model, subQuery);
if (orders.mainQueryOrder.length > 0) {
mainQueryItems.push(` ORDER BY ${orders.mainQueryOrder.join(", ")}`);
} else if (!subQuery && (options.limit != null || options.offset)) {
if (!(0, import_model_utils.isModelStatic)(model)) {
throw new Error("Cannot use offset or limit without a model or order being set");
}
const pks = [];
for (const pkAttrName of mainModelDefinition.primaryKeysAttributeNames) {
const attribute = mainModelAttributes.get(pkAttrName);
pks.push(attribute.columnName !== pkAttrName ? attribute.columnName : pkAttrName);
}
mainQueryItems.push(
` ORDER BY ${pks.map((pk) => `${mainTable.quotedAs}.${this.quoteIdentifier(pk)}`).join(", ")}`
);
}
if (orders.subQueryOrder.length > 0) {
subQueryItems.push(` ORDER BY ${orders.subQueryOrder.join(", ")}`);
} else if (subQuery && (options.limit != null || options.offset)) {
if (!(0, import_model_utils.isModelStatic)(model)) {
throw new Error("Cannot use offset or limit without a model or order being set");
}
const pks = [];
for (const pkAttrName of mainModelDefinition.primaryKeysAttributeNames) {
const attribute = mainModelAttributes.get(pkAttrName);
pks.push(attribute.columnName !== pkAttrName ? attribute.columnName : pkAttrName);
}
subQueryItems.push(
` ORDER BY ${pks.map((pk) => `${mainTable.quotedAs}.${this.quoteIdentifier(pk)}`).join(", ")}`
);
}
} else if (options.limit != null || options.offset) {
if (!(0, import_model_utils.isModelStatic)(model)) {
throw new Error("Cannot use offset or limit without a model or order being set");
}
const pks = [];
for (const pkAttrName of mainModelDefinition.primaryKeysAttributeNames) {
const attribute = mainModelAttributes.get(pkAttrName);
pks.push(attribute.columnName !== pkAttrName ? attribute.columnName : pkAttrName);
}
if (subQuery) {
subQueryItems.push(
` ORDER BY ${pks.map((pk) => `${mainTable.quotedAs}.${this.quoteIdentifier(pk)}`).join(", ")}`
);
} else {
mainQueryItems.push(
` ORDER BY ${pks.map((pk) => `${mainTable.quotedAs}.${this.quoteIdentifier(pk)}`).join(", ")}`
);
}
}
const limitOrder = this.#internals.addLimitAndOffset(options);
if (limitOrder && !options.groupedLimit) {
if (subQuery) {
subQueryItems.push(limitOrder);
} else {
mainQueryItems.push(limitOrder);
}
}
if (subQuery) {
this._throwOnEmptyAttributes(attributes.main, {
modelName: model && model.name,
as: mainTable.quotedAs
});
query = `SELECT ${attributes.main.join(", ")} FROM (${subQueryItems.join("")}) AS ${mainTable.quotedAs}${mainJoinQueries.join("")}${mainQueryItems.join("")}`;
} else {
query = mainQueryItems.join("");
}
if (options.lock && this.dialect.supports.lock) {
let lock = options.lock;
if (typeof options.lock === "object") {
lock = options.lock.level;
}
if (this.dialect.supports.lockKey && ["KEY SHARE", "NO KEY UPDATE"].includes(lock)) {
query += ` FOR ${lock}`;
} else if (lock === "SHARE") {
query += ` ${this.dialect.supports.forShare}`;
} else {
query += " FOR UPDATE";
}
if (this.dialect.supports.lockOf && options.lock.of && (0, import_model_utils.isModelStatic)(options.lock.of)) {
query += ` OF ${this.quoteTable(options.lock.of.name)}`;
}
if (this.dialect.supports.skipLocked && options.skipLocked) {
query += " SKIP LOCKED";
}
}
return `${query};`;
}
aliasGrouping(field, model, tableName, options) {
const src = Array.isArray(field) ? field[0] : field;
return this.quote(
this._getAliasForField(tableName, src, options) || src,
model,
void 0,
options
);
}
escapeAttributes(attributes, options, mainTableAs) {
const quotedMainTableAs = mainTableAs && this.quoteIdentifier(mainTableAs);
return attributes && attributes.map((attr) => {
let addTable = true;
if (attr instanceof import_base_sql_expression.BaseSqlExpression) {
return this.formatSqlExpression(attr, options);
}
if (Array.isArray(attr)) {
if (attr.length !== 2) {
throw new Error(
`${JSON.stringify(attr)} is not a valid attribute definition. Please use the following format: ['attribute definition', 'alias']`
);
}
attr = [...attr];
if (attr[0] instanceof import_base_sql_expression.BaseSqlExpression) {
attr[0] = this.formatSqlExpression(attr[0], options);
addTable = false;
} else {
attr[0] = this.quoteIdentifier(attr[0]);
}
let alias = attr[1];
if (options.minifyAliases) {
alias = this._getMinifiedAlias(alias, mainTableAs, options);
}
attr = [attr[0], this.quoteIdentifier(alias)].join(" AS ");
} else {
attr = this.quoteIdentifier(attr, options.model);
}
if (!(0, import_isEmpty.default)(options.include) && (!attr.includes(".") || options.dotNotation) && addTable) {
attr = `${quotedMainTableAs}.${attr}`;
}
return attr;
});
}
generateInclude(include, parentTableName, topLevelInfo, options) {
const joinQueries = {
mainQuery: [],
subQuery: []
};
const mainChildIncludes = [];
const subChildIncludes = [];
let requiredMismatch = false;
const includeAs = {
internalAs: include.as,
externalAs: include.as
};
const attributes = {
main: [],
subQuery: []
};
topLevelInfo.options.keysEscaped = true;
if (topLevelInfo.options && topLevelInfo.options.indexHints) {
delete topLevelInfo.options.indexHints;
}
if (topLevelInfo.names.name !== parentTableName.externalAs && topLevelInfo.names.as !== parentTableName.externalAs) {
includeAs.internalAs = `${parentTableName.internalAs}->${include.as}`;
includeAs.externalAs = `${parentTableName.externalAs}.${include.as}`;
}
if (topLevelInfo.options.includeIgnoreAttributes !== false) {
include.model._expandAttributes(include);
(0, import