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

Commit bce3d9aa by Gareth Oakley Committed by Sushant

feat(query-generator): Generate INSERTs using bind parameters (#9431)

1 parent b05abdd4
......@@ -33,6 +33,12 @@ ABSTRACT.prototype.stringify = function stringify(value, options) {
}
return value;
};
ABSTRACT.prototype.bindParam = function bindParam(value, options) {
if (this._bindParam) {
return this._bindParam(value, options);
}
return options.bindParam(this.stringify(value, options));
};
function STRING(length, binary) {
const options = typeof length === 'object' && length || {length, binary};
......@@ -274,16 +280,22 @@ DECIMAL.prototype.validate = function validate(value) {
for (const floating of [FLOAT, DOUBLE, REAL]) {
floating.prototype.escape = false;
floating.prototype._stringify = function _stringify(value) {
floating.prototype._value = function _value(value) {
if (isNaN(value)) {
return "'NaN'";
return 'NaN';
} else if (!isFinite(value)) {
const sign = value < 0 ? '-' : '';
return "'" + sign + "Infinity'";
return sign + 'Infinity';
}
return value;
};
floating.prototype._stringify = function _stringify(value) {
return "'" + this._value(value) + "'";
};
floating.prototype._bindParam = function _bindParam(value, options) {
return options.bindParam(this._value(value));
};
}
function BOOLEAN() {
......@@ -529,6 +541,17 @@ BLOB.prototype._hexify = function _hexify(hex) {
return "X'" + hex + "'";
};
BLOB.prototype._bindParam = function _bindParam(value, options) {
if (!Buffer.isBuffer(value)) {
if (Array.isArray(value)) {
value = Buffer.from(value);
} else {
value = Buffer.from(value.toString());
}
}
return options.bindParam(value);
};
function RANGE(subtype) {
const options = _.isPlainObject(subtype) ? subtype : {subtype};
......@@ -703,6 +726,9 @@ GEOMETRY.prototype.escape = false;
GEOMETRY.prototype._stringify = function _stringify(value, options) {
return 'GeomFromText(' + options.escape(Wkt.convert(value)) + ')';
};
GEOMETRY.prototype._bindParam = function _bindParam(value, options) {
return 'GeomFromText(' + options.bindParam(Wkt.convert(value)) + ')';
};
function GEOGRAPHY(type, srid) {
const options = _.isPlainObject(type) ? type : {type, srid};
......@@ -721,6 +747,9 @@ GEOGRAPHY.prototype.escape = false;
GEOGRAPHY.prototype._stringify = function _stringify(value, options) {
return 'GeomFromText(' + options.escape(Wkt.convert(value)) + ')';
};
GEOGRAPHY.prototype._bindParam = function _bindParam(value, options) {
return 'GeomFromText(' + options.bindParam(Wkt.convert(value)) + ')';
};
for (const helper of Object.keys(helpers)) {
for (const DataType of helpers[helper]) {
......
......@@ -106,6 +106,8 @@ class QueryGenerator {
const modelAttributeMap = {};
const fields = [];
const values = [];
const bind = [];
const bindParam = this.bindParam(bind);
let query;
let valueQuery = '<%= tmpTable %>INSERT<%= ignoreDuplicates %> INTO <%= table %> (<%= attributes %>)<%= output %> VALUES (<%= values %>)';
let emptyQuery = '<%= tmpTable %>INSERT<%= ignoreDuplicates %> INTO <%= table %><%= output %>';
......@@ -169,7 +171,14 @@ class QueryGenerator {
}
}
if (_.get(this, ['sequelize', 'options', 'dialectOptions', 'prependSearchPath']) || options.searchPath) {
// Not currently supported with search path (requires output of multiple queries)
options.bindParam = false;
}
if (this._dialect.supports.EXCEPTION && options.exception) {
// Not currently supported with bind parameters (requires output of multiple queries)
options.bindParam = false;
// 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 (semver.gte(this.sequelize.options.databaseVersion, '9.2.0')) {
......@@ -211,7 +220,11 @@ class QueryGenerator {
identityWrapperRequired = true;
}
values.push(this.escape(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'INSERT' }));
if (value instanceof Utils.SequelizeMethod || options.bindParam === false) {
values.push(this.escape(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'INSERT' }));
} else {
values.push(this.format(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'INSERT' }, bindParam));
}
}
}
}
......@@ -234,7 +247,13 @@ class QueryGenerator {
].join(' ');
}
return _.template(query, this._templateSettings)(replacements);
query = _.template(query, this._templateSettings)(replacements);
// Used by Postgres upsertQuery and calls to here with options.exception set to true
const result = { query };
if (options.bindParam !== false) {
result.bind = bind;
}
return result;
}
/**
......@@ -967,15 +986,7 @@ class QueryGenerator {
return this.handleSequelizeMethod(value);
} else {
if (field && field.type) {
if (this.typeValidation && field.type.validate && value) {
if (options.isList && Array.isArray(value)) {
for (const item of value) {
field.type.validate(item, options);
}
} else {
field.type.validate(value, options);
}
}
this.validate(value, field, options);
if (field.type.stringify) {
// Users shouldn't have to worry about these args - just give them a function that takes a single arg
......@@ -995,6 +1006,53 @@ class QueryGenerator {
return SqlString.escape(value, this.options.timezone, this.dialect);
}
bindParam(bind) {
return value => {
bind.push(value);
return '$' + bind.length;
};
}
/*
Returns a bind parameter representation of a value (e.g. a string, number or date)
@private
*/
format(value, field, options, bindParam) {
options = options || {};
if (value !== null && value !== undefined) {
if (value instanceof Utils.SequelizeMethod) {
throw new Error('Cannot pass SequelizeMethod as a bind parameter - use escape instead');
} else {
if (field && field.type) {
this.validate(value, field, options);
if (field.type.bindParam) {
return field.type.bindParam(value, { escape: _.identity, field, timezone: this.options.timezone, operation: options.operation, bindParam });
}
}
}
}
return bindParam(value);
}
/*
Validate a value against a field specification
@private
*/
validate(value, field, options) {
if (this.typeValidation && field.type.validate && value) {
if (options.isList && Array.isArray(value)) {
for (const item of value) {
field.type.validate(item, options);
}
} else {
field.type.validate(value, options);
}
}
}
isIdentifierQuoted(identifier) {
return QuoteHelper.isIdentifierQuoted(identifier);
}
......
......@@ -41,6 +41,9 @@ class Query extends AbstractQuery {
paramType.typeOptions = {precision: 30, scale: 15};
}
}
if (Buffer.isBuffer(value)) {
paramType.type = TYPES.VarBinary;
}
return paramType;
}
......
......@@ -433,6 +433,9 @@ module.exports = BaseTypes => {
GEOMETRY.prototype._stringify = function _stringify(value, options) {
return 'ST_GeomFromGeoJSON(' + options.escape(JSON.stringify(value)) + ')';
};
GEOMETRY.prototype._bindParam = function _bindParam(value, options) {
return 'ST_GeomFromGeoJSON(' + options.bindParam(value) + ')';
};
function GEOGRAPHY(type, srid) {
if (!(this instanceof GEOGRAPHY)) return new GEOGRAPHY(type, srid);
......@@ -469,6 +472,9 @@ module.exports = BaseTypes => {
GEOGRAPHY.prototype._stringify = function _stringify(value, options) {
return 'ST_GeomFromGeoJSON(' + options.escape(JSON.stringify(value)) + ')';
};
GEOGRAPHY.prototype.bindParam = function bindParam(value, options) {
return 'ST_GeomFromGeoJSON(' + options.bindParam(value) + ')';
};
let hstore;
function HSTORE() {
......@@ -491,12 +497,18 @@ module.exports = BaseTypes => {
};
HSTORE.prototype.escape = false;
HSTORE.prototype._stringify = function _stringify(value) {
HSTORE.prototype._value = function _value(value) {
if (!hstore) {
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
hstore = require('./hstore');
}
return "'" + hstore.stringify(value) + "'";
return hstore.stringify(value);
};
HSTORE.prototype._stringify = function _stringify(value) {
return "'" + this._value(value) + "'";
};
HSTORE.prototype._bindParam = function _bindParam(value, options) {
return options.bindParam(this._value(value));
};
BaseTypes.HSTORE.types.postgres = {
......@@ -533,10 +545,9 @@ module.exports = BaseTypes => {
};
RANGE.prototype.escape = false;
RANGE.prototype._stringify = function _stringify(values, options) {
RANGE.prototype._value = function _value(values, options) {
if (!Array.isArray(values)) {
return "'" + this.options.subtype.stringify(values, options) + "'::" +
this.toCastType();
return this.options.subtype.stringify(values, options);
}
const valueInclusivity = [true, false];
......@@ -560,7 +571,21 @@ module.exports = BaseTypes => {
// Array.map does not preserve extra array properties
valuesStringified.inclusive = valueInclusivity;
return '\'' + range.stringify(valuesStringified) + '\'';
return range.stringify(valuesStringified);
};
RANGE.prototype._stringify = function _stringify(values, options) {
const value = this._value(values, options);
if (!Array.isArray(values)) {
return `'${value}'::` + this.toCastType();
}
return `'${value}'`;
};
RANGE.prototype._bindParam = function _bindParam(values, options) {
const value = this._value(values, options);
if (!Array.isArray(values)) {
return options.bindParam(value) + '::' + this.toCastType();
}
return options.bindParam(value);
};
BaseTypes.RANGE.types.postgres = {
......@@ -569,8 +594,11 @@ module.exports = BaseTypes => {
};
BaseTypes.ARRAY.prototype.escape = false;
BaseTypes.ARRAY.prototype._stringify = function _stringify(values, options) {
let str = 'ARRAY[' + values.map(value => {
BaseTypes.ARRAY.prototype._value = function _value(values, options) {
return values.map(value => {
if (options && options.bindParam && this.type && this.type._value) {
return this.type._value(value, options);
}
if (this.type && this.type.stringify) {
value = this.type.stringify(value, options);
......@@ -579,7 +607,10 @@ module.exports = BaseTypes => {
}
}
return options.escape(value);
}, this).join(',') + ']';
}, this);
};
BaseTypes.ARRAY.prototype._stringify = function _stringify(values, options) {
let str = 'ARRAY[' + this._value(values, options).join(',') + ']';
if (this.type) {
const Utils = require('../../utils');
......@@ -597,6 +628,9 @@ module.exports = BaseTypes => {
return str;
};
BaseTypes.ARRAY.prototype._bindParam = function _bindParam(values, options) {
return options.bindParam(this._value(values, options));
};
function ENUM(options) {
if (!(this instanceof ENUM)) return new ENUM(options);
......
......@@ -344,17 +344,18 @@ class PostgresQueryGenerator extends AbstractQueryGenerator {
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
const primaryField = this.quoteIdentifier(model.primaryKeyField);
let insert = this.insertQuery(tableName, insertValues, model.rawAttributes, options);
const insertOptions = _.defaults({ bindParam: false }, options);
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, insertOptions);
let update = this.updateQuery(tableName, updateValues, where, options, model.rawAttributes);
insert = insert.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
insert.query = insert.query.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
update = update.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
return this.exceptionFn(
'sequelize_upsert',
tableName,
'OUT created boolean, OUT primary_key text',
`${insert} created := true;`,
`${insert.query} created := true;`,
`${update}; created := false`
);
}
......@@ -812,7 +813,7 @@ class PostgresQueryGenerator extends AbstractQueryGenerator {
dataType = dataType.replace(/BIGINT/, '');
} else if (_.includes(dataType, 'SMALLINT')) {
dataType = dataType.replace(/SERIAL/, 'SMALLSERIAL');
dataType = dataType.replace(/SMALLINT/, '');
dataType = dataType.replace(/SMALLINT/, '');
} else {
dataType = dataType.replace(/INTEGER/, '');
}
......
......@@ -200,9 +200,13 @@ class SQLiteQueryGenerator extends MySqlQueryGenerator {
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
options.ignoreDuplicates = true;
const sql = this.insertQuery(tableName, insertValues, model.rawAttributes, options) + ' ' + this.updateQuery(tableName, updateValues, where, options, model.rawAttributes);
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, options);
const update = this.updateQuery(tableName, updateValues, where, options, model.rawAttributes);
return sql;
const query = insert.query + ' ' + update;
const bind = insert.bind;
return { query, bind };
}
updateQuery(tableName, attrValueHash, where, options, attributes) {
......@@ -480,4 +484,4 @@ class SQLiteQueryGenerator extends MySqlQueryGenerator {
}
};
module.exports = SQLiteQueryGenerator;
\ No newline at end of file
module.exports = SQLiteQueryGenerator;
......@@ -57,7 +57,7 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
});
});
const testSuccess = function(Type, value) {
const testSuccess = function(Type, value, options) {
const parse = Type.constructor.parse = sinon.spy(value => {
return value;
});
......@@ -65,6 +65,12 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
const stringify = Type.constructor.prototype.stringify = sinon.spy(function() {
return Sequelize.ABSTRACT.prototype.stringify.apply(this, arguments);
});
let bindParam;
if (options && options.useBindParam) {
bindParam = Type.constructor.prototype.bindParam = sinon.spy(function() {
return Sequelize.ABSTRACT.prototype.bindParam.apply(this, arguments);
});
}
const User = current.define('user', {
field: Type
......@@ -83,10 +89,17 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
return User.findAll().get(0);
}).then(() => {
expect(parse).to.have.been.called;
expect(stringify).to.have.been.called;
if (options && options.useBindParam) {
expect(bindParam).to.have.been.called;
} else {
expect(stringify).to.have.been.called;
}
delete Type.constructor.parse;
delete Type.constructor.prototype.stringify;
if (options && options.useBindParam) {
delete Type.constructor.prototype.bindParam;
}
});
};
......@@ -117,18 +130,18 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
}
if (current.dialect.supports.HSTORE) {
it('calls parse and stringify for HSTORE', () => {
it('calls parse and bindParam for HSTORE', () => {
const Type = new Sequelize.HSTORE();
return testSuccess(Type, { test: 42, nested: false });
return testSuccess(Type, { test: 42, nested: false }, { useBindParam: true });
});
}
if (current.dialect.supports.RANGE) {
it('calls parse and stringify for RANGE', () => {
it('calls parse and bindParam for RANGE', () => {
const Type = new Sequelize.RANGE(new Sequelize.INTEGER());
return testSuccess(Type, [1, 2]);
return testSuccess(Type, [1, 2], { useBindParam: true });
});
}
......@@ -147,13 +160,13 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
it('calls parse and stringify for TIME', () => {
const Type = new Sequelize.TIME();
return testSuccess(Type, new Date());
return testSuccess(Type, moment(new Date()).format('HH:mm:ss'));
});
it('calls parse and stringify for BLOB', () => {
const Type = new Sequelize.BLOB();
return testSuccess(Type, 'foobar');
return testSuccess(Type, 'foobar', { useBindParam: true });
});
it('calls parse and stringify for CHAR', () => {
......@@ -208,27 +221,27 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
}
});
it('calls parse and stringify for DOUBLE', () => {
it('calls parse and bindParam for DOUBLE', () => {
const Type = new Sequelize.DOUBLE();
return testSuccess(Type, 1.5);
return testSuccess(Type, 1.5, { useBindParam: true });
});
it('calls parse and stringify for FLOAT', () => {
it('calls parse and bindParam for FLOAT', () => {
const Type = new Sequelize.FLOAT();
if (dialect === 'postgres') {
// Postgres doesn't have float, maps to either decimal or double
testFailure(Type);
} else {
return testSuccess(Type, 1.5);
return testSuccess(Type, 1.5, { useBindParam: true });
}
});
it('calls parse and stringify for REAL', () => {
it('calls parse and bindParam for REAL', () => {
const Type = new Sequelize.REAL();
return testSuccess(Type, 1.5);
return testSuccess(Type, 1.5, { useBindParam: true });
});
it('calls parse and stringify for UUID', () => {
......@@ -254,10 +267,10 @@ describe(Support.getTestDialectTeaser('DataTypes'), () => {
});
if (current.dialect.supports.GEOMETRY) {
it('calls parse and stringify for GEOMETRY', () => {
it('calls parse and bindParam for GEOMETRY', () => {
const Type = new Sequelize.GEOMETRY();
return testSuccess(Type, { type: 'Point', coordinates: [125.6, 10.1] });
return testSuccess(Type, { type: 'Point', coordinates: [125.6, 10.1] }, { useBindParam: true });
});
it('should parse an empty GEOMETRY field', () => {
......
......@@ -190,15 +190,15 @@ if (dialect.match(/^postgres/)) {
});
});
it('should stringify hstore with insert', function() {
it('should NOT stringify hstore with insert', function() {
return this.User.create({
username: 'bob',
email: ['myemail@email.com'],
settings: { mailing: false, push: 'facebook', frequency: 3 }
}, {
logging(sql) {
const expected = '\'"mailing"=>"false","push"=>"facebook","frequency"=>"3"\',\'"default"=>"\'\'value\'\'"\'';
expect(sql.indexOf(expected)).not.to.equal(-1);
const unexpected = '\'"mailing"=>"false","push"=>"facebook","frequency"=>"3"\',\'"default"=>"\'\'value\'\'"\'';
expect(sql).not.to.include(unexpected);
}
});
});
......@@ -597,12 +597,13 @@ if (dialect.match(/^postgres/)) {
return this.User.sync({ force: true });
});
it('should use postgres "TIMESTAMP WITH TIME ZONE" instead of "DATETIME"', function() {
it('should use bind params instead of "TIMESTAMP WITH TIME ZONE"', function() {
return this.User.create({
dates: []
}, {
logging(sql) {
expect(sql.indexOf('TIMESTAMP WITH TIME ZONE')).to.be.greaterThan(0);
expect(sql).not.to.contain('TIMESTAMP WITH TIME ZONE');
expect(sql).not.to.contain('DATETIME');
}
});
});
......@@ -863,7 +864,7 @@ if (dialect.match(/^postgres/)) {
expect(newUser.course_period[0].value).to.equalTime(period2[0]); // lower bound
expect(newUser.course_period[1].value).to.equalTime(period2[1]); // upper bound
expect(newUser.course_period[0].inclusive).to.deep.equal(true); // inclusive
expect(newUser.course_period[1].inclusive).to.deep.equal(false); // exclusive
expect(newUser.course_period[1].inclusive).to.deep.equal(false); // exclusive
});
});
});
......@@ -887,7 +888,7 @@ if (dialect.match(/^postgres/)) {
expect(users[0].course_period[0].value).to.equalTime(period[0]); // lower bound
expect(users[0].course_period[1].value).to.equalTime(period[1]); // upper bound
expect(users[0].course_period[0].inclusive).to.deep.equal(true); // inclusive
expect(users[0].course_period[1].inclusive).to.deep.equal(false); // exclusive
expect(users[0].course_period[1].inclusive).to.deep.equal(false); // exclusive
});
});
});
......
......@@ -3,6 +3,7 @@
const chai = require('chai'),
expect = chai.expect,
Support = require('./support'),
dialect = Support.getTestDialect(),
Sequelize = Support.Sequelize,
current = Support.sequelize,
DataTypes = Sequelize.DataTypes;
......@@ -26,19 +27,17 @@ describe('model', () => {
});
});
it('should stringify json with insert', function() {
it('should use a placeholder for json with insert', function() {
return this.User.create({
username: 'bob',
emergency_contact: { name: 'joe', phones: [1337, 42] }
}, {
fields: ['id', 'username', 'document', 'emergency_contact'],
logging: sql => {
const expected = '\'{"name":"joe","phones":[1337,42]}\'';
const expectedEscaped = '\'{\\"name\\":\\"joe\\",\\"phones\\":[1337,42]}\'';
if (sql.indexOf(expected) === -1) {
expect(sql.indexOf(expectedEscaped)).not.to.equal(-1);
if (dialect.match(/^mysql/)) {
expect(sql).to.include('?');
} else {
expect(sql.indexOf(expected)).not.to.equal(-1);
expect(sql).to.include('$1');
}
}
});
......
......@@ -940,7 +940,7 @@ describe(Support.getTestDialectTeaser('Model'), () => {
}
});
it('casts empty arrays correctly for postgresql insert', function() {
it('does not cast arrays for postgresql insert', function() {
if (dialect !== 'postgres') {
expect('').to.equal('');
return void 0;
......@@ -956,8 +956,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return User.create({myvals: [], mystr: []}, {
logging(sql) {
test = true;
expect(sql.indexOf('ARRAY[]::INTEGER[]')).to.be.above(-1);
expect(sql.indexOf('ARRAY[]::VARCHAR(255)[]')).to.be.above(-1);
expect(sql).not.to.contain('ARRAY[]::INTEGER[]');
expect(sql).not.to.contain('ARRAY[]::VARCHAR(255)[]');
}
});
}).then(() => {
......@@ -984,8 +984,8 @@ describe(Support.getTestDialectTeaser('Model'), () => {
return user.save({
logging(sql) {
test = true;
expect(sql.indexOf('ARRAY[]::INTEGER[]')).to.be.above(-1);
expect(sql.indexOf('ARRAY[]::VARCHAR(255)[]')).to.be.above(-1);
expect(sql).to.contain('ARRAY[]::INTEGER[]');
expect(sql).to.contain('ARRAY[]::VARCHAR(255)[]');
}
});
});
......@@ -1139,7 +1139,7 @@ describe(Support.getTestDialectTeaser('Model'), () => {
logging(sql) {
expect(sql).to.exist;
test = true;
expect(sql.toUpperCase().indexOf('INSERT')).to.be.above(-1);
expect(sql.toUpperCase()).to.contain('INSERT');
}
});
}).then(() => {
......
......@@ -213,7 +213,8 @@ const Support = {
return url;
},
expectsql(query, expectations) {
expectsql(query, assertions) {
const expectations = assertions.query || assertions;
let expectation = expectations[Support.sequelize.dialect.name];
if (!expectation) {
......@@ -229,7 +230,12 @@ const Support = {
if (_.isError(query)) {
expect(query.message).to.equal(expectation.message);
} else {
expect(query).to.equal(expectation);
expect(query.query || query).to.equal(expectation);
}
if (assertions.bind) {
const bind = assertions.bind[Support.sequelize.dialect.name] || assertions.bind['default'] || assertions.bind;
expect(query.bind).to.deep.equal(bind);
}
}
};
......
......@@ -93,5 +93,13 @@ describe('QueryGenerator', () => {
.should.be.equal('foo LIKE bar');
});
});
describe('format', () => {
it('should throw an error if passed SequelizeMethod', function() {
const QG = getAbstractQueryGenerator(this.sequelize);
const value = this.sequelize.fn('UPPER', 'test');
expect(() => QG.format(value)).to.throw(Error);
});
});
});
......@@ -445,47 +445,83 @@ if (dialect === 'mysql') {
insertQuery: [
{
arguments: ['myTable', {name: 'foo'}],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo');"
expectation: {
query: 'INSERT INTO `myTable` (`name`) VALUES ($1);',
bind: ['foo']
}
}, {
arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo\\';DROP TABLE myTable;');"
expectation: {
query: 'INSERT INTO `myTable` (`name`) VALUES ($1);',
bind: ["foo';DROP TABLE myTable;"]
}
}, {
arguments: ['myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}],
expectation: "INSERT INTO `myTable` (`name`,`birthday`) VALUES ('foo','2011-03-27 10:01:55');"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`birthday`) VALUES ($1,$2);',
bind: ['foo', new Date(Date.UTC(2011, 2, 27, 10, 1, 55))]
}
}, {
arguments: ['myTable', {name: 'foo', foo: 1}],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`) VALUES ($1,$2);',
bind: ['foo', 1]
}
}, {
arguments: ['myTable', {data: new Buffer('Sequelize') }],
expectation: "INSERT INTO `myTable` (`data`) VALUES (X'53657175656c697a65');"
expectation: {
query: 'INSERT INTO `myTable` (`data`) VALUES ($1);',
bind: [new Buffer('Sequelize')]
}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: null}],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ($1,$2,$3);',
bind: ['foo', 1, null]
}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: null}],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL);",
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ($1,$2,$3);',
bind: ['foo', 1, null]
},
context: {options: {omitNull: false}}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: null}],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);",
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`) VALUES ($1,$2);',
bind: ['foo', 1]
},
context: {options: {omitNull: true}}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: undefined}],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);",
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`) VALUES ($1,$2);',
bind: ['foo', 1]
},
context: {options: {omitNull: true}}
}, {
arguments: ['myTable', {foo: false}],
expectation: 'INSERT INTO `myTable` (`foo`) VALUES (false);'
expectation: {
query: 'INSERT INTO `myTable` (`foo`) VALUES ($1);',
bind: [false]
}
}, {
arguments: ['myTable', {foo: true}],
expectation: 'INSERT INTO `myTable` (`foo`) VALUES (true);'
expectation: {
query: 'INSERT INTO `myTable` (`foo`) VALUES ($1);',
bind: [true]
}
}, {
arguments: ['myTable', function(sequelize) {
return {
foo: sequelize.fn('NOW')
};
}],
expectation: 'INSERT INTO `myTable` (`foo`) VALUES (NOW());',
expectation: {
query: 'INSERT INTO `myTable` (`foo`) VALUES (NOW());',
bind: []
},
needsSequelize: true
}
],
......@@ -616,7 +652,8 @@ if (dialect === 'mysql') {
});
tests.forEach(test => {
const title = test.title || 'MySQL correctly returns ' + test.expectation + ' for ' + JSON.stringify(test.arguments);
const query = test.expectation.query || test.expectation;
const title = test.title || 'MySQL correctly returns ' + query + ' for ' + JSON.stringify(test.arguments);
it(title, function() {
if (test.needsSequelize) {
if (_.isFunction(test.arguments[1])) test.arguments[1] = test.arguments[1](this.sequelize);
......
'use strict';
const chai = require('chai'),
expect = chai.expect,
Support = require(__dirname + '/../../support'),
dialect = Support.getTestDialect(),
BaseTypes = require(__dirname + '/../../../../lib/data-types'),
DataTypes = require(__dirname + '/../../../../lib/dialects/postgres/data-types')(BaseTypes),
QueryGenerator = require('../../../../lib/dialects/postgres/query-generator');
if (dialect.match(/^postgres/)) {
describe('[POSTGRES Specific] DataTypes', () => {
beforeEach(function() {
this.queryGenerator = new QueryGenerator({
sequelize: this.sequelize,
_dialect: this.sequelize.dialect
});
});
describe('GEOMETRY', () => {
it('should use bindParam fn', function() {
const value = { type: 'Point' };
const bind = [];
const bindParam = this.queryGenerator.bindParam(bind);
const result = DataTypes.GEOMETRY.prototype.bindParam(value, { bindParam });
expect(result).to.equal('ST_GeomFromGeoJSON($1)');
expect(bind).to.eql([value]);
});
});
describe('GEOGRAPHY', () => {
it('should use bindParam fn', function() {
const value = { type: 'Point' };
const bind = [];
const bindParam = this.queryGenerator.bindParam(bind);
const result = DataTypes.GEOGRAPHY.prototype.bindParam(value, { bindParam });
expect(result).to.equal('ST_GeomFromGeoJSON($1)');
expect(bind).to.eql([value]);
});
});
});
}
......@@ -364,42 +364,78 @@ if (dialect === 'sqlite') {
insertQuery: [
{
arguments: ['myTable', { name: 'foo' }],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo');"
expectation: {
query: 'INSERT INTO `myTable` (`name`) VALUES ($1);',
bind: ['foo']
}
}, {
arguments: ['myTable', { name: "'bar'" }],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('''bar''');"
expectation: {
query: 'INSERT INTO `myTable` (`name`) VALUES ($1);',
bind: ["'bar'"]
}
}, {
arguments: ['myTable', {data: new Buffer('Sequelize') }],
expectation: "INSERT INTO `myTable` (`data`) VALUES (X'53657175656c697a65');"
expectation: {
query: 'INSERT INTO `myTable` (`data`) VALUES ($1);',
bind: [new Buffer('Sequelize')]
}
}, {
arguments: ['myTable', { name: 'bar', value: null }],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('bar',NULL);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`value`) VALUES ($1,$2);',
bind: ['bar', null]
}
}, {
arguments: ['myTable', { name: 'bar', value: undefined }],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('bar',NULL);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`value`) VALUES ($1,$2);',
bind: ['bar', undefined]
}
}, {
arguments: ['myTable', {name: 'foo', birthday: moment('2011-03-27 10:01:55 +0000', 'YYYY-MM-DD HH:mm:ss Z').toDate()}],
expectation: "INSERT INTO `myTable` (`name`,`birthday`) VALUES ('foo','2011-03-27 10:01:55.000 +00:00');"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`birthday`) VALUES ($1,$2);',
bind: ['foo', moment('2011-03-27 10:01:55 +0000', 'YYYY-MM-DD HH:mm:ss Z').toDate()]
}
}, {
arguments: ['myTable', { name: 'foo', value: true }],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',1);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`value`) VALUES ($1,$2);',
bind: ['foo', true]
}
}, {
arguments: ['myTable', { name: 'foo', value: false }],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',0);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`value`) VALUES ($1,$2);',
bind: ['foo', false]
}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: null}],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL);"
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ($1,$2,$3);',
bind: ['foo', 1, null]
}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: null}],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL);",
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ($1,$2,$3);',
bind: ['foo', 1, null]
},
context: {options: {omitNull: false}}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: null}],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);",
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`) VALUES ($1,$2);',
bind: ['foo', 1]
},
context: {options: {omitNull: true}}
}, {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: undefined}],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);",
expectation: {
query: 'INSERT INTO `myTable` (`name`,`foo`) VALUES ($1,$2);',
bind: ['foo', 1]
},
context: {options: {omitNull: true}}
}, {
arguments: ['myTable', function(sequelize) {
......@@ -407,7 +443,10 @@ if (dialect === 'sqlite') {
foo: sequelize.fn('NOW')
};
}],
expectation: 'INSERT INTO `myTable` (`foo`) VALUES (NOW());',
expectation: {
query: 'INSERT INTO `myTable` (`foo`) VALUES (NOW());',
bind: []
},
needsSequelize: true
}
],
......@@ -547,7 +586,8 @@ if (dialect === 'sqlite') {
});
tests.forEach(test => {
const title = test.title || 'SQLite correctly returns ' + test.expectation + ' for ' + JSON.stringify(test.arguments);
const query = test.expectation.query || test.expectation;
const title = test.title || 'SQLite correctly returns ' + query + ' for ' + JSON.stringify(test.arguments);
it(title, function() {
if (test.needsSequelize) {
if (_.isFunction(test.arguments[1])) test.arguments[1] = test.arguments[1](this.sequelize);
......
......@@ -27,12 +27,15 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
};
expectsql(sql.insertQuery(User.tableName, {user_name: 'triggertest'}, User.rawAttributes, options),
{
mssql: 'declare @tmp table ([id] INTEGER,[user_name] NVARCHAR(255));INSERT INTO [users] ([user_name]) OUTPUT INSERTED.[id],INSERTED.[user_name] into @tmp VALUES (N\'triggertest\');select * from @tmp;',
postgres: 'INSERT INTO "users" ("user_name") VALUES (\'triggertest\') RETURNING *;',
default: "INSERT INTO `users` (`user_name`) VALUES ('triggertest');"
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;',
postgres: 'INSERT INTO "users" ("user_name") VALUES ($1) RETURNING *;',
default: 'INSERT INTO `users` (`user_name`) VALUES ($1);'
},
bind: ['triggertest']
});
});
});
});
describe('dates', () => {
......@@ -51,10 +54,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
expectsql(timezoneSequelize.dialect.QueryGenerator.insertQuery(User.tableName, {date: new Date(Date.UTC(2015, 0, 20))}, User.rawAttributes, {}),
{
postgres: 'INSERT INTO "users" ("date") VALUES (\'2015-01-20 01:00:00.000 +01:00\');',
sqlite: 'INSERT INTO `users` (`date`) VALUES (\'2015-01-20 00:00:00.000 +00:00\');',
mssql: 'INSERT INTO [users] ([date]) VALUES (N\'2015-01-20 01:00:00.000 +01:00\');',
mysql: "INSERT INTO `users` (`date`) VALUES ('2015-01-20 01:00:00');"
query: {
postgres: 'INSERT INTO "users" ("date") VALUES ($1);',
mssql: 'INSERT INTO [users] ([date]) VALUES ($1);',
default: 'INSERT INTO `users` (`date`) VALUES ($1);'
},
bind: {
sqlite: ['2015-01-20 00:00:00.000 +00:00'],
mysql: ['2015-01-20 01:00:00'],
default: ['2015-01-20 01:00:00.000 +01:00']
}
});
});
......@@ -73,10 +82,16 @@ describe(Support.getTestDialectTeaser('SQL'), () => {
expectsql(timezoneSequelize.dialect.QueryGenerator.insertQuery(User.tableName, {date: new Date(Date.UTC(2015, 0, 20, 1, 2, 3, 89))}, User.rawAttributes, {}),
{
postgres: 'INSERT INTO "users" ("date") VALUES (\'2015-01-20 02:02:03.089 +01:00\');',
sqlite: 'INSERT INTO `users` (`date`) VALUES (\'2015-01-20 01:02:03.089 +00:00\');',
mssql: 'INSERT INTO [users] ([date]) VALUES (N\'2015-01-20 02:02:03.089 +01:00\');',
mysql: "INSERT INTO `users` (`date`) VALUES ('2015-01-20 02:02:03.089');"
query: {
postgres: 'INSERT INTO "users" ("date") VALUES ($1);',
mssql: 'INSERT INTO [users] ([date]) VALUES ($1);',
default: 'INSERT INTO `users` (`date`) VALUES ($1);'
},
bind: {
sqlite: ['2015-01-20 01:02:03.089 +00:00'],
mysql: ['2015-01-20 02:02:03.089'],
default: ['2015-01-20 02:02:03.089 +01:00']
}
});
});
});
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!