Skip to content

Commit

Permalink
feat(postgres): change returning option to only return model attributes
Browse files Browse the repository at this point in the history
If an underlying table has columns that don't exist in the model,
when using `returning: true` the instance will be created with these
extra attributes. This change alters this behavior to only return the
fields defined in the model, provided a model definition is passed.

BREAKING CHANGE: setting `returning: true` will no longer create
attributes on the instance that are not defined in the model.

The old default behavior, if desired, can be used by changing the:

`returning: true`

to

`returning: ['*']`
  • Loading branch information
Americas committed Oct 16, 2019
1 parent f3bb191 commit e92ac61
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 158 deletions.
169 changes: 87 additions & 82 deletions lib/dialects/abstract/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ 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;
Expand All @@ -131,39 +132,12 @@ class QueryGenerator {
}

if (this._dialect.supports.returnValues && options.returning) {
if (this._dialect.supports.returnValues.returning) {
returningFragment = ' RETURNING *';
returningFragment = ' 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 @@ -217,20 +191,29 @@ 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} RETURNING * 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} RETURNING *; 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;
Expand All @@ -252,6 +235,7 @@ class QueryGenerator {
if (options.bindParam !== false) {
result.bind = bind;
}

return result;
}

Expand Down Expand Up @@ -320,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 @@ -352,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 @@ -368,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 = '';
if (this._dialect.supports.returnValues && (this._dialect.supports.returnValues.output || options.returning)) {
const returnValues = this.generateReturnValues(attributes, options);

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)}`;
}
}

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 @@ -466,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 @@ -485,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 @@ -1730,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 e92ac61

Please sign in to comment.