Skip to content

Commit

Permalink
feat(postgres): minify aliases option (#11095)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrrinot authored and sushantdhiman committed Aug 31, 2019
1 parent 4816005 commit 3fcb2d2
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 30 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Expand Up @@ -61,6 +61,12 @@ jobs:
node_js: '6'
sudo: required
env: POSTGRES_VER=postgres-10 SEQ_PG_PORT=8991 DIALECT=postgres
- stage: test
node_js: '6'
sudo: required
env: POSTGRES_VER=postgres-10 SEQ_PG_PORT=8991 SEQ_PG_MINIFY_ALIASES=1 DIALECT=postgres
script:
- npm run test-integration
- stage: test
node_js: '6'
sudo: required
Expand Down
110 changes: 93 additions & 17 deletions lib/dialects/abstract/query-generator.js
Expand Up @@ -225,7 +225,7 @@ class QueryGenerator {

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} INTO response; EXCEPTION ${options.exception} END ${delimiter
' BEGIN '}${valueQuery} INTO response; EXCEPTION ${options.exception} END ${delimiter
} LANGUAGE plpgsql; SELECT (testfunc.response).*, testfunc.sequelize_caught_exception FROM pg_temp.testfunc(); DROP FUNCTION IF EXISTS pg_temp.testfunc()`;
} else {
options.exception = 'WHEN unique_violation THEN NULL;';
Expand Down Expand Up @@ -411,8 +411,8 @@ class QueryGenerator {

for (const key in attrValueHash) {
if (modelAttributeMap && modelAttributeMap[key] &&
modelAttributeMap[key].autoIncrement === true &&
!this._dialect.supports.autoIncrement.update) {
modelAttributeMap[key].autoIncrement === true &&
!this._dialect.supports.autoIncrement.update) {
// not allowed to update identity column
continue;
}
Expand Down Expand Up @@ -1146,6 +1146,12 @@ class QueryGenerator {
let subJoinQueries = [];
let query;

// Aliases can be passed through subqueries and we don't want to reset them
if (this.options.minifyAliases && !options.aliasesMapping) {
options.aliasesMapping = new Map();
options.aliasesByTable = {};
}

// resolve table name options
if (options.tableAs) {
mainTable.as = this.quoteIdentifier(options.tableAs);
Expand Down Expand Up @@ -1274,12 +1280,14 @@ class QueryGenerator {
offset: options.offset,
limit: options.groupedLimit.limit,
order: groupedLimitOrder,
aliasesMapping: options.aliasesMapping,
aliasesByTable: options.aliasesByTable,
where,
include,
model
},
model
).replace(/;$/, '') }) AS sub`; // Every derived table must have its own alias
).replace(/;$/, '')}) AS sub`; // Every derived table must have its own alias
const placeHolder = this.whereItemQuery(Op.placeholder, true, { model });
const splicePos = baseQuery.indexOf(placeHolder);

Expand Down Expand Up @@ -1329,7 +1337,8 @@ class QueryGenerator {

// Add GROUP BY to sub or main query
if (options.group) {
options.group = Array.isArray(options.group) ? options.group.map(t => this.quote(t, model)).join(', ') : this.quote(options.group, model);
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) {
subQueryItems.push(` GROUP BY ${options.group}`);
} else {
Expand Down Expand Up @@ -1399,6 +1408,12 @@ class QueryGenerator {
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);
}

escapeAttributes(attributes, options, mainTableAs) {
return attributes && attributes.map(attr => {
let addTable = true;
Expand All @@ -1420,7 +1435,13 @@ class QueryGenerator {
} else {
deprecations.noRawAttributes();
}
attr = [attr[0], this.quoteIdentifier(attr[1])].join(' AS ');
let alias = attr[1];

if (this.options.minifyAliases) {
alias = this._getMinifiedAlias(alias, mainTableAs, options);
}

attr = [attr[0], this.quoteIdentifier(alias)].join(' AS ');
} else {
attr = !attr.includes(Utils.TICK_CHAR) && !attr.includes('"')
? this.quoteAttribute(attr, options.model)
Expand Down Expand Up @@ -1502,7 +1523,13 @@ class QueryGenerator {
} else {
prefix = `${this.quoteIdentifier(includeAs.internalAs)}.${this.quoteIdentifier(attr)}`;
}
return `${prefix} AS ${this.quoteIdentifier(`${includeAs.externalAs}.${attrAs}`, true)}`;
let alias = `${includeAs.externalAs}.${attrAs}`;

if (this.options.minifyAliases) {
alias = this._getMinifiedAlias(alias, includeAs.internalAs, topLevelInfo.options);
}

return `${prefix} AS ${this.quoteIdentifier(alias, true)}`;
});
if (include.subQuery && topLevelInfo.subQuery) {
for (const attr of includeAttributes) {
Expand Down Expand Up @@ -1588,6 +1615,34 @@ class QueryGenerator {
};
}

_getMinifiedAlias(alias, tableName, options) {
// We do not want to re-alias in case of a subquery
if (options.aliasesByTable[`${tableName}${alias}`]) {
return options.aliasesByTable[`${tableName}${alias}`];
}

// Do not alias custom suquery_orders
if (alias.match(/subquery_order_[0-9]/)) {
return alias;
}

const minifiedAlias = `_${options.aliasesMapping.size}`;

options.aliasesMapping.set(minifiedAlias, alias);
options.aliasesByTable[`${tableName}${alias}`] = minifiedAlias;

return minifiedAlias;
}

_getAliasForField(tableName, field, options) {
if (this.options.minifyAliases) {
if (options.aliasesByTable[`${tableName}${field}`]) {
return options.aliasesByTable[`${tableName}${field}`];
}
}
return null;
}

generateJoin(include, topLevelInfo) {
const association = include.association;
const parent = include.parent;
Expand Down Expand Up @@ -1627,9 +1682,15 @@ class QueryGenerator {
if (topLevelInfo.options.groupedLimit && parentIsTop || topLevelInfo.subQuery && include.parent.subQuery && !include.subQuery) {
if (parentIsTop) {
// The main model attributes is not aliased to a prefix
joinOn = `${this.quoteTable(parent.as || parent.model.name)}.${this.quoteIdentifier(attrLeft)}`;
const tableName = this.quoteTable(parent.as || parent.model.name);

// Check for potential aliased JOIN condition
joinOn = this._getAliasForField(tableName, attrLeft, topLevelInfo.options) || `${tableName}.${this.quoteIdentifier(attrLeft)}`;
} else {
joinOn = this.quoteIdentifier(`${asLeft.replace(/->/g, '.')}.${attrLeft}`);
const joinSource = `${asLeft.replace(/->/g, '.')}.${attrLeft}`;

// Check for potential aliased JOIN condition
joinOn = this._getAliasForField(asLeft, joinSource, topLevelInfo.options) || this.quoteIdentifier(joinSource);
}
}

Expand Down Expand Up @@ -1672,11 +1733,17 @@ class QueryGenerator {
const throughTable = through.model.getTableName();
const throughAs = `${includeAs.internalAs}->${through.as}`;
const externalThroughAs = `${includeAs.externalAs}.${through.as}`;
const throughAttributes = through.attributes.map(attr =>
`${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)
const throughAttributes = through.attributes.map(attr => {
let alias = `${externalThroughAs}.${Array.isArray(attr) ? attr[1] : attr}`;

if (this.options.minifyAliases) {
alias = this._getMinifiedAlias(alias, throughAs, topLevelInfo.options);
}

return `${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(Array.isArray(attr) ? attr[0] : attr)
} AS ${
this.quoteIdentifier(`${externalThroughAs}.${Array.isArray(attr) ? attr[1] : attr}`)}`
);
this.quoteIdentifier(alias)}`;
});
const association = include.association;
const parentIsTop = !include.parent.association && include.parent.model.name === topLevelInfo.options.model.name;
const tableSource = parentTableName;
Expand Down Expand Up @@ -1717,9 +1784,15 @@ class QueryGenerator {
// Used by both join and subquery where
// If parent include was in a subquery need to join on the aliased attribute
if (topLevelInfo.subQuery && !include.subQuery && include.parent.subQuery && !parentIsTop) {
sourceJoinOn = `${this.quoteIdentifier(`${tableSource}.${attrSource}`)} = `;
// If we are minifying aliases and our JOIN target has been minified, we need to use the alias instead of the original column name
const joinSource = this._getAliasForField(tableSource, `${tableSource}.${attrSource}`, topLevelInfo.options) || `${tableSource}.${attrSource}`;

sourceJoinOn = `${this.quoteIdentifier(joinSource)} = `;
} else {
sourceJoinOn = `${this.quoteTable(tableSource)}.${this.quoteIdentifier(attrSource)} = `;
// If we are minifying aliases and our JOIN target has been minified, we need to use the alias instead of the original column name
const aliasedSource = this._getAliasForField(tableSource, attrSource, topLevelInfo.options) || attrSource;

sourceJoinOn = `${this.quoteTable(tableSource)}.${this.quoteIdentifier(aliasedSource)} = `;
}
sourceJoinOn += `${this.quoteIdentifier(throughAs)}.${this.quoteIdentifier(identSource)}`;

Expand Down Expand Up @@ -1892,6 +1965,7 @@ class QueryGenerator {

if (Array.isArray(options.order)) {
for (let order of options.order) {

// wrap if not array
if (!Array.isArray(order)) {
order = [order];
Expand All @@ -1914,7 +1988,9 @@ class QueryGenerator {
// see https://github.com/sequelize/sequelize/issues/8739
const subQueryAttribute = options.attributes.find(a => Array.isArray(a) && a[0] === order[0] && a[1]);
if (subQueryAttribute) {
order[0] = new Utils.Col(subQueryAttribute[1]);
const modelName = this.quoteIdentifier(model.name);

order[0] = new Utils.Col(this._getAliasForField(modelName, subQueryAttribute[1], options) || subQueryAttribute[1]);
}
}

Expand Down Expand Up @@ -2034,7 +2110,7 @@ class QueryGenerator {
return this.whereItemsQuery(arg);
}
return this.escape(arg);
}).join(', ') })`;
}).join(', ')})`;
}
if (smth instanceof Utils.Col) {
if (Array.isArray(smth.col) && !factory) {
Expand Down
19 changes: 15 additions & 4 deletions lib/dialects/postgres/query.js
Expand Up @@ -74,7 +74,7 @@ class Query extends AbstractQuery {
.then(queryResult => {
complete();

const rows = Array.isArray(queryResult)
let rows = Array.isArray(queryResult)
? queryResult.reduce((allRows, r) => allRows.concat(r.rows || []), [])
: queryResult.rows;
const rowCount = Array.isArray(queryResult)
Expand All @@ -84,6 +84,17 @@ class Query extends AbstractQuery {
)
: queryResult.rowCount;

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');

Expand Down Expand Up @@ -153,7 +164,7 @@ class Query extends AbstractQuery {
row.from = defParts[1];
row.to = defParts[3];
let i;
for (i = 5;i <= 8;i += 3) {
for (i = 5; i <= 8; i += 3) {
if (/(UPDATE|DELETE)/.test(defParts[i])) {
row[`on_${defParts[i].toLowerCase()}`] = defParts[i + 1];
}
Expand All @@ -173,7 +184,7 @@ class Query extends AbstractQuery {
return m;
}, {});
result = rows.map(row => {
return _.mapKeys(row, (value, key)=> {
return _.mapKeys(row, (value, key) => {
const targetAttr = attrsMap[key];
if (typeof targetAttr === 'string' && targetAttr !== key) {
return targetAttr;
Expand Down Expand Up @@ -350,7 +361,7 @@ class Query extends AbstractQuery {
parent: err
});
}
// falls through
// falls through
default:
return new sequelizeErrors.DatabaseError(err);
}
Expand Down
4 changes: 3 additions & 1 deletion lib/sequelize.js
Expand Up @@ -168,6 +168,7 @@ class Sequelize {
* @param {boolean} [options.typeValidation=false] Run built-in type validators on insert and update, and select with where clause, e.g. validate that arguments passed to integer fields are integer-like.
* @param {Object} [options.operatorsAliases] String based operator alias. Pass object to limit set of aliased operators.
* @param {Object} [options.hooks] An object of global hook functions that are called before and after certain lifecycle events. Global hooks will run after any model-specific hooks defined for the same event (See `Sequelize.Model.init()` for a list). Additionally, `beforeConnect()`, `afterConnect()`, `beforeDisconnect()`, and `afterDisconnect()` hooks may be defined here.
* @param {boolean} [options.minifyAliases=false] A flag that defines if aliases should be minified (mostly useful to avoid Postgres alias character limit of 64)
*/
constructor(database, username, password, options) {
let config;
Expand Down Expand Up @@ -254,7 +255,8 @@ class Sequelize {
isolationLevel: null,
databaseVersion: 0,
typeValidation: false,
benchmark: false
benchmark: false,
minifyAliases: false
}, options || {});

if (!this.options.dialect) {
Expand Down
3 changes: 2 additions & 1 deletion test/config/config.js
Expand Up @@ -76,6 +76,7 @@ module.exports = {
pool: {
max: env.SEQ_PG_POOL_MAX || env.SEQ_POOL_MAX || 5,
idle: env.SEQ_PG_POOL_IDLE || env.SEQ_POOL_IDLE || 3000
}
},
minifyAliases: env.SEQ_PG_MINIFY_ALIASES
}
};
6 changes: 3 additions & 3 deletions test/integration/associations/belongs-to-many.test.js
Expand Up @@ -1516,9 +1516,9 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
this.comment = comment;
this.tag = tag;
return this.post.setTags([this.tag]);
}).then( () => {
}).then(() => {
return this.comment.setTags([this.tag]);
}).then( () => {
}).then(() => {
return Promise.all([
this.post.getTags(),
this.comment.getTags()
Expand Down Expand Up @@ -1559,7 +1559,7 @@ describe(Support.getTestDialectTeaser('BelongsToMany'), () => {
foreignKey: 'taggable_id'
});

return this.sequelize.sync({ force: true }).then( () => {
return this.sequelize.sync({ force: true }).then(() => {
return Promise.all([
Post.create({ name: 'post1' }),
Comment.create({ name: 'comment1' }),
Expand Down
4 changes: 2 additions & 2 deletions test/integration/dialects/postgres/dao.test.js
Expand Up @@ -222,7 +222,7 @@ if (dialect.match(/^postgres/)) {
}
},
logging(sql) {
expect(sql).to.equal('Executing (default): SELECT "id", "grappling_hook" AS "grapplingHook", "utilityBelt", "createdAt", "updatedAt" FROM "Equipment" AS "Equipment" WHERE "Equipment"."utilityBelt" = \'"grapplingHook"=>"true"\';');
expect(sql).to.contains(' WHERE "Equipment"."utilityBelt" = \'"grapplingHook"=>"true"\';');
}
});
});
Expand All @@ -247,7 +247,7 @@ if (dialect.match(/^postgres/)) {
}
},
logging(sql) {
expect(sql).to.equal('Executing (default): SELECT "id", "grappling_hook" AS "grapplingHook", "utilityBelt", "createdAt", "updatedAt" FROM "Equipment" AS "Equipment" WHERE CAST(("Equipment"."utilityBelt"#>>\'{grapplingHook}\') AS BOOLEAN) = true;');
expect(sql).to.contains(' WHERE CAST(("Equipment"."utilityBelt"#>>\'{grapplingHook}\') AS BOOLEAN) = true;');
}
});
});
Expand Down

0 comments on commit 3fcb2d2

Please sign in to comment.