不要怂,就是干,撸起袖子干!

Commit 722ed505 by Andy Edwards Committed by GitHub

refactor(dialects/postgres): asyncify methods (#12129)

1 parent ceb0de26
...@@ -84,7 +84,7 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -84,7 +84,7 @@ class ConnectionManager extends AbstractConnectionManager {
return this.lib.types.getTypeParser(oid, ...args); return this.lib.types.getTypeParser(oid, ...args);
} }
connect(config) { async connect(config) {
config.user = config.username; config.user = config.username;
const connectionConfig = _.pick(config, [ const connectionConfig = _.pick(config, [
'user', 'password', 'host', 'database', 'port' 'user', 'password', 'host', 'database', 'port'
...@@ -121,7 +121,7 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -121,7 +121,7 @@ class ConnectionManager extends AbstractConnectionManager {
])); ]));
} }
return new Promise((resolve, reject) => { const connection = await new Promise((resolve, reject) => {
let responded = false; let responded = false;
const connection = new this.lib.Client(connectionConfig); const connection = new this.lib.Client(connectionConfig);
...@@ -194,76 +194,71 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -194,76 +194,71 @@ class ConnectionManager extends AbstractConnectionManager {
resolve(connection); resolve(connection);
} }
}); });
}).then(connection => { });
let query = '';
if (this.sequelize.options.standardConformingStrings !== false && connection['standard_conforming_strings'] !== 'on') {
// Disable escape characters in strings
// see https://github.com/sequelize/sequelize/issues/3545 (security issue)
// see https://www.postgresql.org/docs/current/static/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS
query += 'SET standard_conforming_strings=on;';
}
if (this.sequelize.options.clientMinMessages !== false) { let query = '';
query += `SET client_min_messages TO ${this.sequelize.options.clientMinMessages};`;
}
if (!this.sequelize.config.keepDefaultTimezone) { if (this.sequelize.options.standardConformingStrings !== false && connection['standard_conforming_strings'] !== 'on') {
const isZone = !!moment.tz.zone(this.sequelize.options.timezone); // Disable escape characters in strings
if (isZone) { // see https://github.com/sequelize/sequelize/issues/3545 (security issue)
query += `SET TIME ZONE '${this.sequelize.options.timezone}';`; // see https://www.postgresql.org/docs/current/static/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS
} else { query += 'SET standard_conforming_strings=on;';
query += `SET TIME ZONE INTERVAL '${this.sequelize.options.timezone}' HOUR TO MINUTE;`; }
}
}
if (query) { if (this.sequelize.options.clientMinMessages !== false) {
return Promise.resolve(connection.query(query)).then(() => connection); query += `SET client_min_messages TO ${this.sequelize.options.clientMinMessages};`;
} }
return connection;
}).then(connection => { if (!this.sequelize.config.keepDefaultTimezone) {
if (Object.keys(this.nameOidMap).length === 0 && const isZone = !!moment.tz.zone(this.sequelize.options.timezone);
this.enumOids.oids.length === 0 && if (isZone) {
this.enumOids.arrayOids.length === 0) { query += `SET TIME ZONE '${this.sequelize.options.timezone}';`;
return Promise.resolve(this._refreshDynamicOIDs(connection)).then(() => connection); } else {
query += `SET TIME ZONE INTERVAL '${this.sequelize.options.timezone}' HOUR TO MINUTE;`;
} }
return connection; }
}).then(connection => {
// Don't let a Postgres restart (or error) to take down the whole app if (query) {
connection.on('error', error => { await connection.query(query);
connection._invalid = true; }
debug(`connection error ${error.code || error.message}`); if (Object.keys(this.nameOidMap).length === 0 &&
this.pool.destroy(connection); this.enumOids.oids.length === 0 &&
}); this.enumOids.arrayOids.length === 0) {
return connection; await this._refreshDynamicOIDs(connection);
}
// Don't let a Postgres restart (or error) to take down the whole app
connection.on('error', error => {
connection._invalid = true;
debug(`connection error ${error.code || error.message}`);
this.pool.destroy(connection);
}); });
return connection;
} }
disconnect(connection) { async disconnect(connection) {
if (connection._ending) { if (connection._ending) {
debug('connection tried to disconnect but was already at ENDING state'); debug('connection tried to disconnect but was already at ENDING state');
return Promise.resolve(); return;
} }
return promisify(callback => connection.end(callback))(); return await promisify(callback => connection.end(callback))();
} }
validate(connection) { validate(connection) {
return !connection._invalid && !connection._ending; return !connection._invalid && !connection._ending;
} }
_refreshDynamicOIDs(connection) { async _refreshDynamicOIDs(connection) {
const databaseVersion = this.sequelize.options.databaseVersion; const databaseVersion = this.sequelize.options.databaseVersion;
const supportedVersion = '8.3.0'; const supportedVersion = '8.3.0';
// Check for supported version // Check for supported version
if ( (databaseVersion && semver.gte(databaseVersion, supportedVersion)) === false) { if ( (databaseVersion && semver.gte(databaseVersion, supportedVersion)) === false) {
return Promise.resolve(); return;
} }
// Refresh dynamic OIDs for some types const results = await (connection || this.sequelize).query(
// These include Geometry / Geography / HStore / Enum / Citext / Range
return (connection || this.sequelize).query(
'WITH ranges AS (' + 'WITH ranges AS (' +
' SELECT pg_range.rngtypid, pg_type.typname AS rngtypname,' + ' SELECT pg_range.rngtypid, pg_type.typname AS rngtypname,' +
' pg_type.typarray AS rngtyparray, pg_range.rngsubtype' + ' pg_type.typarray AS rngtyparray, pg_range.rngsubtype' +
...@@ -273,46 +268,46 @@ class ConnectionManager extends AbstractConnectionManager { ...@@ -273,46 +268,46 @@ class ConnectionManager extends AbstractConnectionManager {
' ranges.rngtypname, ranges.rngtypid, ranges.rngtyparray' + ' ranges.rngtypname, ranges.rngtypid, ranges.rngtyparray' +
' FROM pg_type LEFT OUTER JOIN ranges ON pg_type.oid = ranges.rngsubtype' + ' FROM pg_type LEFT OUTER JOIN ranges ON pg_type.oid = ranges.rngsubtype' +
' WHERE (pg_type.typtype IN(\'b\', \'e\'));' ' WHERE (pg_type.typtype IN(\'b\', \'e\'));'
).then(results => { );
let result = Array.isArray(results) ? results.pop() : results;
let result = Array.isArray(results) ? results.pop() : results;
// When searchPath is prepended then two statements are executed and the result is
// an array of those two statements. First one is the SET search_path and second is // When searchPath is prepended then two statements are executed and the result is
// the SELECT query result. // an array of those two statements. First one is the SET search_path and second is
if (Array.isArray(result)) { // the SELECT query result.
if (result[0].command === 'SET') { if (Array.isArray(result)) {
result = result.pop(); if (result[0].command === 'SET') {
} result = result.pop();
} }
}
const newNameOidMap = {}; const newNameOidMap = {};
const newEnumOids = { oids: [], arrayOids: [] }; const newEnumOids = { oids: [], arrayOids: [] };
for (const row of result.rows) { for (const row of result.rows) {
// Mapping enums, handled separatedly // Mapping enums, handled separatedly
if (row.typtype === 'e') { if (row.typtype === 'e') {
newEnumOids.oids.push(row.oid); newEnumOids.oids.push(row.oid);
if (row.typarray) newEnumOids.arrayOids.push(row.typarray); if (row.typarray) newEnumOids.arrayOids.push(row.typarray);
continue; continue;
} }
// Mapping base types and their arrays // Mapping base types and their arrays
newNameOidMap[row.typname] = { oid: row.oid }; newNameOidMap[row.typname] = { oid: row.oid };
if (row.typarray) newNameOidMap[row.typname].arrayOid = row.typarray; if (row.typarray) newNameOidMap[row.typname].arrayOid = row.typarray;
// Mapping ranges(of base types) and their arrays // Mapping ranges(of base types) and their arrays
if (row.rngtypid) { if (row.rngtypid) {
newNameOidMap[row.typname].rangeOid = row.rngtypid; newNameOidMap[row.typname].rangeOid = row.rngtypid;
if (row.rngtyparray) newNameOidMap[row.typname].arrayRangeOid = row.rngtyparray; if (row.rngtyparray) newNameOidMap[row.typname].arrayRangeOid = row.rngtyparray;
}
} }
}
// Replace all OID mappings. Avoids temporary empty OID mappings. // Replace all OID mappings. Avoids temporary empty OID mappings.
this.nameOidMap = newNameOidMap; this.nameOidMap = newNameOidMap;
this.enumOids = newEnumOids; this.enumOids = newEnumOids;
this.refreshTypeParser(dataTypes.postgres); this.refreshTypeParser(dataTypes.postgres);
});
} }
_clearDynamicOIDs() { _clearDynamicOIDs() {
......
...@@ -26,7 +26,7 @@ const _ = require('lodash'); ...@@ -26,7 +26,7 @@ const _ = require('lodash');
* @returns {Promise} * @returns {Promise}
* @private * @private
*/ */
function ensureEnums(qi, tableName, attributes, options, model) { async function ensureEnums(qi, tableName, attributes, options, model) {
const keys = Object.keys(attributes); const keys = Object.keys(attributes);
const keyLen = keys.length; const keyLen = keys.length;
...@@ -50,108 +50,106 @@ function ensureEnums(qi, tableName, attributes, options, model) { ...@@ -50,108 +50,106 @@ function ensureEnums(qi, tableName, attributes, options, model) {
} }
} }
return Promise.all(promises).then(results => { const results = await Promise.all(promises);
promises = []; promises = [];
let enumIdx = 0; let enumIdx = 0;
// This little function allows us to re-use the same code that prepends or appends new value to enum array // This little function allows us to re-use the same code that prepends or appends new value to enum array
const addEnumValue = (field, value, relativeValue, position = 'before', spliceStart = promises.length) => { const addEnumValue = (field, value, relativeValue, position = 'before', spliceStart = promises.length) => {
const valueOptions = _.clone(options); const valueOptions = _.clone(options);
valueOptions.before = null; valueOptions.before = null;
valueOptions.after = null; valueOptions.after = null;
switch (position) { switch (position) {
case 'after': case 'after':
valueOptions.after = relativeValue; valueOptions.after = relativeValue;
break; break;
case 'before': case 'before':
default: default:
valueOptions.before = relativeValue; valueOptions.before = relativeValue;
break; break;
} }
promises.splice(spliceStart, 0, () => { promises.splice(spliceStart, 0, () => {
return qi.sequelize.query(qi.QueryGenerator.pgEnumAdd( return qi.sequelize.query(qi.QueryGenerator.pgEnumAdd(
tableName, field, value, valueOptions tableName, field, value, valueOptions
), valueOptions); ), valueOptions);
}); });
}; };
for (i = 0; i < keyLen; i++) {
const attribute = attributes[keys[i]];
const type = attribute.type;
const enumType = type.type || type;
const field = attribute.field || keys[i];
if (
type instanceof DataTypes.ENUM ||
type instanceof DataTypes.ARRAY && enumType instanceof DataTypes.ENUM //ARRAY sub type is ENUM
) {
// If the enum type doesn't exist then create it
if (!results[enumIdx]) {
promises.push(() => {
return qi.sequelize.query(qi.QueryGenerator.pgEnum(tableName, field, enumType, options), Object.assign({}, options, { raw: true }));
});
} else if (!!results[enumIdx] && !!model) {
const enumVals = qi.QueryGenerator.fromArray(results[enumIdx].enum_value);
const vals = enumType.values;
// Going through already existing values allows us to make queries that depend on those values
// We will prepend all new values between the old ones, but keep in mind - we can't change order of already existing values
// Then we append the rest of new values AFTER the latest already existing value
// E.g.: [1,2] -> [0,2,1] ==> [1,0,2]
// E.g.: [1,2,3] -> [2,1,3,4] ==> [1,2,3,4]
// E.g.: [1] -> [0,2,3] ==> [1,0,2,3]
let lastOldEnumValue;
let rightestPosition = -1;
for (let oldIndex = 0; oldIndex < enumVals.length; oldIndex++) {
const enumVal = enumVals[oldIndex];
const newIdx = vals.indexOf(enumVal);
lastOldEnumValue = enumVal;
if (newIdx === -1) {
continue;
}
const newValuesBefore = vals.slice(0, newIdx); for (i = 0; i < keyLen; i++) {
const promisesLength = promises.length; const attribute = attributes[keys[i]];
// we go in reverse order so we could stop when we meet old value const type = attribute.type;
for (let reverseIdx = newValuesBefore.length - 1; reverseIdx >= 0; reverseIdx--) { const enumType = type.type || type;
if (~enumVals.indexOf(newValuesBefore[reverseIdx])) { const field = attribute.field || keys[i];
break;
}
addEnumValue(field, newValuesBefore[reverseIdx], lastOldEnumValue, 'before', promisesLength); if (
} type instanceof DataTypes.ENUM ||
type instanceof DataTypes.ARRAY && enumType instanceof DataTypes.ENUM //ARRAY sub type is ENUM
) {
// If the enum type doesn't exist then create it
if (!results[enumIdx]) {
promises.push(() => {
return qi.sequelize.query(qi.QueryGenerator.pgEnum(tableName, field, enumType, options), Object.assign({}, options, { raw: true }));
});
} else if (!!results[enumIdx] && !!model) {
const enumVals = qi.QueryGenerator.fromArray(results[enumIdx].enum_value);
const vals = enumType.values;
// Going through already existing values allows us to make queries that depend on those values
// We will prepend all new values between the old ones, but keep in mind - we can't change order of already existing values
// Then we append the rest of new values AFTER the latest already existing value
// E.g.: [1,2] -> [0,2,1] ==> [1,0,2]
// E.g.: [1,2,3] -> [2,1,3,4] ==> [1,2,3,4]
// E.g.: [1] -> [0,2,3] ==> [1,0,2,3]
let lastOldEnumValue;
let rightestPosition = -1;
for (let oldIndex = 0; oldIndex < enumVals.length; oldIndex++) {
const enumVal = enumVals[oldIndex];
const newIdx = vals.indexOf(enumVal);
lastOldEnumValue = enumVal;
if (newIdx === -1) {
continue;
}
// we detect the most 'right' position of old value in new enum array so we can append new values to it const newValuesBefore = vals.slice(0, newIdx);
if (newIdx > rightestPosition) { const promisesLength = promises.length;
rightestPosition = newIdx; // we go in reverse order so we could stop when we meet old value
for (let reverseIdx = newValuesBefore.length - 1; reverseIdx >= 0; reverseIdx--) {
if (~enumVals.indexOf(newValuesBefore[reverseIdx])) {
break;
} }
addEnumValue(field, newValuesBefore[reverseIdx], lastOldEnumValue, 'before', promisesLength);
} }
if (lastOldEnumValue && rightestPosition < vals.length - 1) { // we detect the most 'right' position of old value in new enum array so we can append new values to it
const remainingEnumValues = vals.slice(rightestPosition + 1); if (newIdx > rightestPosition) {
for (let reverseIdx = remainingEnumValues.length - 1; reverseIdx >= 0; reverseIdx--) { rightestPosition = newIdx;
addEnumValue(field, remainingEnumValues[reverseIdx], lastOldEnumValue, 'after');
}
} }
}
enumIdx++; if (lastOldEnumValue && rightestPosition < vals.length - 1) {
const remainingEnumValues = vals.slice(rightestPosition + 1);
for (let reverseIdx = remainingEnumValues.length - 1; reverseIdx >= 0; reverseIdx--) {
addEnumValue(field, remainingEnumValues[reverseIdx], lastOldEnumValue, 'after');
}
} }
enumIdx++;
} }
} }
}
return promises const result = await promises
.reduce((promise, asyncFunction) => promise.then(asyncFunction), Promise.resolve()) .reduce(async (promise, asyncFunction) => await asyncFunction(await promise), Promise.resolve());
.then(result => {
// If ENUM processed, then refresh OIDs // If ENUM processed, then refresh OIDs
if (promises.length) { if (promises.length) {
return Promise.resolve(qi.sequelize.dialect.connectionManager._refreshDynamicOIDs()).then(() => result); await qi.sequelize.dialect.connectionManager._refreshDynamicOIDs();
} }
return result; return result;
});
});
} }
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!