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

Commit 8e516f1a by Jan Aagaard Meier

Clean up test and added docs for rewamped raw query handling

1 parent fd1ace03
...@@ -8,7 +8,12 @@ ...@@ -8,7 +8,12 @@
- [FEATURE] Validations are now enabled by default for upsert. - [FEATURE] Validations are now enabled by default for upsert.
- [FEATURE] Preliminary support for `include.through.where` - [FEATURE] Preliminary support for `include.through.where`
- [SECURITY/BUG] Fixed injection issue in direction param for order - [SECURITY/BUG] Fixed injection issue in direction param for order
- [FEATURE/BUG] Raw queries always return all results (including affected rows etc). - [FEATURE/BUG] Raw queries always return all results (including affected rows etc). This means you should change all promise listeners on `sequelize.query` to use `.spread` instead of `.then`, unless you are passing a query type.
#### Backwards compatability changes
- The default query type for `sequelize.query` is now `RAW` - this means that two arguments (results and metadata) will be returned by default and you should use `.spread`
- The 4th argument to `sequelize.query` has been deprecated in favor of `options.replacements`
>>>>>>> Clean up test and added docs for rewamped raw query handling
# 2.0.0-rc7 # 2.0.0-rc7
- [FEATURE] Throw an error if no where clause is given to `Model.destroy()`. - [FEATURE] Throw an error if no where clause is given to `Model.destroy()`.
......
As there are often use cases in which it is just easier to execute raw / already prepared SQL queries, you can utilize the function `sequelize.query`.
By default the function will return two arguments - a results array, and an object containing metadata (affected rows etc.). Note that since this is a raw query, the metadata (property names etc.) is dialect specific.
```js
sequelize.query("UPDATE users SET y = 42 WHERE x = 12").spread(function(results, metadata) {
// Results will be an empty array and metadata will contain the number of affected rows.
})
```
In cases where you don't need to access the metadata you can pass in a query type to tell sequelize how to format the results. For example, for a simple select query you could do:
```js
sequelize.query("SELECT * FROM `users`", { type: sequelize.QueryTypes.SELECT})
.then(function(users) {
// We don't need spread here, since only the results will be returned for select queries
})
```
Several other query types are available. [Peek into the source for details](https://github.com/sequelize/sequelize/blob/master/lib/query-types.js)
A second, optional, argument is the _callee_, which is a model. If you pass a model the returned data will be instances of that model.
```js
// Callee is the model definition. This allows you to easily map a query to a predefined model
sequelize.query('SELECT * FROM projects', Projects).then(function(projects){
// Each record will now be a instance of Project
})
```
# Replacements
Replacements in a query can be done in two different ways, either using named parameters (starting with `:`), or unnamed, represented by a `?`. Replacements can be passed in the options object.
* If an array is passed, `?` will be replaced in the order that they appear in the array
* If an object is passed, `:key` will be replaced with the keys from that object. If the object contains keys not found in the query or vice verca, an exception will be thrown.
```js
sequelize.query('SELECT * FROM projects WHERE status = ?',
{ replacements: ['active'] }
).then(function(projects) {
console.log(projects)
})
sequelize.query('SELECT * FROM projects WHERE status = :status ',
{ replacements: { status: 'active' }}
).then(function(projects) {
console.log(projects)
})
```
...@@ -225,9 +225,9 @@ var sequelize = new Sequelize('database', 'username', 'password', { ...@@ -225,9 +225,9 @@ var sequelize = new Sequelize('database', 'username', 'password', {
## Executing raw SQL queries ## Executing raw SQL queries
As there are often use cases in which it is just easier to execute raw / already prepared SQL queries, you can utilize the function`sequelize.query`. As there are often use cases in which it is just easier to execute raw / already prepared SQL queries, you can utilize the function `sequelize.query`.
Here is how it works: Here is how it works:
```js ```js
// Arguments for raw queries // Arguments for raw queries
...@@ -324,4 +324,4 @@ sequelize.query('select 1 as `foo.bar.baz`').success(function(rows) { ...@@ -324,4 +324,4 @@ sequelize.query('select 1 as `foo.bar.baz`').success(function(rows) {
[0]: /docs/latest/usage#options [0]: /docs/latest/usage#options
\ No newline at end of file
...@@ -175,15 +175,12 @@ module.exports = (function() { ...@@ -175,15 +175,12 @@ module.exports = (function() {
} }
} }
return o; if (this.options.nest) {
}); o = Dot.transform(o);
}
if (this.options.nest) {
result = result.map(function(entry){
return Dot.transform(entry);
});
}
return o;
}, this);
// Queries with include // Queries with include
} else if (this.options.hasJoin === true) { } else if (this.options.hasJoin === true) {
results = groupJoinData(results, { results = groupJoinData(results, {
......
...@@ -83,7 +83,7 @@ module.exports = (function() { ...@@ -83,7 +83,7 @@ module.exports = (function() {
}); });
options = Utils._.extend({ options = Utils._.extend({
logging: this.sequelize.options.logging logging: this.sequelize.options.logging,
}, options || {}); }, options || {});
// Postgres requires a special SQL command for enums // Postgres requires a special SQL command for enums
......
...@@ -227,7 +227,11 @@ module.exports = (function() { ...@@ -227,7 +227,11 @@ module.exports = (function() {
*/ */
Sequelize.prototype.Promise = Sequelize.Promise = Promise; Sequelize.prototype.Promise = Sequelize.Promise = Promise;
Sequelize.QueryTypes = QueryTypes; /**
* Available query types for use with `sequelize.query`
* @property QueryTypes
*/
Sequelize.prototype.QueryTypes = Sequelize.QueryTypes = QueryTypes;
/** /**
* Exposes the validator.js object, so you can extend it with custom validation functions. The validator is exposed both on the instance, and on the constructor. * Exposes the validator.js object, so you can extend it with custom validation functions. The validator is exposed both on the instance, and on the constructor.
...@@ -624,17 +628,30 @@ module.exports = (function() { ...@@ -624,17 +628,30 @@ module.exports = (function() {
/** /**
* Execute a query on the DB, with the posibility to bypass all the sequelize goodness. * Execute a query on the DB, with the posibility to bypass all the sequelize goodness.
* *
* If you do not provide other arguments than the SQL, raw will be assumed to the true, and sequelize will not try to do any formatting to the results of the query. * By default, the function will return two arguments: an array of results, and a metadata object, containing number of affected rows etc. Use `.spread` to access the results.
*
* If you are running a type of query where you don't need the metadata, for example a `SELECT` query, you can pass in a query type to make sequelize format the results:
*
* ```js
* sequlize.query('SELECT...').spread(function (results, metadata) {
*
* });
*
* sequlize.query('SELECT...', { type: sequelize.QueryTypes.SELECT }).then(function (results) {
*
* })
* ```
* *
* @method query * @method query
* @param {String} sql * @param {String} sql
* @param {Instance} [callee] If callee is provided, the returned data will be put into the callee * @param {Instance|Model} [callee] If callee is provided, the returned data will be put into the callee
* @param {Object} [options={}] Query options. * @param {Object} [options={}] Query options.
* @param {Boolean} [options.raw] If true, sequelize will not try to format the results of the query, or build an instance of a model from the result * @param {Boolean} [options.raw] If true, sequelize will not try to format the results of the query, or build an instance of a model from the result
* @param {Transaction} [options.transaction] The transaction that the query should be executed under * @param {Transaction} [options.transaction=null] The transaction that the query should be executed under
* @param {String} [options.type='SELECT'] The type of query you are executing. The query type affects how results are formatted before they are passed back. If no type is provided sequelize will try to guess the right type based on the sql, and fall back to SELECT. The type is a string, but `Sequelize.QueryTypes` is provided is convenience shortcuts. Current options are SELECT, BULKUPDATE and BULKDELETE * @param {String} [options.type='RAW'] The type of query you are executing. The query type affects how results are formatted before they are passed back. The type is a string, but `Sequelize.QueryTypes` is provided as convenience shortcuts.
* @param {Boolean} [options.nest=false] If true, transforms objects with `.` separated property names into nested objects using [dottie.js](https://github.com/mickhansen/dottie.js). For example { 'user.username': 'john' } becomes { user: { username: 'john' }} * @param {Boolean} [options.nest=false] If true, transforms objects with `.` separated property names into nested objects using [dottie.js](https://github.com/mickhansen/dottie.js). For example { 'user.username': 'john' } becomes { user: { username: 'john' }}. When `nest` is true, the query type is assumed to be `'SELECT'`, unless otherwise specified
* @param {Object|Array} [replacements] Either an object of named parameter replacements in the format `:param` or an array of unnamed replacements to replace `?` in your SQL. * @param {Object|Array} [options.replacements] Either an object of named parameter replacements in the format `:param` or an array of unnamed replacements to replace `?` in your SQL.
*
* @return {Promise} * @return {Promise}
* *
* @see {Model#build} for more information about callee. * @see {Model#build} for more information about callee.
...@@ -643,30 +660,52 @@ module.exports = (function() { ...@@ -643,30 +660,52 @@ module.exports = (function() {
var self = this; var self = this;
sql = sql.trim(); sql = sql.trim();
if (arguments.length === 4) { if (arguments.length === 4) {
if (Array.isArray(replacements)) { deprecated('passing raw query replacements as the 4th argument to sequelize.query is deprecated. Please use options.replacements instead');
sql = Utils.format([sql].concat(replacements), this.options.dialect); options.replacements = replacements;
}
else {
sql = Utils.formatNamedParameters(sql, replacements, this.options.dialect);
}
} else if (arguments.length === 3) { } else if (arguments.length === 3) {
options = options; options = options;
} else if (arguments.length === 2) { } else if (arguments.length === 2) {
options = {}; if (callee instanceof Sequelize.Model) {
options = {};
} else {
options = callee;
callee = undefined;
}
} else { } else {
options = { raw: true }; options = { raw: true };
} }
if (!(callee instanceof Sequelize.Model)) {
// When callee is not set, assume raw query
options.raw = true;
}
if (options.replacements) {
if (Array.isArray(options.replacements)) {
sql = Utils.format([sql].concat(options.replacements), this.options.dialect);
}
else {
sql = Utils.formatNamedParameters(sql, options.replacements, this.options.dialect);
}
}
options = Utils._.extend(Utils._.clone(this.options.query), options); options = Utils._.extend(Utils._.clone(this.options.query), options);
options = Utils._.defaults(options, { options = Utils._.defaults(options, {
logging: this.options.hasOwnProperty('logging') ? this.options.logging : console.log, logging: this.options.hasOwnProperty('logging') ? this.options.logging : console.log
type: QueryTypes.RAW
}); });
if (options.transaction === undefined && Sequelize.cls) { if (options.transaction === undefined && Sequelize.cls) {
options.transaction = Sequelize.cls.get('transaction'); options.transaction = Sequelize.cls.get('transaction');
} }
if (!options.type) {
if (options.nest) {
options.type = QueryTypes.SELECT;
} else {
options.type = QueryTypes.RAW;
}
}
if (options.transaction && options.transaction.finished) { if (options.transaction && options.transaction.finished) {
return Promise.reject(options.transaction.finished+' has been called on this transaction, you can no longer use it'); return Promise.reject(options.transaction.finished+' has been called on this transaction, you can no longer use it');
} }
......
...@@ -19,6 +19,7 @@ pages: ...@@ -19,6 +19,7 @@ pages:
- ['docs/hooks.md', 'Documentation', 'Hooks'] - ['docs/hooks.md', 'Documentation', 'Hooks']
- ['docs/transactions.md', 'Documentation', 'Transactions'] - ['docs/transactions.md', 'Documentation', 'Transactions']
- ['docs/legacy.md', 'Documentation', 'Working with legacy tables'] - ['docs/legacy.md', 'Documentation', 'Working with legacy tables']
- ['docs/raw-queries.md', 'Documentation', 'Raw queries']
- ['docs/migrations.md', 'Documentation', 'Migrations'] - ['docs/migrations.md', 'Documentation', 'Migrations']
- ['api/sequelize.md', 'API', 'Sequelize'] - ['api/sequelize.md', 'API', 'Sequelize']
...@@ -29,4 +30,4 @@ pages: ...@@ -29,4 +30,4 @@ pages:
- ['api/promise.md', 'API', 'Promise'] - ['api/promise.md', 'API', 'Promise']
- ['api/transaction.md', 'API', 'Transaction'] - ['api/transaction.md', 'API', 'Transaction']
- ['api/datatypes.md', 'API', 'Datatypes'] - ['api/datatypes.md', 'API', 'Datatypes']
- ['api/errors.md', 'API', 'Errors'] - ['api/errors.md', 'API', 'Errors']
\ No newline at end of file
...@@ -44,7 +44,7 @@ if (Support.dialectIsMySQL()) { ...@@ -44,7 +44,7 @@ if (Support.dialectIsMySQL()) {
// After 100 ms the DB connection will be disconnected for inactivity // After 100 ms the DB connection will be disconnected for inactivity
setTimeout(function() { setTimeout(function() {
// This query will be queued just after the `client.end` is executed and before its callback is called // This query will be queued just after the `client.end` is executed and before its callback is called
sequelize.query('SELECT COUNT(*) AS count FROM Users').on('success', function(count) { sequelize.query('SELECT COUNT(*) AS count FROM Users', { type: sequelize.QueryTypes.SELECT }).on('success', function(count) {
expect(count[0].count).to.equal(1); expect(count[0].count).to.equal(1);
done(); done();
}).error(function(error) { }).error(function(error) {
......
...@@ -1402,9 +1402,9 @@ describe(Support.getTestDialectTeaser('Model'), function() { ...@@ -1402,9 +1402,9 @@ describe(Support.getTestDialectTeaser('Model'), function() {
}).spread(function(result) { }).spread(function(result) {
expect(result[0].username).to.equal('Tony'); expect(result[0].username).to.equal('Tony');
return User.destroy({where: {username: ['Tony', 'Max']}, force: true}); return User.destroy({where: {username: ['Tony', 'Max']}, force: true});
}).spread(function() { }).then(function() {
return self.sequelize.query('SELECT * FROM paranoidusers', null, {raw: true}); return self.sequelize.query('SELECT * FROM paranoidusers', null, {raw: true});
}).then(function(users) { }).spread(function(users) {
expect(users).to.have.length(1); expect(users).to.have.length(1);
expect(users[0].username).to.equal('Tobi'); expect(users[0].username).to.equal('Tobi');
}); });
......
...@@ -386,68 +386,4 @@ describe(Support.getTestDialectTeaser('QueryInterface'), function() { ...@@ -386,68 +386,4 @@ describe(Support.getTestDialectTeaser('QueryInterface'), function() {
}); });
}); });
}); });
describe('describeForeignKeys', function() {
beforeEach(function() {
return this.queryInterface.createTable('users', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
}
}).bind(this).then(function() {
return this.queryInterface.createTable('hosts', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
admin: {
type: DataTypes.INTEGER,
references: 'users',
referenceKey: 'id'
},
operator: {
type: DataTypes.INTEGER,
references: 'users',
referenceKey: 'id',
onUpdate: 'cascade'
},
owner: {
type: DataTypes.INTEGER,
references: 'users',
referenceKey: 'id',
onUpdate: 'cascade',
onDelete: 'set null'
}
});
});
});
it('should get a list of foreign keys for the table', function(done) {
var sql =
this.queryInterface.QueryGenerator.getForeignKeysQuery('hosts', this.sequelize.config.database);
this.sequelize.query(sql).complete(function(err, fks) {
expect(err).to.be.null;
expect(fks).to.have.length(3);
var keys = Object.keys(fks[0]),
keys2 = Object.keys(fks[1]),
keys3 = Object.keys(fks[2]);
if (dialect === 'postgres' || dialect === 'postgres-native') {
expect(keys).to.have.length(6);
expect(keys2).to.have.length(7);
expect(keys3).to.have.length(7);
} else if (dialect === 'sqlite') {
expect(keys).to.have.length(8);
} else if (dialect === 'mysql' || dialect === 'mssql') {
expect(keys).to.have.length(1);
} else {
console.log("This test doesn't support " + dialect);
}
done();
});
});
});
}); });
...@@ -284,7 +284,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -284,7 +284,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
return self.sequelize.query(self.insertQuery); return self.sequelize.query(self.insertQuery);
}).then(function() { }).then(function() {
return self.sequelize.query('select username as ' + qq('user.username') + ' from ' + qq(self.User.tableName) + '', null, { raw: true, nest: true }); return self.sequelize.query('select username as ' + qq('user.username') + ' from ' + qq(self.User.tableName) + '', null, { raw: true, nest: true });
}).spread(function(users) { }).then(function(users) {
expect(users.map(function(u) { return u.user; })).to.deep.equal([{'username': 'john'}]); expect(users.map(function(u) { return u.user; })).to.deep.equal([{'username': 'john'}]);
}); });
}); });
...@@ -309,13 +309,11 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -309,13 +309,11 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
console.log('FIXME: I want to be supported in this dialect as well :-('); console.log('FIXME: I want to be supported in this dialect as well :-(');
} }
it('uses the passed DAOFactory', function(done) { it('uses the passed model', function() {
var self = this; return this.sequelize.query(this.insertQuery).bind(this).then(function() {
self.sequelize.query(this.insertQuery).success(function() { return this.sequelize.query('SELECT * FROM ' + qq(this.User.tableName) + ';', this.User, { type: this.sequelize.QueryTypes.SELECT });
self.sequelize.query('SELECT * FROM ' + qq(self.User.tableName) + ';', self.User).success(function(users) { }).then(function(users) {
expect(users[0].Model).to.equal(self.User); expect(users[0].Model).to.equal(this.User);
done();
});
}); });
}); });
...@@ -337,7 +335,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -337,7 +335,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
}); });
it('replaces token with the passed array', function(done) { it('replaces token with the passed array', function(done) {
this.sequelize.query('select ? as foo, ? as bar', null, { raw: true }, [1, 2]).success(function(result) { this.sequelize.query('select ? as foo, ? as bar', null, { type: this.sequelize.QueryTypes.SELECT }, [1, 2]).success(function(result) {
expect(result).to.deep.equal([{ foo: 1, bar: 2 }]); expect(result).to.deep.equal([{ foo: 1, bar: 2 }]);
done(); done();
}); });
...@@ -398,7 +396,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -398,7 +396,7 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
}).to.throw(Error, /Named parameter ":\w+" has no value in the given object\./g); }).to.throw(Error, /Named parameter ":\w+" has no value in the given object\./g);
}); });
it('handles AS in conjunction with functions just fine', function(done) { it('handles AS in conjunction with functions just fine', function() {
var datetime = (dialect === 'sqlite' ? 'date(\'now\')' : 'NOW()'); var datetime = (dialect === 'sqlite' ? 'date(\'now\')' : 'NOW()');
if (dialect === 'mssql') { if (dialect === 'mssql') {
datetime = 'GETDATE()'; datetime = 'GETDATE()';
...@@ -435,10 +433,10 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -435,10 +433,10 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
this.t = t; this.t = t;
return this.sequelize.set({ foo: 'bar' }, { transaction: t }); return this.sequelize.set({ foo: 'bar' }, { transaction: t });
}).then(function() { }).then(function() {
return this.sequelize.query('SELECT @foo as `foo`', null, { raw: true, plain: true, transaction: this.t }); return this.sequelize.query('SELECT @foo as `foo`', { transaction: this.t, type: this.sequelize.QueryTypes.SELECT });
}).then(function(data) { }).then(function(data) {
expect(data).to.be.ok; expect(data[0]).to.be.ok;
expect(data.foo).to.be.equal('bar'); expect(data[0].foo).to.be.equal('bar');
return this.t.commit(); return this.t.commit();
}); });
}); });
...@@ -451,11 +449,11 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -451,11 +449,11 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
foos: 'bars' foos: 'bars'
}, { transaction: t }); }, { transaction: t });
}).then(function() { }).then(function() {
return this.sequelize.query('SELECT @foo as `foo`, @foos as `foos`', null, { raw: true, plain: true, transaction: this.t }); return this.sequelize.query('SELECT @foo as `foo`, @foos as `foos`', { transaction: this.t, type: this.sequelize.QueryTypes.SELECT });
}).then(function(data) { }).then(function(data) {
expect(data).to.be.ok; expect(data[0]).to.be.ok;
expect(data.foo).to.be.equal('bar'); expect(data[0].foo).to.be.equal('bar');
expect(data.foos).to.be.equal('bars'); expect(data[0].foos).to.be.equal('bars');
return this.t.commit(); return this.t.commit();
}); });
}); });
...@@ -884,8 +882,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -884,8 +882,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
self self
.sequelizeWithTransaction .sequelizeWithTransaction
.query(sql, null, { plain: true, raw: true, transaction: transaction }) .query(sql, null, { plain: true, raw: true, transaction: transaction, type: self.sequelize.QueryTypes.SELECT })
.spread(function(result) { callback(result.cnt); }); .then(function(result) { callback(result.cnt); });
}; };
TransactionTest.sync({ force: true }).success(function() { TransactionTest.sync({ force: true }).success(function() {
...@@ -919,8 +917,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() { ...@@ -919,8 +917,8 @@ describe(Support.getTestDialectTeaser('Sequelize'), function() {
self self
.sequelizeWithTransaction .sequelizeWithTransaction
.query(sql, null, { plain: true, raw: true, transaction: transaction }) .query(sql, { transaction: transaction, type: self.sequelize.QueryTypes.SELECT })
.success(function(result) { callback(parseInt(result.cnt, 10)); }); .success(function(result) { callback(parseInt(result[0].cnt, 10)); });
}; };
TransactionTest.sync({ force: true }).success(function() { TransactionTest.sync({ force: true }).success(function() {
......
...@@ -28,8 +28,8 @@ if (dialect !== 'sqlite') { ...@@ -28,8 +28,8 @@ if (dialect !== 'sqlite') {
var query = 'SELECT ' + now + ' as now'; var query = 'SELECT ' + now + ' as now';
return Promise.all([ return Promise.all([
this.sequelize.query(query), this.sequelize.query(query, { type: this.sequelize.QueryTypes.SELECT }),
this.sequelizeWithTimezone.query(query) this.sequelizeWithTimezone.query(query, { type: this.sequelize.QueryTypes.SELECT })
]).spread(function(now1, now2) { ]).spread(function(now1, now2) {
var elapsedQueryTime = (Date.now() - startQueryTime) + 20; var elapsedQueryTime = (Date.now() - startQueryTime) + 20;
expect(now1[0].now.getTime()).to.be.closeTo(now2[0].now.getTime(), elapsedQueryTime); expect(now1[0].now.getTime()).to.be.closeTo(now2[0].now.getTime(), elapsedQueryTime);
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!