Skip to content

Commit

Permalink
feat(postgres): change returning option to only return model attribut…
Browse files Browse the repository at this point in the history
…es (#11526)
  • Loading branch information
Americas authored and sushantdhiman committed Oct 19, 2019
1 parent 29c9be3 commit 26ea410
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 158 deletions.
173 changes: 91 additions & 82 deletions lib/dialects/abstract/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,17 @@ class QueryGenerator {
_.defaults(options, this.options);

const modelAttributeMap = {};
const bind = [];
const fields = [];
const returningModelAttributes = [];
const values = [];
const bind = [];
const quotedTable = this.quoteTable(table);
const bindParam = options.bindParam === undefined ? this.bindParam(bind) : options.bindParam;
let query;
let valueQuery = '';
let emptyQuery = '';
let outputFragment = '';
let returningFragment = '';
let identityWrapperRequired = false;
let tmpTable = ''; //tmpTable declaration for trigger

Expand All @@ -130,39 +132,12 @@ class QueryGenerator {
}

if (this._dialect.supports.returnValues && options.returning) {
if (this._dialect.supports.returnValues.returning) {
valueQuery += ' RETURNING *';
emptyQuery += ' RETURNING *';
} else if (this._dialect.supports.returnValues.output) {
outputFragment = ' OUTPUT INSERTED.*';

//To capture output rows when there is a trigger on MSSQL DB
if (modelAttributes && options.hasTrigger && this._dialect.supports.tmpTableTrigger) {

let tmpColumns = '';
let outputColumns = '';

for (const modelKey in modelAttributes) {
const attribute = modelAttributes[modelKey];
if (!(attribute.type instanceof DataTypes.VIRTUAL)) {
if (tmpColumns.length > 0) {
tmpColumns += ',';
outputColumns += ',';
}

tmpColumns += `${this.quoteIdentifier(attribute.field)} ${attribute.type.toSql()}`;
outputColumns += `INSERTED.${this.quoteIdentifier(attribute.field)}`;
}
}
const returnValues = this.generateReturnValues(modelAttributes, options);

tmpTable = `declare @tmp table (${tmpColumns});`;
outputFragment = ` OUTPUT ${outputColumns} into @tmp`;
const selectFromTmp = ';select * from @tmp';

valueQuery += selectFromTmp;
emptyQuery += selectFromTmp;
}
}
returningModelAttributes.push(...returnValues.returnFields);
returningFragment = returnValues.returningFragment;
tmpTable = returnValues.tmpTable || '';
outputFragment = returnValues.outputFragment || '';
}

if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) {
Expand Down Expand Up @@ -216,21 +191,33 @@ class QueryGenerator {
valueQuery = `${tmpTable}INSERT${replacements.ignoreDuplicates} INTO ${quotedTable} (${replacements.attributes})${replacements.output} VALUES (${replacements.values})${replacements.onConflictDoNothing}${valueQuery}`;
emptyQuery = `${tmpTable}INSERT${replacements.ignoreDuplicates} INTO ${quotedTable}${replacements.output}${replacements.onConflictDoNothing}${emptyQuery}`;

// Mostly for internal use, so we expect the user to know what he's doing!
// pg_temp functions are private per connection, so we never risk this function interfering with another one.
if (this._dialect.supports.EXCEPTION && options.exception) {
// Mostly for internal use, so we expect the user to know what he's doing!
// pg_temp functions are private per connection, so we never risk this function interfering with another one.
const dropFunction = 'DROP FUNCTION IF EXISTS pg_temp.testfunc()';

if (returningModelAttributes.length === 0) {
returningModelAttributes.push('*');
}

if (semver.gte(this.sequelize.options.databaseVersion, '9.2.0')) {
// >= 9.2 - Use a UUID but prefix with 'func_' (numbers first not allowed)
const delimiter = `$func_${uuidv4().replace(/-/g, '')}$`;
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} 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()`;
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 {
const selectQuery = `SELECT ${returningModelAttributes.join(', ')} FROM pg_temp.testfunc();`;

options.exception = 'WHEN unique_violation THEN NULL;';
valueQuery = `CREATE OR REPLACE FUNCTION pg_temp.testfunc() RETURNS SETOF ${quotedTable} AS $body$ BEGIN RETURN QUERY ${valueQuery}; EXCEPTION ${options.exception} END; $body$ LANGUAGE plpgsql; SELECT * FROM pg_temp.testfunc(); DROP FUNCTION IF EXISTS pg_temp.testfunc();`;
valueQuery = `CREATE OR REPLACE FUNCTION pg_temp.testfunc() RETURNS SETOF ${quotedTable} AS $body$ BEGIN RETURN QUERY ${valueQuery
} RETURNING *; EXCEPTION ${options.exception} END; $body$ LANGUAGE plpgsql; ${selectQuery} ${dropFunction}`;
}
} else {
valueQuery += returningFragment;
emptyQuery += returningFragment;
}

if (this._dialect.supports['ON DUPLICATE KEY'] && options.onDuplicate) {
Expand All @@ -248,6 +235,7 @@ class QueryGenerator {
if (options.bindParam !== false) {
result.bind = bind;
}

return result;
}

Expand Down Expand Up @@ -316,11 +304,10 @@ class QueryGenerator {
const onConflictDoNothing = options.ignoreDuplicates ? this._dialect.supports.inserts.onConflictDoNothing : '';
let returning = '';

if (this._dialect.supports.returnValues && Array.isArray(options.returning)) {
const fields = options.returning.map(field => this.quoteIdentifier(field)).join(',');
returning += ` RETURNING ${fields}`;
} else {
returning += this._dialect.supports.returnValues && options.returning ? ' RETURNING *' : '';
if (this._dialect.supports.returnValues && options.returning) {
const returnValues = this.generateReturnValues(fieldMappedAttributes, options);

returning += returnValues.returningFragment;
}

return `INSERT${ignoreDuplicates} INTO ${this.quoteTable(tableName)} (${attributes}) VALUES ${tuples.join(',')}${onDuplicateKeyUpdate}${onConflictDoNothing}${returning};`;
Expand Down Expand Up @@ -348,7 +335,6 @@ class QueryGenerator {
const modelAttributeMap = {};
let outputFragment = '';
let tmpTable = ''; // tmpTable declaration for trigger
let selectFromTmp = ''; // Select statement for trigger
let suffix = '';

if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) {
Expand All @@ -364,39 +350,16 @@ class QueryGenerator {
}
}

if (this._dialect.supports.returnValues) {
if (this._dialect.supports.returnValues.output) {
// we always need this for mssql
outputFragment = ' OUTPUT INSERTED.*';

//To capture output rows when there is a trigger on MSSQL DB
if (attributes && options.hasTrigger && this._dialect.supports.tmpTableTrigger) {
let tmpColumns = '';
let outputColumns = '';

for (const modelKey in attributes) {
const attribute = attributes[modelKey];
if (!(attribute.type instanceof DataTypes.VIRTUAL)) {
if (tmpColumns.length > 0) {
tmpColumns += ',';
outputColumns += ',';
}

tmpColumns += `${this.quoteIdentifier(attribute.field)} ${attribute.type.toSql()}`;
outputColumns += `INSERTED.${this.quoteIdentifier(attribute.field)}`;
}
}
if (this._dialect.supports.returnValues && (this._dialect.supports.returnValues.output || options.returning)) {
const returnValues = this.generateReturnValues(attributes, options);

tmpTable = `declare @tmp table (${tmpColumns}); `;
outputFragment = ` OUTPUT ${outputColumns} into @tmp`;
selectFromTmp = ';select * from @tmp';
suffix += returnValues.returningFragment;
tmpTable = returnValues.tmpTable || '';
outputFragment = returnValues.outputFragment || '';

suffix += selectFromTmp;
}
} else if (this._dialect.supports.returnValues && options.returning) {
// ensure that the return output is properly mapped to model fields.
// ensure that the return output is properly mapped to model fields.
if (!this._dialect.supports.returnValues.output && options.returning) {
options.mapToModel = true;
suffix += ' RETURNING *';
}
}

Expand Down Expand Up @@ -462,12 +425,10 @@ class QueryGenerator {
let returningFragment = '';

if (this._dialect.supports.returnValues && options.returning) {
if (this._dialect.supports.returnValues.returning) {
options.mapToModel = true;
returningFragment = 'RETURNING *';
} else if (this._dialect.supports.returnValues.output) {
outputFragment = ' OUTPUT INSERTED.*';
}
const returnValues = this.generateReturnValues(null, options);

outputFragment = returnValues.outputFragment;
returningFragment = returnValues.returningFragment;
}

for (const key in attrValueHash) {
Expand All @@ -481,7 +442,7 @@ class QueryGenerator {
values.push(`${this.quoteIdentifier(key)}=${this.escape(value)}`);
}

return `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')}${outputFragment} ${this.whereQuery(where)} ${returningFragment}`.trim();
return `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')}${outputFragment} ${this.whereQuery(where)}${returningFragment}`.trim();
}

/*
Expand Down Expand Up @@ -1726,6 +1687,54 @@ class QueryGenerator {
};
}

/**
* Returns the SQL fragments to handle returning the attributes from an insert/update query.
*
* @param {Object} modelAttributes An object with the model attributes.
* @param {Object} options An object with options.
*
* @private
*/
generateReturnValues(modelAttributes, options) {
const returnFields = [];
const returnTypes = [];
let outputFragment = '';
let returningFragment = '';
let tmpTable = '';

if (Array.isArray(options.returning)) {
returnFields.push(...options.returning.map(field => this.quoteIdentifier(field)));
} else if (modelAttributes) {
_.each(modelAttributes, attribute => {
if (!(attribute.type instanceof DataTypes.VIRTUAL)) {
returnFields.push(this.quoteIdentifier(attribute.field));
returnTypes.push(attribute.type);
}
});
}

if (_.isEmpty(returnFields)) {
returnFields.push('*');
}

if (this._dialect.supports.returnValues.returning) {
returningFragment = ` RETURNING ${returnFields.join(',')}`;
} else if (this._dialect.supports.returnValues.output) {
outputFragment = ` OUTPUT ${returnFields.map(field => `INSERTED.${field}`).join(',')}`;

//To capture output rows when there is a trigger on MSSQL DB
if (options.hasTrigger && this._dialect.supports.tmpTableTrigger) {
const tmpColumns = returnFields.map((field, i) => `${field} ${returnTypes[i].toSql()}`);

tmpTable = `DECLARE @tmp TABLE (${tmpColumns.join(',')}); `;
outputFragment += ' INTO @tmp';
returningFragment = '; SELECT * FROM @tmp';
}
}

return { outputFragment, returnFields, returningFragment, tmpTable };
}

generateThroughJoin(include, includeAs, parentTableName, topLevelInfo) {
const through = include.through;
const throughTable = through.model.getTableName();
Expand Down
4 changes: 3 additions & 1 deletion lib/dialects/mssql/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,9 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
outputFragment = '';

if (options.returning) {
outputFragment = ' OUTPUT INSERTED.*';
const returnValues = this.generateReturnValues(attributes, options);

outputFragment = returnValues.outputFragment;
}

const emptyQuery = `INSERT INTO ${quotedTable}${outputFragment} DEFAULT VALUES`;
Expand Down
2 changes: 1 addition & 1 deletion lib/dialects/postgres/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ class PostgresQueryGenerator extends AbstractQueryGenerator {
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
const primaryField = this.quoteIdentifier(model.primaryKeyField);

const upsertOptions = _.defaults({ bindParam: false }, options);
const upsertOptions = _.defaults({ bindParam: false, returning: ['*'] }, options);
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);

Expand Down

0 comments on commit 26ea410

Please sign in to comment.