From 0f7cdaf7acf6e85f76c35284c5ac424a130a365e Mon Sep 17 00:00:00 2001 From: Douglas Christopher Wilson Date: Sun, 6 Mar 2022 19:59:02 -0500 Subject: [PATCH 1/3] Remove object key-value-pair escape behavior --- HISTORY.md | 5 +++++ README.md | 24 ++++++-------------- lib/SqlString.js | 26 ++++------------------ test/unit/test-SqlString.js | 44 +++++++++---------------------------- 4 files changed, 26 insertions(+), 73 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index aea1dfc..e170095 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,8 @@ +unreleased +========== + + * Remove object key-value-pair escape behavior + 2.3.3 / 2022-03-06 ================== diff --git a/README.md b/README.md index a00c560..ec4312e 100644 --- a/README.md +++ b/README.md @@ -75,24 +75,14 @@ Different value types are escaped differently, here is how: 'b'], ['c', 'd']]` turns into `('a', 'b'), ('c', 'd')` * Objects that have a `toSqlString` method will have `.toSqlString()` called and the returned value is used as the raw SQL. -* Objects are turned into `key = 'val'` pairs for each enumerable property on - the object. If the property's value is a function, it is skipped; if the - property's value is an object, toString() is called on it and the returned - value is used. * `undefined` / `null` are converted to `NULL` * `NaN` / `Infinity` are left as-is. MySQL does not support these, and trying to insert them as values will trigger MySQL errors until they implement support. +* All other values types are converted to a string using the global `String()` + and the resulting value is escaped. -You may have noticed that this escaping allows you to do neat things like this: - -```js -var post = {id: 1, title: 'Hello MySQL'}; -var sql = SqlString.format('INSERT INTO posts SET ?', post); -console.log(sql); // INSERT INTO posts SET `id` = 1, `title` = 'Hello MySQL' -``` - -And the `toSqlString` method allows you to form complex queries with functions: +The `toSqlString` method allows you to form complex queries with functions: ```js var CURRENT_TIMESTAMP = { toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }; @@ -176,8 +166,7 @@ console.log(sql); // SELECT * FROM `users` WHERE `id` = 1 Following this you then have a valid, escaped query that you can then send to the database safely. This is useful if you are looking to prepare the query before actually sending it to the database. -You also have the option (but are not required) to pass in `stringifyObject` and `timeZone`, -allowing you provide a custom means of turning objects into strings, as well as a +You also have the option (but are not required) to pass in `timeZone`, allowing you provide a location-specific/timezone-aware `Date`. This can be further combined with the `SqlString.raw()` helper to generate SQL @@ -185,8 +174,9 @@ that includes MySQL functions as dynamic vales: ```js var userId = 1; -var data = { email: 'foobar@example.com', modified: SqlString.raw('NOW()') }; -var sql = SqlString.format('UPDATE ?? SET ? WHERE `id` = ?', ['users', data, userId]); +var email = 'foobar@example.com'; +var sql = SqlString.format('UPDATE ?? SET `email` = ?, `modified` = ? WHERE `id` = ?', + ['users', email, SqlString.raw('NOW()'), userId]); console.log(sql); // UPDATE `users` SET `email` = 'foobar@example.com', `modified` = NOW() WHERE `id` = 1 ``` diff --git a/lib/SqlString.js b/lib/SqlString.js index 8206dad..cf610e8 100644 --- a/lib/SqlString.js +++ b/lib/SqlString.js @@ -31,14 +31,14 @@ SqlString.escapeId = function escapeId(val, forbidQualified) { } }; -SqlString.escape = function escape(val, stringifyObjects, timeZone) { +SqlString.escape = function escape(val, timeZone) { if (val === undefined || val === null) { return 'NULL'; } switch (typeof val) { case 'boolean': return (val) ? 'true' : 'false'; - case 'number': return val + ''; + case 'number': return String(val); case 'object': if (Object.prototype.toString.call(val) === '[object Date]') { return SqlString.dateToString(val, timeZone || 'local'); @@ -48,12 +48,10 @@ SqlString.escape = function escape(val, stringifyObjects, timeZone) { return SqlString.bufferToString(val); } else if (typeof val.toSqlString === 'function') { return String(val.toSqlString()); - } else if (stringifyObjects) { - return escapeString(val.toString()); } else { - return SqlString.objectToValues(val, timeZone); + return escapeString(String(val)); } - default: return escapeString(val); + default: return escapeString(String(val)); } }; @@ -167,22 +165,6 @@ SqlString.bufferToString = function bufferToString(buffer) { return 'X' + escapeString(buffer.toString('hex')); }; -SqlString.objectToValues = function objectToValues(object, timeZone) { - var sql = ''; - - for (var key in object) { - var val = object[key]; - - if (typeof val === 'function') { - continue; - } - - sql += (sql.length === 0 ? '' : ', ') + SqlString.escapeId(key) + ' = ' + SqlString.escape(val, true, timeZone); - } - - return sql; -}; - SqlString.raw = function raw(sql) { if (typeof sql !== 'string') { throw new TypeError('argument sql must be a string'); diff --git a/test/unit/test-SqlString.js b/test/unit/test-SqlString.js index 580aa4e..9dea225 100644 --- a/test/unit/test-SqlString.js +++ b/test/unit/test-SqlString.js @@ -71,16 +71,9 @@ test('SqlString.escape', { assert.equal(SqlString.escape(SqlString.raw('NOW()')), 'NOW()'); }, - 'objects are turned into key value pairs': function() { - assert.equal(SqlString.escape({a: 'b', c: 'd'}), "`a` = 'b', `c` = 'd'"); - }, - - 'objects function properties are ignored': function() { - assert.equal(SqlString.escape({a: 'b', c: function() {}}), "`a` = 'b'"); - }, - - 'object values toSqlString is called': function() { - assert.equal(SqlString.escape({id: { toSqlString: function() { return 'LAST_INSERT_ID()'; } }}), '`id` = LAST_INSERT_ID()'); + 'objects are turned into string value': function() { + assert.equal(SqlString.escape({ 'hello': 'world' }), "'[object Object]'"); + assert.equal(SqlString.escape({ toString: function () { return 'hello'; } }), "'hello'"); }, 'objects toSqlString is called': function() { @@ -91,18 +84,6 @@ test('SqlString.escape', { assert.equal(SqlString.escape({ toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }), 'CURRENT_TIMESTAMP()'); }, - 'nested objects are cast to strings': function() { - assert.equal(SqlString.escape({a: {nested: true}}), "`a` = '[object Object]'"); - }, - - 'nested objects use toString': function() { - assert.equal(SqlString.escape({a: { toString: function() { return 'foo'; } }}), "`a` = 'foo'"); - }, - - 'nested objects use toString is quoted': function() { - assert.equal(SqlString.escape({a: { toString: function() { return "f'oo"; } }}), "`a` = 'f\\'oo'"); - }, - 'arrays are turned into lists': function() { assert.equal(SqlString.escape([1, 2, 'c']), "1, 2, 'c'"); }, @@ -179,7 +160,7 @@ test('SqlString.escape', { 'dates are converted to specified time zone "Z"': function() { var expected = '2012-05-07 11:42:03.002'; var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var string = SqlString.escape(date, false, 'Z'); + var string = SqlString.escape(date, 'Z'); assert.strictEqual(string, "'" + expected + "'"); }, @@ -187,7 +168,7 @@ test('SqlString.escape', { 'dates are converted to specified time zone "+01"': function() { var expected = '2012-05-07 12:42:03.002'; var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var string = SqlString.escape(date, false, '+01'); + var string = SqlString.escape(date, '+01'); assert.strictEqual(string, "'" + expected + "'"); }, @@ -195,7 +176,7 @@ test('SqlString.escape', { 'dates are converted to specified time zone "+0200"': function() { var expected = '2012-05-07 13:42:03.002'; var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var string = SqlString.escape(date, false, '+0200'); + var string = SqlString.escape(date, '+0200'); assert.strictEqual(string, "'" + expected + "'"); }, @@ -203,15 +184,15 @@ test('SqlString.escape', { 'dates are converted to specified time zone "-05:00"': function() { var expected = '2012-05-07 06:42:03.002'; var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var string = SqlString.escape(date, false, '-05:00'); + var string = SqlString.escape(date, '-05:00'); assert.strictEqual(string, "'" + expected + "'"); }, 'dates are converted to UTC for unknown time zone': function() { var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2)); - var expected = SqlString.escape(date, false, 'Z'); - var string = SqlString.escape(date, false, 'foo'); + var expected = SqlString.escape(date, 'Z'); + var string = SqlString.escape(date, 'foo'); assert.strictEqual(string, expected); }, @@ -291,13 +272,8 @@ test('SqlString.format', { assert.equal(sql, '?'); }, - 'objects is converted to values': function () { + 'objects is converted to string value': function () { var sql = SqlString.format('?', { 'hello': 'world' }, false); - assert.equal(sql, "`hello` = 'world'"); - }, - - 'objects is not converted to values': function () { - var sql = SqlString.format('?', { 'hello': 'world' }, true); assert.equal(sql, "'[object Object]'"); var sql = SqlString.format('?', { toString: function () { return 'hello'; } }, true); From 9d7cb825165ac20a4bce9386b59f8f8c949959e3 Mon Sep 17 00:00:00 2001 From: Douglas Christopher Wilson Date: Sun, 6 Mar 2022 20:08:11 -0500 Subject: [PATCH 2/3] Remove array escape behavior --- HISTORY.md | 1 + README.md | 6 +----- lib/SqlString.js | 28 +--------------------------- test/unit/test-SqlString.js | 24 ++++-------------------- 4 files changed, 7 insertions(+), 52 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index e170095..365947e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,7 @@ unreleased ========== + * Remove array escape behavior * Remove object key-value-pair escape behavior 2.3.3 / 2022-03-06 diff --git a/README.md b/README.md index ec4312e..035b8cf 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,6 @@ Different value types are escaped differently, here is how: * Date objects are converted to `'YYYY-mm-dd HH:ii:ss'` strings * Buffers are converted to hex strings, e.g. `X'0fa5'` * Strings are safely escaped -* Arrays are turned into list, e.g. `['a', 'b']` turns into `'a', 'b'` -* Nested arrays are turned into grouped lists (for bulk inserts), e.g. `[['a', - 'b'], ['c', 'd']]` turns into `('a', 'b'), ('c', 'd')` * Objects that have a `toSqlString` method will have `.toSqlString()` called and the returned value is used as the raw SQL. * `undefined` / `null` are converted to `NULL` @@ -144,8 +141,7 @@ like to have escaped like this: ```js var userId = 1; -var columns = ['username', 'email']; -var sql = SqlString.format('SELECT ?? FROM ?? WHERE id = ?', [columns, 'users', userId]); +var sql = SqlString.format('SELECT ??, ?? FROM ?? WHERE id = ?', ['username', 'email', 'users', userId]); console.log(sql); // SELECT `username`, `email` FROM `users` WHERE id = 1 ``` **Please note that this last character sequence is experimental and syntax might change** diff --git a/lib/SqlString.js b/lib/SqlString.js index cf610e8..e945d43 100644 --- a/lib/SqlString.js +++ b/lib/SqlString.js @@ -16,15 +16,7 @@ var CHARS_ESCAPE_MAP = { }; SqlString.escapeId = function escapeId(val, forbidQualified) { - if (Array.isArray(val)) { - var sql = ''; - - for (var i = 0; i < val.length; i++) { - sql += (i === 0 ? '' : ', ') + SqlString.escapeId(val[i], forbidQualified); - } - - return sql; - } else if (forbidQualified) { + if (forbidQualified) { return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``') + '`'; } else { return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``').replace(QUAL_GLOBAL_REGEXP, '`.`') + '`'; @@ -42,8 +34,6 @@ SqlString.escape = function escape(val, timeZone) { case 'object': if (Object.prototype.toString.call(val) === '[object Date]') { return SqlString.dateToString(val, timeZone || 'local'); - } else if (Array.isArray(val)) { - return SqlString.arrayToList(val, timeZone); } else if (Buffer.isBuffer(val)) { return SqlString.bufferToString(val); } else if (typeof val.toSqlString === 'function') { @@ -55,22 +45,6 @@ SqlString.escape = function escape(val, timeZone) { } }; -SqlString.arrayToList = function arrayToList(array, timeZone) { - var sql = ''; - - for (var i = 0; i < array.length; i++) { - var val = array[i]; - - if (Array.isArray(val)) { - sql += (i === 0 ? '' : ', ') + '(' + SqlString.arrayToList(val, timeZone) + ')'; - } else { - sql += (i === 0 ? '' : ', ') + SqlString.escape(val, true, timeZone); - } - } - - return sql; -}; - SqlString.format = function format(sql, values, stringifyObjects, timeZone) { if (values == null) { return sql; diff --git a/test/unit/test-SqlString.js b/test/unit/test-SqlString.js index 9dea225..a8bcc0d 100644 --- a/test/unit/test-SqlString.js +++ b/test/unit/test-SqlString.js @@ -40,12 +40,8 @@ test('SqlString.escapeId', { assert.equal(SqlString.escapeId('id1.id2', true), '`id1.id2`'); }, - 'arrays are turned into lists': function() { - assert.equal(SqlString.escapeId(['a', 'b', 't.c']), '`a`, `b`, `t`.`c`'); - }, - - 'nested arrays are flattened': function() { - assert.equal(SqlString.escapeId(['a', ['b', ['t.c']]]), '`a`, `b`, `t`.`c`'); + 'arrays are stringified and then escaped': function() { + assert.equal(SqlString.escapeId(['a', 'b', 'c']), '`a,b,c`'); } }); @@ -84,20 +80,8 @@ test('SqlString.escape', { assert.equal(SqlString.escape({ toSqlString: function() { return 'CURRENT_TIMESTAMP()'; } }), 'CURRENT_TIMESTAMP()'); }, - 'arrays are turned into lists': function() { - assert.equal(SqlString.escape([1, 2, 'c']), "1, 2, 'c'"); - }, - - 'nested arrays are turned into grouped lists': function() { - assert.equal(SqlString.escape([[1, 2, 3], [4, 5, 6], ['a', 'b', {nested: true}]]), "(1, 2, 3), (4, 5, 6), ('a', 'b', '[object Object]')"); - }, - - 'nested objects inside arrays are cast to strings': function() { - assert.equal(SqlString.escape([1, {nested: true}, 2]), "1, '[object Object]', 2"); - }, - - 'nested objects inside arrays use toString': function() { - assert.equal(SqlString.escape([1, { toString: function() { return 'foo'; } }, 2]), "1, 'foo', 2"); + 'arrays are stringified and escaped': function() { + assert.equal(SqlString.escape([1, 2, 'c']), "'1,2,c'"); }, 'strings are quoted': function() { From baa8c98a8fc87f8a4dfec369d54cfd8a2af8ecb6 Mon Sep 17 00:00:00 2001 From: Douglas Christopher Wilson Date: Sun, 6 Mar 2022 20:11:12 -0500 Subject: [PATCH 3/3] Change identifier escaping to always be literal --- HISTORY.md | 1 + README.md | 17 ----------------- lib/SqlString.js | 9 ++------- test/unit/test-SqlString.js | 6 +----- 4 files changed, 4 insertions(+), 29 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 365947e..5662115 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,7 @@ unreleased ========== + * Change identifier escaping to always be literal * Remove array escape behavior * Remove object key-value-pair escape behavior diff --git a/README.md b/README.md index 035b8cf..8f645b1 100644 --- a/README.md +++ b/README.md @@ -119,23 +119,6 @@ var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId(sorter); console.log(sql); // SELECT * FROM posts ORDER BY `date` ``` -It also supports adding qualified identifiers. It will escape both parts. - -```js -var sorter = 'date'; -var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId('posts.' + sorter); -console.log(sql); // SELECT * FROM posts ORDER BY `posts`.`date` -``` - -If you do not want to treat `.` as qualified identifiers, you can set the second -argument to `true` in order to keep the string as a literal identifier: - -```js -var sorter = 'date.2'; -var sql = 'SELECT * FROM posts ORDER BY ' + SqlString.escapeId(sorter, true); -console.log(sql); // SELECT * FROM posts ORDER BY `date.2` -``` - Alternatively, you can use `??` characters as placeholders for identifiers you would like to have escaped like this: diff --git a/lib/SqlString.js b/lib/SqlString.js index e945d43..14babef 100644 --- a/lib/SqlString.js +++ b/lib/SqlString.js @@ -1,7 +1,6 @@ var SqlString = exports; var ID_GLOBAL_REGEXP = /`/g; -var QUAL_GLOBAL_REGEXP = /\./g; var CHARS_GLOBAL_REGEXP = /[\0\b\t\n\r\x1a\"\'\\]/g; // eslint-disable-line no-control-regex var CHARS_ESCAPE_MAP = { '\0' : '\\0', @@ -15,12 +14,8 @@ var CHARS_ESCAPE_MAP = { '\\' : '\\\\' }; -SqlString.escapeId = function escapeId(val, forbidQualified) { - if (forbidQualified) { - return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``') + '`'; - } else { - return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``').replace(QUAL_GLOBAL_REGEXP, '`.`') + '`'; - } +SqlString.escapeId = function escapeId(val) { + return '`' + String(val).replace(ID_GLOBAL_REGEXP, '``') + '`'; }; SqlString.escape = function escape(val, timeZone) { diff --git a/test/unit/test-SqlString.js b/test/unit/test-SqlString.js index a8bcc0d..3f5b086 100644 --- a/test/unit/test-SqlString.js +++ b/test/unit/test-SqlString.js @@ -29,11 +29,7 @@ test('SqlString.escapeId', { }, 'value containing separator is quoted': function() { - assert.equal(SqlString.escapeId('id1.id2'), '`id1`.`id2`'); - }, - - 'value containing separator and escapes is quoted': function() { - assert.equal(SqlString.escapeId('id`1.i`d2'), '`id``1`.`i``d2`'); + assert.equal(SqlString.escapeId('id1.id2'), '`id1.id2`'); }, 'value containing separator is fully escaped when forbidQualified': function() {