From 6ad1deda9f410e4a97ffbd8abe5bc5289d51ee26 Mon Sep 17 00:00:00 2001 From: MaolmeoX Date: Mon, 9 Nov 2026 10:27:25 +0000 Subject: [PATCH] fix: join parameterized SQL statements in linear time ParameterizedSQL.join builds the joined SQL and one fresh params array in a single pass. Appending each statement through ParameterizedSQL.append copied the accumulated params array once per statement, so building an inq/nin clause over N values cost O(N^2): 35,000 values blocked the event loop for about 2.3 s. The params arrays of the joined statements are left untouched, since the constructor keeps the caller's array by reference. Fixes #1003 Signed-off-by: MaolmeoX --- lib/parameterized-sql.js | 19 ++++++++++++++++-- test/sql.test.js | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/parameterized-sql.js b/lib/parameterized-sql.js index a5957b13..910dd89d 100644 --- a/lib/parameterized-sql.js +++ b/lib/parameterized-sql.js @@ -105,10 +105,25 @@ ParameterizedSQL.append = function(currentStmt, stmt, separator) { */ ParameterizedSQL.join = function(sqls, separator) { assert(Array.isArray(sqls), 'sqls must be an array'); - const ps = new ParameterizedSQL('', []); + separator = typeof separator === 'string' ? separator : ' '; + let sql = ''; + const params = []; for (let i = 0, n = sqls.length; i < n; i++) { - this.append(ps, sqls[i], separator); + const stmt = (sqls[i] instanceof ParameterizedSQL) ? sqls[i] : + new ParameterizedSQL(sqls[i]); + if (sql) { + sql += separator; + } + if (stmt.sql) { + sql += stmt.sql; + } + for (let j = 0, m = stmt.params.length; j < m; j++) { + params.push(stmt.params[j]); + } } + const ps = new ParameterizedSQL('', []); + ps.sql = sql; + ps.params = params; return ps; }; diff --git a/test/sql.test.js b/test/sql.test.js index 213b47b7..287993ea 100644 --- a/test/sql.test.js +++ b/test/sql.test.js @@ -489,6 +489,49 @@ describe('sql connector', function() { ); }); + it('joins SQL statements', function() { + const stmt = ParameterizedSQL.join([ + '', + new ParameterizedSQL('`NAME`=?', ['John']), + {sql: '`VIP`=?', params: [true]}, + '', + '`TOKEN` IS NULL', + {sql: '`ID` IN (?,?)', params: [1, [2, 3]]}, + ], ' AND '); + expect(stmt).to.be.instanceOf(ParameterizedSQL); + expect(stmt.toJSON()).to.eql({ + sql: '`NAME`=? AND `VIP`=? AND AND `TOKEN` IS NULL AND `ID` IN (?,?)', + params: ['John', true, 1, [2, 3]], + }); + expect(ParameterizedSQL.join(['SELECT *', 'FROM `CUSTOMER`']).toJSON()) + .to.eql({sql: 'SELECT * FROM `CUSTOMER`', params: []}); + }); + + it('joins SQL statements without modifying their params', function() { + const params1 = ['John']; + const params2 = ['Mary']; + const stmt1 = new ParameterizedSQL('`NAME`=?', params1); + const stmt2 = new ParameterizedSQL('`NAME`=?', params2); + const joined = ParameterizedSQL.join([stmt1, stmt2], ' OR '); + joined.params.push('Jane'); + expect(joined.params).to.eql(['John', 'Mary', 'Jane']); + expect(params1).to.eql(['John']); + expect(params2).to.eql(['Mary']); + expect(stmt1.params).to.equal(params1); + expect(stmt2.params).to.equal(params2); + }); + + it('joins a large number of SQL statements in linear time', function() { + this.timeout(1000); + const values = []; + for (let i = 0; i < 50000; i++) { + values.push(new ParameterizedSQL('?', [i])); + } + const stmt = ParameterizedSQL.join(values, ','); + expect(stmt.params).to.have.length(50000); + expect(stmt.params[49999]).to.equal(49999); + }); + it('should throw if params does not match placeholders', function() { expect(function() { let stmt1 = 'SELECT * from `CUSTOMER`';