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

Commit 26ea410e by Ricardo Proença Committed by Sushant

feat(postgres): change returning option to only return model attributes (#11526)

1 parent 29c9be37
...@@ -102,15 +102,17 @@ class QueryGenerator { ...@@ -102,15 +102,17 @@ class QueryGenerator {
_.defaults(options, this.options); _.defaults(options, this.options);
const modelAttributeMap = {}; const modelAttributeMap = {};
const bind = [];
const fields = []; const fields = [];
const returningModelAttributes = [];
const values = []; const values = [];
const bind = [];
const quotedTable = this.quoteTable(table); const quotedTable = this.quoteTable(table);
const bindParam = options.bindParam === undefined ? this.bindParam(bind) : options.bindParam; const bindParam = options.bindParam === undefined ? this.bindParam(bind) : options.bindParam;
let query; let query;
let valueQuery = ''; let valueQuery = '';
let emptyQuery = ''; let emptyQuery = '';
let outputFragment = ''; let outputFragment = '';
let returningFragment = '';
let identityWrapperRequired = false; let identityWrapperRequired = false;
let tmpTable = ''; //tmpTable declaration for trigger let tmpTable = ''; //tmpTable declaration for trigger
...@@ -130,39 +132,12 @@ class QueryGenerator { ...@@ -130,39 +132,12 @@ class QueryGenerator {
} }
if (this._dialect.supports.returnValues && options.returning) { if (this._dialect.supports.returnValues && options.returning) {
if (this._dialect.supports.returnValues.returning) { const returnValues = this.generateReturnValues(modelAttributes, options);
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)}`;
}
}
tmpTable = `declare @tmp table (${tmpColumns});`; returningModelAttributes.push(...returnValues.returnFields);
outputFragment = ` OUTPUT ${outputColumns} into @tmp`; returningFragment = returnValues.returningFragment;
const selectFromTmp = ';select * from @tmp'; tmpTable = returnValues.tmpTable || '';
outputFragment = returnValues.outputFragment || '';
valueQuery += selectFromTmp;
emptyQuery += selectFromTmp;
}
}
} }
if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) { if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) {
...@@ -216,21 +191,33 @@ class QueryGenerator { ...@@ -216,21 +191,33 @@ class QueryGenerator {
valueQuery = `${tmpTable}INSERT${replacements.ignoreDuplicates} INTO ${quotedTable} (${replacements.attributes})${replacements.output} VALUES (${replacements.values})${replacements.onConflictDoNothing}${valueQuery}`; 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}`; 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) { if (this._dialect.supports.EXCEPTION && options.exception) {
// Mostly for internal use, so we expect the user to know what he's doing! const dropFunction = 'DROP FUNCTION IF EXISTS pg_temp.testfunc()';
// pg_temp functions are private per connection, so we never risk this function interfering with another one.
if (returningModelAttributes.length === 0) {
returningModelAttributes.push('*');
}
if (semver.gte(this.sequelize.options.databaseVersion, '9.2.0')) { if (semver.gte(this.sequelize.options.databaseVersion, '9.2.0')) {
// >= 9.2 - Use a UUID but prefix with 'func_' (numbers first not allowed) // >= 9.2 - Use a UUID but prefix with 'func_' (numbers first not allowed)
const delimiter = `$func_${uuidv4().replace(/-/g, '')}$`; 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;'; 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}` + 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} RETURNING * INTO response; EXCEPTION ${options.exception} END ${delimiter} LANGUAGE plpgsql; ${selectQuery} ${dropFunction}`;
} LANGUAGE plpgsql; SELECT (testfunc.response).*, testfunc.sequelize_caught_exception FROM pg_temp.testfunc(); DROP FUNCTION IF EXISTS pg_temp.testfunc()`;
} else { } else {
const selectQuery = `SELECT ${returningModelAttributes.join(', ')} FROM pg_temp.testfunc();`;
options.exception = 'WHEN unique_violation THEN NULL;'; 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) { if (this._dialect.supports['ON DUPLICATE KEY'] && options.onDuplicate) {
...@@ -248,6 +235,7 @@ class QueryGenerator { ...@@ -248,6 +235,7 @@ class QueryGenerator {
if (options.bindParam !== false) { if (options.bindParam !== false) {
result.bind = bind; result.bind = bind;
} }
return result; return result;
} }
...@@ -316,11 +304,10 @@ class QueryGenerator { ...@@ -316,11 +304,10 @@ class QueryGenerator {
const onConflictDoNothing = options.ignoreDuplicates ? this._dialect.supports.inserts.onConflictDoNothing : ''; const onConflictDoNothing = options.ignoreDuplicates ? this._dialect.supports.inserts.onConflictDoNothing : '';
let returning = ''; let returning = '';
if (this._dialect.supports.returnValues && Array.isArray(options.returning)) { if (this._dialect.supports.returnValues && options.returning) {
const fields = options.returning.map(field => this.quoteIdentifier(field)).join(','); const returnValues = this.generateReturnValues(fieldMappedAttributes, options);
returning += ` RETURNING ${fields}`;
} else { returning += returnValues.returningFragment;
returning += this._dialect.supports.returnValues && options.returning ? ' RETURNING *' : '';
} }
return `INSERT${ignoreDuplicates} INTO ${this.quoteTable(tableName)} (${attributes}) VALUES ${tuples.join(',')}${onDuplicateKeyUpdate}${onConflictDoNothing}${returning};`; return `INSERT${ignoreDuplicates} INTO ${this.quoteTable(tableName)} (${attributes}) VALUES ${tuples.join(',')}${onDuplicateKeyUpdate}${onConflictDoNothing}${returning};`;
...@@ -348,7 +335,6 @@ class QueryGenerator { ...@@ -348,7 +335,6 @@ class QueryGenerator {
const modelAttributeMap = {}; const modelAttributeMap = {};
let outputFragment = ''; let outputFragment = '';
let tmpTable = ''; // tmpTable declaration for trigger let tmpTable = ''; // tmpTable declaration for trigger
let selectFromTmp = ''; // Select statement for trigger
let suffix = ''; let suffix = '';
if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) { if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) {
...@@ -364,39 +350,16 @@ class QueryGenerator { ...@@ -364,39 +350,16 @@ class QueryGenerator {
} }
} }
if (this._dialect.supports.returnValues) { if (this._dialect.supports.returnValues && (this._dialect.supports.returnValues.output || options.returning)) {
if (this._dialect.supports.returnValues.output) { const returnValues = this.generateReturnValues(attributes, options);
// 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)}`;
}
}
tmpTable = `declare @tmp table (${tmpColumns}); `; suffix += returnValues.returningFragment;
outputFragment = ` OUTPUT ${outputColumns} into @tmp`; tmpTable = returnValues.tmpTable || '';
selectFromTmp = ';select * from @tmp'; outputFragment = returnValues.outputFragment || '';
suffix += selectFromTmp; // ensure that the return output is properly mapped to model fields.
} if (!this._dialect.supports.returnValues.output && options.returning) {
} else if (this._dialect.supports.returnValues && options.returning) {
// ensure that the return output is properly mapped to model fields.
options.mapToModel = true; options.mapToModel = true;
suffix += ' RETURNING *';
} }
} }
...@@ -462,12 +425,10 @@ class QueryGenerator { ...@@ -462,12 +425,10 @@ class QueryGenerator {
let returningFragment = ''; let returningFragment = '';
if (this._dialect.supports.returnValues && options.returning) { if (this._dialect.supports.returnValues && options.returning) {
if (this._dialect.supports.returnValues.returning) { const returnValues = this.generateReturnValues(null, options);
options.mapToModel = true;
returningFragment = 'RETURNING *'; outputFragment = returnValues.outputFragment;
} else if (this._dialect.supports.returnValues.output) { returningFragment = returnValues.returningFragment;
outputFragment = ' OUTPUT INSERTED.*';
}
} }
for (const key in attrValueHash) { for (const key in attrValueHash) {
...@@ -481,7 +442,7 @@ class QueryGenerator { ...@@ -481,7 +442,7 @@ class QueryGenerator {
values.push(`${this.quoteIdentifier(key)}=${this.escape(value)}`); 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();
} }
/* /*
...@@ -1726,6 +1687,54 @@ class QueryGenerator { ...@@ -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) { generateThroughJoin(include, includeAs, parentTableName, topLevelInfo) {
const through = include.through; const through = include.through;
const throughTable = through.model.getTableName(); const throughTable = through.model.getTableName();
......
...@@ -316,7 +316,9 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator { ...@@ -316,7 +316,9 @@ class MSSQLQueryGenerator extends AbstractQueryGenerator {
outputFragment = ''; outputFragment = '';
if (options.returning) { if (options.returning) {
outputFragment = ' OUTPUT INSERTED.*'; const returnValues = this.generateReturnValues(attributes, options);
outputFragment = returnValues.outputFragment;
} }
const emptyQuery = `INSERT INTO ${quotedTable}${outputFragment} DEFAULT VALUES`; const emptyQuery = `INSERT INTO ${quotedTable}${outputFragment} DEFAULT VALUES`;
......
...@@ -343,7 +343,7 @@ class PostgresQueryGenerator extends AbstractQueryGenerator { ...@@ -343,7 +343,7 @@ class PostgresQueryGenerator extends AbstractQueryGenerator {
upsertQuery(tableName, insertValues, updateValues, where, model, options) { upsertQuery(tableName, insertValues, updateValues, where, model, options) {
const primaryField = this.quoteIdentifier(model.primaryKeyField); 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 insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes); const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
......
...@@ -2187,27 +2187,26 @@ class Model { ...@@ -2187,27 +2187,26 @@ class Model {
/** /**
* Builds a new model instance and calls save on it. * Builds a new model instance and calls save on it.
*
* @see * @see
* {@link Model.build} * {@link Model.build}
* @see * @see
* {@link Model.save} * {@link Model.save}
* *
* @param {Object} values hash of data values to create new record with * @param {Object} values Hash of data values to create new record with
* @param {Object} [options] build and query options * @param {Object} [options] Build and query options
* @param {boolean} [options.raw=false] If set to true, values will ignore field and virtual setters. * @param {boolean} [options.raw=false] If set to true, values will ignore field and virtual setters.
* @param {boolean} [options.isNewRecord=true] Is this new record * @param {boolean} [options.isNewRecord=true] Is this new record
* @param {Array} [options.include] an array of include options - Used to build prefetched/included model instances. See `set` * @param {Array} [options.include] An array of include options - Used to build prefetched/included model instances. See `set`
* @param {Array} [options.fields] If set, only columns matching those in fields will be saved * @param {string[]} [options.fields] An optional array of strings, representing database columns. If fields is provided, only those columns will be validated and saved.
* @param {string[]} [options.fields] An optional array of strings, representing database columns. If fields is provided, only those columns will be validated and saved. * @param {boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
* @param {boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated. * @param {boolean} [options.validate=true] If false, validations won't be run.
* @param {boolean} [options.validate=true] If false, validations won't be run. * @param {boolean} [options.hooks=true] Run before and after create / update + validate hooks
* @param {boolean} [options.hooks=true] Run before and after create / update + validate hooks * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging).
* @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging). * @param {Transaction} [options.transaction] Transaction to run query under
* @param {Transaction} [options.transaction] Transaction to run query under * @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only)
* @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only) * @param {boolean|Array} [options.returning=true] Appends RETURNING <model columns> to get back all defined values; if an array of column names, append RETURNING <columns> to get back specific columns (Postgres only)
* @param {boolean} [options.returning=true] Return the affected rows (only for postgres)
* *
* @returns {Promise<Model>} * @returns {Promise<Model>}
* *
...@@ -2326,6 +2325,7 @@ class Model { ...@@ -2326,6 +2325,7 @@ class Model {
} }
options.exception = true; options.exception = true;
options.returning = true;
return this.create(values, options).then(instance => { return this.create(values, options).then(instance => {
if (instance.get(this.primaryKeyAttribute, { raw: true }) === null) { if (instance.get(this.primaryKeyAttribute, { raw: true }) === null) {
...@@ -2421,18 +2421,18 @@ class Model { ...@@ -2421,18 +2421,18 @@ class Model {
* * MSSQL - Implemented as a single query using `MERGE` and `WHEN (NOT) MATCHED THEN` * * MSSQL - Implemented as a single query using `MERGE` and `WHEN (NOT) MATCHED THEN`
* **Note** that SQLite returns undefined for created, no matter if the row was created or updated. This is because SQLite always runs INSERT OR IGNORE + UPDATE, in a single query, so there is no way to know whether the row was inserted or not. * **Note** that SQLite returns undefined for created, no matter if the row was created or updated. This is because SQLite always runs INSERT OR IGNORE + UPDATE, in a single query, so there is no way to know whether the row was inserted or not.
* *
* @param {Object} values hash of values to upsert * @param {Object} values hash of values to upsert
* @param {Object} [options] upsert options * @param {Object} [options] upsert options
* @param {boolean} [options.validate=true] Run validations before the row is inserted * @param {boolean} [options.validate=true] Run validations before the row is inserted
* @param {Array} [options.fields=Object.keys(this.attributes)] The fields to insert / update. Defaults to all changed fields * @param {Array} [options.fields=Object.keys(this.attributes)] The fields to insert / update. Defaults to all changed fields
* @param {boolean} [options.hooks=true] Run before / after upsert hooks? * @param {boolean} [options.hooks=true] Run before / after upsert hooks?
* @param {boolean} [options.returning=false] Append RETURNING * to get back auto generated values (Postgres only) * @param {boolean} [options.returning=false] If true, fetches back auto generated values (Postgres only)
* @param {Transaction} [options.transaction] Transaction to run query under * @param {Transaction} [options.transaction] Transaction to run query under
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging). * @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging).
* @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only) * @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only)
* *
* @returns {Promise<boolean>} Returns a boolean indicating whether the row was created or updated. For MySQL/MariaDB, it returns `true` when inserted and `false` when updated. For Postgres/MSSQL with (options.returning=true), it returns record and created boolean with signature `<Model, created>`. * @returns {Promise<boolean>} Returns a boolean indicating whether the row was created or updated. For MySQL/MariaDB, it returns `true` when inserted and `false` when updated. For Postgres/MSSQL with `options.returning` true, it returns record and created boolean with signature `<Model, created>`.
*/ */
static upsert(values, options) { static upsert(values, options) {
options = Object.assign({ options = Object.assign({
...@@ -2513,19 +2513,19 @@ class Model { ...@@ -2513,19 +2513,19 @@ class Model {
* *
* If validation fails, the promise is rejected with an array-like [AggregateError](http://bluebirdjs.com/docs/api/aggregateerror.html) * If validation fails, the promise is rejected with an array-like [AggregateError](http://bluebirdjs.com/docs/api/aggregateerror.html)
* *
* @param {Array} records List of objects (key/value pairs) to create instances from * @param {Array} records List of objects (key/value pairs) to create instances from
* @param {Object} [options] Bulk create options * @param {Object} [options] Bulk create options
* @param {Array} [options.fields] Fields to insert (defaults to all fields) * @param {Array} [options.fields] Fields to insert (defaults to all fields)
* @param {boolean} [options.validate=false] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation * @param {boolean} [options.validate=false] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation
* @param {boolean} [options.hooks=true] Run before / after bulk create hooks? * @param {boolean} [options.hooks=true] Run before / after bulk create hooks?
* @param {boolean} [options.individualHooks=false] Run before / after create hooks for each individual Instance? BulkCreate hooks will still be run if options.hooks is true. * @param {boolean} [options.individualHooks=false] Run before / after create hooks for each individual Instance? BulkCreate hooks will still be run if options.hooks is true.
* @param {boolean} [options.ignoreDuplicates=false] Ignore duplicate values for primary keys? (not supported by MSSQL or Postgres < 9.5) * @param {boolean} [options.ignoreDuplicates=false] Ignore duplicate values for primary keys? (not supported by MSSQL or Postgres < 9.5)
* @param {Array} [options.updateOnDuplicate] Fields to update if row key already exists (on duplicate key update)? (only supported by MySQL, MariaDB, SQLite >= 3.24.0 & Postgres >= 9.5). By default, all fields are updated. * @param {Array} [options.updateOnDuplicate] Fields to update if row key already exists (on duplicate key update)? (only supported by MySQL, MariaDB, SQLite >= 3.24.0 & Postgres >= 9.5). By default, all fields are updated.
* @param {Transaction} [options.transaction] Transaction to run query under * @param {Transaction} [options.transaction] Transaction to run query under
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging). * @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging).
* @param {boolean|Array} [options.returning=false] If true, append RETURNING * to get back all values; if an array of column names, append RETURNING <columns> to get back specific columns (Postgres only) * @param {boolean|Array} [options.returning=false] If true, append RETURNING <model columns> to get back all defined values; if an array of column names, append RETURNING <columns> to get back specific columns (Postgres only)
* @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only) * @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only)
* *
* @returns {Promise<Array<Model>>} * @returns {Promise<Array<Model>>}
*/ */
...@@ -2708,7 +2708,7 @@ class Model { ...@@ -2708,7 +2708,7 @@ class Model {
// Map returning attributes to fields // Map returning attributes to fields
if (options.returning && Array.isArray(options.returning)) { if (options.returning && Array.isArray(options.returning)) {
options.returning = options.returning.map(attr => model.rawAttributes[attr].field || attr); options.returning = options.returning.map(attr => _.get(model.rawAttributes[attr], 'field', attr));
} }
return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => { return model.QueryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes).then(results => {
...@@ -3025,24 +3025,24 @@ class Model { ...@@ -3025,24 +3025,24 @@ class Model {
/** /**
* Update multiple instances that match the where options. * Update multiple instances that match the where options.
* *
* @param {Object} values hash of values to update * @param {Object} values hash of values to update
* @param {Object} options update options * @param {Object} options update options
* @param {Object} options.where Options to describe the scope of the search. * @param {Object} options.where Options to describe the scope of the search.
* @param {boolean} [options.paranoid=true] If true, only non-deleted records will be updated. If false, both deleted and non-deleted records will be updated. Only applies if `options.paranoid` is true for the model. * @param {boolean} [options.paranoid=true] If true, only non-deleted records will be updated. If false, both deleted and non-deleted records will be updated. Only applies if `options.paranoid` is true for the model.
* @param {Array} [options.fields] Fields to update (defaults to all fields) * @param {Array} [options.fields] Fields to update (defaults to all fields)
* @param {boolean} [options.validate=true] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation * @param {boolean} [options.validate=true] Should each row be subject to validation before it is inserted. The whole insert will fail if one row fails validation
* @param {boolean} [options.hooks=true] Run before / after bulk update hooks? * @param {boolean} [options.hooks=true] Run before / after bulk update hooks?
* @param {boolean} [options.sideEffects=true] Whether or not to update the side effects of any virtual setters. * @param {boolean} [options.sideEffects=true] Whether or not to update the side effects of any virtual setters.
* @param {boolean} [options.individualHooks=false] Run before / after update hooks?. If true, this will execute a SELECT followed by individual UPDATEs. A select is needed, because the row data needs to be passed to the hooks * @param {boolean} [options.individualHooks=false] Run before / after update hooks?. If true, this will execute a SELECT followed by individual UPDATEs. A select is needed, because the row data needs to be passed to the hooks
* @param {boolean} [options.returning=false] Return the affected rows (only for postgres) * @param {boolean|Array} [options.returning=false] If true, append RETURNING <model columns> to get back all defined values; if an array of column names, append RETURNING <columns> to get back specific columns (Postgres only)
* @param {number} [options.limit] How many rows to update (only for mysql and mariadb, implemented as TOP(n) for MSSQL; for sqlite it is supported only when rowid is present) * @param {number} [options.limit] How many rows to update (only for mysql and mariadb, implemented as TOP(n) for MSSQL; for sqlite it is supported only when rowid is present)
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging). * @param {boolean} [options.benchmark=false] Pass query execution time in milliseconds as second argument to logging function (options.logging).
* @param {Transaction} [options.transaction] Transaction to run query under * @param {Transaction} [options.transaction] Transaction to run query under
* @param {boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated. * @param {boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
* *
* @returns {Promise<Array<number,number>>} The promise returns an array with one or two elements. The first element is always the number * @returns {Promise<Array<number,number>>} The promise returns an array with one or two elements. The first element is always the number
* of affected rows, while the second element is the actual affected rows (only supported in postgres with `options.returning` true.) * of affected rows, while the second element is the actual affected rows (only supported in postgres with `options.returning` true).
* *
*/ */
static update(values, options) { static update(values, options) {
...@@ -3316,16 +3316,16 @@ class Model { ...@@ -3316,16 +3316,16 @@ class Model {
* @see * @see
* {@link Model#reload} * {@link Model#reload}
* *
* @param {string|Array|Object} fields If a string is provided, that column is incremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is incremented by the value given. * @param {string|Array|Object} fields If a string is provided, that column is incremented by the value of `by` given in options. If an array is provided, the same is true for each column. If and object is provided, each column is incremented by the value given.
* @param {Object} options increment options * @param {Object} options increment options
* @param {Object} options.where conditions hash * @param {Object} options.where conditions hash
* @param {number} [options.by=1] The number to increment by * @param {number} [options.by=1] The number to increment by
* @param {boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated. * @param {boolean} [options.silent=false] If true, the updatedAt timestamp will not be updated.
* @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql. * @param {Function} [options.logging=false] A function that gets executed while running the query to log the sql.
* @param {Transaction} [options.transaction] Transaction to run query under * @param {Transaction} [options.transaction] Transaction to run query under
* @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only) * @param {string} [options.searchPath=DEFAULT] An optional parameter to specify the schema search_path (Postgres only)
* *
* @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning: true`, whenever supported by dialect * @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning` true, whenever supported by dialect
*/ */
static increment(fields, options) { static increment(fields, options) {
options = options || {}; options = options || {};
...@@ -3411,8 +3411,8 @@ class Model { ...@@ -3411,8 +3411,8 @@ class Model {
* @see * @see
* {@link Model#reload} * {@link Model#reload}
* @since 4.36.0 * @since 4.36.0
*
* @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning: true`, whenever supported by dialect * @returns {Promise<Model[],?number>} returns an array of affected rows and affected count with `options.returning` true, whenever supported by dialect
*/ */
static decrement(fields, options) { static decrement(fields, options) {
options = _.defaults({ increment: false }, options, { options = _.defaults({ increment: false }, options, {
......
...@@ -929,14 +929,16 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -929,14 +929,16 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return User.sync({ force: true }).then(() => { return User.sync({ force: true }).then(() => {
return User.create({ username: 'Peter', secretValue: '42' }).then(user => { return User.create({ username: 'Peter', secretValue: '42' }).then(user => {
return user.update({ secretValue: '43' }, { return user.update({ secretValue: '43' }, {
fields: ['secretValue'], logging(sql) { fields: ['secretValue'],
logging(sql) {
test = true; test = true;
if (dialect === 'mssql') { if (dialect === 'mssql') {
expect(sql).to.not.contain('createdAt'); expect(sql).to.not.contain('createdAt');
} else { } else {
expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]=(\$1|\?),[`"]+updatedAt[`"]+=(\$2|\?)\s+WHERE [`"]+id[`"]+\s=\s(\$3|\?)/); expect(sql).to.match(/UPDATE\s+[`"]+User1s[`"]+\s+SET\s+[`"]+secretValue[`"]=(\$1|\?),[`"]+updatedAt[`"]+=(\$2|\?)\s+WHERE [`"]+id[`"]+\s=\s(\$3|\?)/);
} }
} },
returning: ['*']
}); });
}); });
}).then(() => { }).then(() => {
......
...@@ -628,6 +628,56 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -628,6 +628,56 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
}); });
it('should only return fields that are not defined in the model (with returning: true)', function() {
const User = this.sequelize.define('user');
return User
.sync({ force: true })
.then(() => this.sequelize.queryInterface.addColumn('users', 'not_on_model', Sequelize.STRING))
.then(() => User.bulkCreate([
{},
{},
{}
], {
returning: true
}))
.then(users =>
User.findAll()
.then(actualUsers => [users, actualUsers])
)
.then(([users, actualUsers]) => {
expect(users.length).to.eql(actualUsers.length);
users.forEach(user => {
expect(user.get()).not.to.have.property('not_on_model');
});
});
});
it('should return fields that are not defined in the model (with returning: ["*"])', function() {
const User = this.sequelize.define('user');
return User
.sync({ force: true })
.then(() => this.sequelize.queryInterface.addColumn('users', 'not_on_model', Sequelize.STRING))
.then(() => User.bulkCreate([
{},
{},
{}
], {
returning: ['*']
}))
.then(users =>
User.findAll()
.then(actualUsers => [users, actualUsers])
)
.then(([users, actualUsers]) => {
expect(users.length).to.eql(actualUsers.length);
users.forEach(user => {
expect(user.get()).to.have.property('not_on_model');
});
});
});
}); });
} }
......
...@@ -393,6 +393,18 @@ describe(Support.getTestDialectTeaser('Model'), () => { ...@@ -393,6 +393,18 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}); });
}); });
it('should ignore option returning', function() {
return this.User.findOrCreate({
where: { username: 'Username' },
defaults: { data: 'ThisIsData' },
returning: false
}).then(([user, created]) => {
expect(user.username).to.equal('Username');
expect(user.data).to.equal('ThisIsData');
expect(created).to.be.true;
});
});
if (current.dialect.supports.transactions) { if (current.dialect.supports.transactions) {
it('should release transaction when meeting errors', function() { it('should release transaction when meeting errors', function() {
const test = times => { const test = times => {
......
...@@ -28,8 +28,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ...@@ -28,8 +28,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
expectsql(sql.insertQuery(User.tableName, { user_name: 'triggertest' }, User.rawAttributes, options), expectsql(sql.insertQuery(User.tableName, { user_name: 'triggertest' }, User.rawAttributes, options),
{ {
query: { query: {
mssql: 'declare @tmp table ([id] INTEGER,[user_name] NVARCHAR(255));INSERT INTO [users] ([user_name]) OUTPUT INSERTED.[id],INSERTED.[user_name] into @tmp VALUES ($1);select * from @tmp;', mssql: 'DECLARE @tmp TABLE ([id] INTEGER,[user_name] NVARCHAR(255)); INSERT INTO [users] ([user_name]) OUTPUT INSERTED.[id],INSERTED.[user_name] INTO @tmp VALUES ($1); SELECT * FROM @tmp;',
postgres: 'INSERT INTO "users" ("user_name") VALUES ($1) RETURNING *;', postgres: 'INSERT INTO "users" ("user_name") VALUES ($1) RETURNING "id","user_name";',
default: 'INSERT INTO `users` (`user_name`) VALUES ($1);' default: 'INSERT INTO `users` (`user_name`) VALUES ($1);'
}, },
bind: ['triggertest'] bind: ['triggertest']
......
...@@ -28,8 +28,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => { ...@@ -28,8 +28,8 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
expectsql(sql.updateQuery(User.tableName, { user_name: 'triggertest' }, { id: 2 }, options, User.rawAttributes), expectsql(sql.updateQuery(User.tableName, { user_name: 'triggertest' }, { id: 2 }, options, User.rawAttributes),
{ {
query: { query: {
mssql: 'declare @tmp table ([id] INTEGER,[user_name] NVARCHAR(255)); UPDATE [users] SET [user_name]=$1 OUTPUT INSERTED.[id],INSERTED.[user_name] into @tmp WHERE [id] = $2;select * from @tmp', mssql: 'DECLARE @tmp TABLE ([id] INTEGER,[user_name] NVARCHAR(255)); UPDATE [users] SET [user_name]=$1 OUTPUT INSERTED.[id],INSERTED.[user_name] INTO @tmp WHERE [id] = $2; SELECT * FROM @tmp',
postgres: 'UPDATE "users" SET "user_name"=$1 WHERE "id" = $2 RETURNING *', postgres: 'UPDATE "users" SET "user_name"=$1 WHERE "id" = $2 RETURNING "id","user_name"',
default: 'UPDATE `users` SET `user_name`=$1 WHERE `id` = $2' default: 'UPDATE `users` SET `user_name`=$1 WHERE `id` = $2'
}, },
bind: { bind: {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!