Skip to content

Commit

Permalink
feat(query-generator): Generate INSERTs using bind parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
gazoakley committed May 28, 2018
1 parent 780f7c0 commit 09fe634
Show file tree
Hide file tree
Showing 17 changed files with 534 additions and 126 deletions.
35 changes: 32 additions & 3 deletions lib/data-types.js
Expand Up @@ -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};
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 = new Buffer(value);
} else {
value = new Buffer(value.toString());
}
}
return options.bindParam(value);
};

function RANGE(subtype) {
const options = _.isPlainObject(subtype) ? subtype : {subtype};

Expand Down Expand Up @@ -707,6 +730,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};
Expand All @@ -725,6 +751,9 @@ GEOGRAPHY.prototype.escape = false;
GEOGRAPHY.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)) + ')';
};

for (const helper of Object.keys(helpers)) {
for (const DataType of helpers[helper]) {
Expand Down
82 changes: 71 additions & 11 deletions lib/dialects/abstract/query-generator.js
Expand Up @@ -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 %>';
Expand Down Expand Up @@ -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')) {
Expand Down Expand Up @@ -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));
}
}
}
}
Expand All @@ -234,7 +247,12 @@ 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
if (options.bindParam === false) {
return query;
}
return { query, bind };
}

/**
Expand Down Expand Up @@ -961,15 +979,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
Expand All @@ -989,6 +999,56 @@ 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 });
}
if (field.type.stringify) {
value = field.type.stringify(value, { escape: _.identity, field, timezone: this.options.timezone, operation: options.operation });
}
}
}
}

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);
}
Expand Down
3 changes: 3 additions & 0 deletions lib/dialects/mssql/query.js
Expand Up @@ -41,6 +41,9 @@ class Query extends AbstractQuery {
paramType.typeOptions = {precision: 30, scale: 15};
}
}
if (Buffer.isBuffer(value)) {
paramType.type = TYPES.VarBinary;
}
return paramType;
}

Expand Down
72 changes: 67 additions & 5 deletions lib/dialects/postgres/data-types.js
Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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 = {
Expand Down Expand Up @@ -533,6 +545,26 @@ module.exports = BaseTypes => {
};

RANGE.prototype.escape = false;
RANGE.prototype._value = function _value(values, options) {
if (!Array.isArray(values)) {
return this.options.subtype.stringify(values, options);
}
const valuesStringified = values.map(value => {
if (_.includes([null, -Infinity, Infinity], value)) {
// Pass through "unbounded" bounds unchanged
return value;
} else if (this.options.subtype.stringify) {
return this.options.subtype.stringify(value, options);
} else {
return options.escape(value);
}
});

// Array.map does not preserve extra array properties
valuesStringified.inclusive = values.inclusive;

return range.stringify(valuesStringified);
};
RANGE.prototype._stringify = function _stringify(values, options) {
if (!Array.isArray(values)) {
return "'" + this.options.subtype.stringify(values, options) + "'::" +
Expand All @@ -554,15 +586,39 @@ module.exports = BaseTypes => {

return '\'' + range.stringify(valuesStringified) + '\'';
};
RANGE.prototype._bindParam = function _bindParam(values, options) {
if (!Array.isArray(values)) {
return options.bindParam(this.options.subtype.stringify(values, options)) + '::' +
this.toCastType();
}
const valuesStringified = values.map(value => {
if (_.includes([null, -Infinity, Infinity], value)) {
// Pass through "unbounded" bounds unchanged
return value;
} else if (this.options.subtype.stringify) {
return this.options.subtype.stringify(value, options);
} else {
return options.escape(value);
}
});

// Array.map does not preserve extra array properties
valuesStringified.inclusive = values.inclusive;

return options.bindParam(range.stringify(valuesStringified));
};

BaseTypes.RANGE.types.postgres = {
oids: [3904, 3906, 3908, 3910, 3912, 3926],
array_oids: [3905, 3907, 3909, 3911, 3913, 3927]
};

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

Expand All @@ -571,7 +627,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');
Expand All @@ -589,6 +648,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);
Expand Down
3 changes: 2 additions & 1 deletion lib/dialects/postgres/query-generator.js
Expand Up @@ -344,7 +344,8 @@ 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);
let 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`);
Expand Down
10 changes: 7 additions & 3 deletions lib/dialects/sqlite/query-generator.js
Expand Up @@ -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) {
Expand Down Expand Up @@ -485,4 +489,4 @@ class SQLiteQueryGenerator extends MySqlQueryGenerator {
}
};

module.exports = SQLiteQueryGenerator;
module.exports = SQLiteQueryGenerator;

0 comments on commit 09fe634

Please sign in to comment.