You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
12 KiB
323 lines
12 KiB
"use strict"; |
|
const AbstractQuery = require("../abstract/query"); |
|
const QueryTypes = require("../../query-types"); |
|
const sequelizeErrors = require("../../errors"); |
|
const _ = require("lodash"); |
|
const { logger } = require("../../utils/logger"); |
|
const debug = logger.debugContext("sql:pg"); |
|
class Query extends AbstractQuery { |
|
static formatBindParameters(sql, values, dialect) { |
|
const stringReplaceFunc = (value) => typeof value === "string" ? value.replace(/\0/g, "\\0") : value; |
|
let bindParam; |
|
if (Array.isArray(values)) { |
|
bindParam = values.map(stringReplaceFunc); |
|
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0]; |
|
} else { |
|
bindParam = []; |
|
let i = 0; |
|
const seen = {}; |
|
const replacementFunc = (match, key, values2) => { |
|
if (seen[key] !== void 0) { |
|
return seen[key]; |
|
} |
|
if (values2[key] !== void 0) { |
|
i = i + 1; |
|
bindParam.push(stringReplaceFunc(values2[key])); |
|
seen[key] = `$${i}`; |
|
return `$${i}`; |
|
} |
|
return void 0; |
|
}; |
|
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0]; |
|
} |
|
return [sql, bindParam]; |
|
} |
|
async run(sql, parameters) { |
|
const { connection } = this; |
|
if (!_.isEmpty(this.options.searchPath)) { |
|
sql = this.sequelize.getQueryInterface().queryGenerator.setSearchPath(this.options.searchPath) + sql; |
|
} |
|
if (this.sequelize.options.minifyAliases && this.options.includeAliases) { |
|
_.toPairs(this.options.includeAliases).sort((a, b) => b[1].length - a[1].length).forEach(([alias, original]) => { |
|
const reg = new RegExp(_.escapeRegExp(original), "g"); |
|
sql = sql.replace(reg, alias); |
|
}); |
|
} |
|
this.sql = sql; |
|
const query = parameters && parameters.length ? new Promise((resolve, reject) => connection.query(sql, parameters, (error, result) => error ? reject(error) : resolve(result))) : new Promise((resolve, reject) => connection.query(sql, (error, result) => error ? reject(error) : resolve(result))); |
|
const complete = this._logQuery(sql, debug, parameters); |
|
let queryResult; |
|
const errForStack = new Error(); |
|
try { |
|
queryResult = await query; |
|
} catch (error) { |
|
if (error.code === "ECONNRESET" || /Unable to set non-blocking to true/i.test(error) || /SSL SYSCALL error: EOF detected/i.test(error) || /Local: Authentication failure/i.test(error) || error.message === "Query read timeout") { |
|
connection._invalid = true; |
|
} |
|
error.sql = sql; |
|
error.parameters = parameters; |
|
throw this.formatError(error, errForStack.stack); |
|
} |
|
complete(); |
|
let rows = Array.isArray(queryResult) ? queryResult.reduce((allRows, r) => allRows.concat(r.rows || []), []) : queryResult.rows; |
|
const rowCount = Array.isArray(queryResult) ? queryResult.reduce((count, r) => Number.isFinite(r.rowCount) ? count + r.rowCount : count, 0) : queryResult.rowCount || 0; |
|
if (this.sequelize.options.minifyAliases && this.options.aliasesMapping) { |
|
rows = rows.map((row) => _.toPairs(row).reduce((acc, [key, value]) => { |
|
const mapping = this.options.aliasesMapping.get(key); |
|
acc[mapping || key] = value; |
|
return acc; |
|
}, {})); |
|
} |
|
const isTableNameQuery = sql.startsWith("SELECT table_name FROM information_schema.tables"); |
|
const isRelNameQuery = sql.startsWith("SELECT relname FROM pg_class WHERE oid IN"); |
|
if (isRelNameQuery) { |
|
return rows.map((row) => ({ |
|
name: row.relname, |
|
tableName: row.relname.split("_")[0] |
|
})); |
|
} |
|
if (isTableNameQuery) { |
|
return rows.map((row) => Object.values(row)); |
|
} |
|
if (rows[0] && rows[0].sequelize_caught_exception !== void 0) { |
|
if (rows[0].sequelize_caught_exception !== null) { |
|
throw this.formatError({ |
|
sql, |
|
parameters, |
|
code: "23505", |
|
detail: rows[0].sequelize_caught_exception |
|
}); |
|
} |
|
for (const row of rows) { |
|
delete row.sequelize_caught_exception; |
|
} |
|
} |
|
if (this.isShowIndexesQuery()) { |
|
for (const row of rows) { |
|
const attributes = /ON .*? (?:USING .*?\s)?\(([^]*)\)/gi.exec(row.definition)[1].split(","); |
|
const columns = _.zipObject(row.column_indexes, this.sequelize.getQueryInterface().queryGenerator.fromArray(row.column_names)); |
|
delete row.column_indexes; |
|
delete row.column_names; |
|
let field; |
|
let attribute; |
|
row.fields = row.indkey.split(" ").map((indKey, index) => { |
|
field = columns[indKey]; |
|
if (!field) { |
|
return null; |
|
} |
|
attribute = attributes[index]; |
|
return { |
|
attribute: field, |
|
collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : void 0, |
|
order: attribute.includes("DESC") ? "DESC" : attribute.includes("ASC") ? "ASC" : void 0, |
|
length: void 0 |
|
}; |
|
}).filter((n) => n !== null); |
|
delete row.columns; |
|
} |
|
return rows; |
|
} |
|
if (this.isForeignKeysQuery()) { |
|
const result = []; |
|
for (const row of rows) { |
|
let defParts; |
|
if (row.condef !== void 0 && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) { |
|
row.id = row.constraint_name; |
|
row.table = defParts[2]; |
|
row.from = defParts[1]; |
|
row.to = defParts[3]; |
|
let i; |
|
for (i = 5; i <= 8; i += 3) { |
|
if (/(UPDATE|DELETE)/.test(defParts[i])) { |
|
row[`on_${defParts[i].toLowerCase()}`] = defParts[i + 1]; |
|
} |
|
} |
|
} |
|
result.push(row); |
|
} |
|
return result; |
|
} |
|
if (this.isSelectQuery()) { |
|
let result = rows; |
|
if (this.options.raw === false && this.sequelize.options.quoteIdentifiers === false) { |
|
const attrsMap = _.reduce(this.model.rawAttributes, (m, v, k) => { |
|
m[k.toLowerCase()] = k; |
|
return m; |
|
}, {}); |
|
result = rows.map((row) => { |
|
return _.mapKeys(row, (value, key) => { |
|
const targetAttr = attrsMap[key]; |
|
if (typeof targetAttr === "string" && targetAttr !== key) { |
|
return targetAttr; |
|
} |
|
return key; |
|
}); |
|
}); |
|
} |
|
return this.handleSelectQuery(result); |
|
} |
|
if (QueryTypes.DESCRIBE === this.options.type) { |
|
const result = {}; |
|
for (const row of rows) { |
|
result[row.Field] = { |
|
type: row.Type.toUpperCase(), |
|
allowNull: row.Null === "YES", |
|
defaultValue: row.Default, |
|
comment: row.Comment, |
|
special: row.special ? this.sequelize.getQueryInterface().queryGenerator.fromArray(row.special) : [], |
|
primaryKey: row.Constraint === "PRIMARY KEY" |
|
}; |
|
if (result[row.Field].type === "BOOLEAN") { |
|
result[row.Field].defaultValue = { "false": false, "true": true }[result[row.Field].defaultValue]; |
|
if (result[row.Field].defaultValue === void 0) { |
|
result[row.Field].defaultValue = null; |
|
} |
|
} |
|
if (typeof result[row.Field].defaultValue === "string") { |
|
result[row.Field].defaultValue = result[row.Field].defaultValue.replace(/'/g, ""); |
|
if (result[row.Field].defaultValue.includes("::")) { |
|
const split = result[row.Field].defaultValue.split("::"); |
|
if (split[1].toLowerCase() !== "regclass)") { |
|
result[row.Field].defaultValue = split[0]; |
|
} |
|
} |
|
} |
|
} |
|
return result; |
|
} |
|
if (this.isVersionQuery()) { |
|
return rows[0].server_version; |
|
} |
|
if (this.isShowOrDescribeQuery()) { |
|
return rows; |
|
} |
|
if (QueryTypes.BULKUPDATE === this.options.type) { |
|
if (!this.options.returning) { |
|
return parseInt(rowCount, 10); |
|
} |
|
return this.handleSelectQuery(rows); |
|
} |
|
if (QueryTypes.BULKDELETE === this.options.type) { |
|
return parseInt(rowCount, 10); |
|
} |
|
if (this.isInsertQuery() || this.isUpdateQuery() || this.isUpsertQuery()) { |
|
if (this.instance && this.instance.dataValues) { |
|
if (this.isInsertQuery() && !this.isUpsertQuery() && rowCount === 0) { |
|
throw new sequelizeErrors.EmptyResultError(); |
|
} |
|
for (const key in rows[0]) { |
|
if (Object.prototype.hasOwnProperty.call(rows[0], key)) { |
|
const record = rows[0][key]; |
|
const attr = _.find(this.model.rawAttributes, (attribute) => attribute.fieldName === key || attribute.field === key); |
|
this.instance.dataValues[attr && attr.fieldName || key] = record; |
|
} |
|
} |
|
} |
|
if (this.isUpsertQuery()) { |
|
return [ |
|
this.instance, |
|
null |
|
]; |
|
} |
|
return [ |
|
this.instance || rows && (this.options.plain && rows[0] || rows) || void 0, |
|
rowCount |
|
]; |
|
} |
|
if (this.isRawQuery()) { |
|
return [rows, queryResult]; |
|
} |
|
return rows; |
|
} |
|
formatError(err, errStack) { |
|
let match; |
|
let table; |
|
let index; |
|
let fields; |
|
let errors; |
|
let message; |
|
const code = err.code || err.sqlState; |
|
const errMessage = err.message || err.messagePrimary; |
|
const errDetail = err.detail || err.messageDetail; |
|
switch (code) { |
|
case "23503": |
|
index = errMessage.match(/violates foreign key constraint "(.+?)"/); |
|
index = index ? index[1] : void 0; |
|
table = errMessage.match(/on table "(.+?)"/); |
|
table = table ? table[1] : void 0; |
|
return new sequelizeErrors.ForeignKeyConstraintError({ |
|
message: errMessage, |
|
fields: null, |
|
index, |
|
table, |
|
parent: err, |
|
stack: errStack |
|
}); |
|
case "23505": |
|
if (errDetail && (match = errDetail.replace(/"/g, "").match(/Key \((.*?)\)=\((.*?)\)/))) { |
|
fields = _.zipObject(match[1].split(", "), match[2].split(", ")); |
|
errors = []; |
|
message = "Validation error"; |
|
_.forOwn(fields, (value, field) => { |
|
errors.push(new sequelizeErrors.ValidationErrorItem(this.getUniqueConstraintErrorMessage(field), "unique violation", field, value, this.instance, "not_unique")); |
|
}); |
|
if (this.model && this.model.uniqueKeys) { |
|
_.forOwn(this.model.uniqueKeys, (constraint) => { |
|
if (_.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) { |
|
message = constraint.msg; |
|
return false; |
|
} |
|
}); |
|
} |
|
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields, stack: errStack }); |
|
} |
|
return new sequelizeErrors.UniqueConstraintError({ |
|
message: errMessage, |
|
parent: err, |
|
stack: errStack |
|
}); |
|
case "23P01": |
|
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/); |
|
if (match) { |
|
fields = _.zipObject(match[1].split(", "), match[2].split(", ")); |
|
} |
|
message = "Exclusion constraint error"; |
|
return new sequelizeErrors.ExclusionConstraintError({ |
|
message, |
|
constraint: err.constraint, |
|
fields, |
|
table: err.table, |
|
parent: err, |
|
stack: errStack |
|
}); |
|
case "42704": |
|
if (err.sql && /(CONSTRAINT|INDEX)/gi.test(err.sql)) { |
|
message = "Unknown constraint error"; |
|
index = errMessage.match(/(?:constraint|index) "(.+?)"/i); |
|
index = index ? index[1] : void 0; |
|
table = errMessage.match(/relation "(.+?)"/i); |
|
table = table ? table[1] : void 0; |
|
throw new sequelizeErrors.UnknownConstraintError({ |
|
message, |
|
constraint: index, |
|
fields, |
|
table, |
|
parent: err, |
|
stack: errStack |
|
}); |
|
} |
|
default: |
|
return new sequelizeErrors.DatabaseError(err, { stack: errStack }); |
|
} |
|
} |
|
isForeignKeysQuery() { |
|
return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql); |
|
} |
|
getInsertIdField() { |
|
return "id"; |
|
} |
|
} |
|
module.exports = Query; |
|
module.exports.Query = Query; |
|
module.exports.default = Query; |
|
//# sourceMappingURL=query.js.map
|
|
|