From 231164864ac365f2d52578405f5627a526219a78 Mon Sep 17 00:00:00 2001 From: Paul Rutter Date: Wed, 25 Oct 2023 13:05:47 +0200 Subject: [PATCH 1/9] Performance improvements for AlaSQL - Add pattern cache when using like (building RegEx is expensive) - Optimize when having lots of values in a (NOT) IN statement, using JS Set - Remove RegEx checking for DATE function - When using NOW, just return the date (not sure why a date is translated to a string and then to a date again) --- src/15utility.js | 86 ++++++++++++++++++++++++--------------------- src/50expression.js | 24 ++++++++++++- src/61date.js | 18 +--------- 3 files changed, 69 insertions(+), 59 deletions(-) diff --git a/src/15utility.js b/src/15utility.js index a3f79d05ab..eb370243b8 100755 --- a/src/15utility.js +++ b/src/15utility.js @@ -1,9 +1,9 @@ /*jshint unused:false*/ /* - Utilities for Alasql.js + Utilities for Alasql.js - @todo Review the list of utilities - @todo Find more effective utilities + @todo Review the list of utilities + @todo Find more effective utilities */ /** @@ -59,7 +59,7 @@ function returnTrue() { @function @return {undefined} Always undefined */ -function returnUndefined() {} +function returnUndefined() { } /** Escape string @@ -354,7 +354,7 @@ var loadFile = (utils.loadFile = function (path, asy, success, error) { } else if (utils.isCordova) { /* If Cordova */ utils.global.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fileSystem) { - fileSystem.root.getFile(path, {create: false}, function (fileEntry) { + fileSystem.root.getFile(path, { create: false }, function (fileEntry) { fileEntry.file(function (file) { var fileReader = new FileReader(); fileReader.onloadend = function (e) { @@ -565,7 +565,7 @@ var removeFile = (utils.removeFile = function (path, cb) { utils.global.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fileSystem) { fileSystem.root.getFile( path, - {create: false}, + { create: false }, function (fileEntry) { fileEntry.remove(cb); cb && cb(); // jshint ignore:line @@ -633,7 +633,7 @@ var fileExists = (utils.fileExists = function (path, cb) { utils.global.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fileSystem) { fileSystem.root.getFile( path, - {create: false}, + { create: false }, function (fileEntry) { cb(true); }, @@ -699,7 +699,7 @@ var saveFile = (utils.saveFile = function (path, data, cb, opts) { } else if (utils.isCordova) { utils.global.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function (fileSystem) { // alasql.utils.removeFile(path,function(){ - fileSystem.root.getFile(path, {create: true}, function (fileEntry) { + fileSystem.root.getFile(path, { create: true }, function (fileEntry) { fileEntry.createWriter(function (fileWriter) { fileWriter.onwriteend = function () { if (cb) { @@ -774,7 +774,7 @@ var saveFile = (utils.saveFile = function (path, data, cb, opts) { disableAutoBom: false, }; alasql.utils.extend(opt, opts); - var blob = new Blob([data], {type: 'text/plain;charset=utf-8'}); + var blob = new Blob([data], { type: 'text/plain;charset=utf-8' }); saveAs(blob, path, opt.disableAutoBom); if (cb) { res = cb(res); @@ -1175,43 +1175,47 @@ var domEmptyChildren = (utils.domEmptyChildren = function (container) { @parameter {string} escape Escape character (optional) @return {boolean} If value LIKE pattern ESCAPE escape */ - +var patternCache = {}; var like = (utils.like = function (pattern, value, escape) { - // Verify escape character - if (!escape) escape = ''; - - var i = 0; - var s = '^'; - - while (i < pattern.length) { - var c = pattern[i], - c1 = ''; - if (i < pattern.length - 1) c1 = pattern[i + 1]; - - if (c === escape) { - s += '\\' + c1; - i++; - } else if (c === '[' && c1 === '^') { - s += '[^'; + if (!patternCache[pattern]) { + // Verify escape character + if (!escape) escape = ''; + + var i = 0; + var s = '^'; + + while (i < pattern.length) { + var c = pattern[i], + c1 = ''; + if (i < pattern.length - 1) c1 = pattern[i + 1]; + + if (c === escape) { + s += '\\' + c1; + i++; + } else if (c === '[' && c1 === '^') { + s += '[^'; + i++; + } else if (c === '[' || c === ']') { + s += c; + } else if (c === '%') { + s += '[\\s\\S]*'; + } else if (c === '_') { + s += '.'; + } else if ('/.*+?|(){}'.indexOf(c) > -1) { + s += '\\' + c; + } else { + s += c; + } i++; - } else if (c === '[' || c === ']') { - s += c; - } else if (c === '%') { - s += '[\\s\\S]*'; - } else if (c === '_') { - s += '.'; - } else if ('/.*+?|(){}'.indexOf(c) > -1) { - s += '\\' + c; - } else { - s += c; } - i++; + + s += '$'; + // if(value == undefined) return false; + //console.log(s,value,(value||'').search(RegExp(s))>-1); + patternCache[pattern] = RegExp(s.toUpperCase()) } + return ('' + (value || '')).toUpperCase().search(patternCache[pattern]) > -1; - s += '$'; - // if(value == undefined) return false; - //console.log(s,value,(value||'').search(RegExp(s))>-1); - return ('' + (value ?? '')).search(RegExp(s, 'i')) > -1; }); utils.glob = function (value, pattern) { diff --git a/src/50expression.js b/src/50expression.js index 9f64bafce1..cad6484cd3 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -482,6 +482,17 @@ '))'; s += '.indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))>-1)'; + } else if (Array.isArray(this.right) && this.right.every((value) => value.value)) { + // Added patch to have a better performance for when you have a lot of entries in an IN statement + if (!alasql.sets) { + alasql.sets = {}; + } + const allValues = this.right.map((value) => value.value); + const allValuesStr = allValues.join(","); + if (!alasql.sets[allValuesStr]) { + alasql.sets[allValuesStr] = new Set(allValues); + } + s = 'alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; } else if (Array.isArray(this.right)) { // if(this.right.length == 0) return 'false'; s = @@ -505,6 +516,17 @@ s += 'alasql.utils.flatArray(this.queriesfn[' + this.queriesidx + '](params,null,p))'; s += '.indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))<0)'; + } else if (Array.isArray(this.right) && this.right.every((value) => value.value)) { + // Added patch to have a better performance for when you have a lot of entries in a NOT IN statement + if (!alasql.sets) { + alasql.sets = {}; + } + const allValues = this.right.map((value) => value.value); + const allValuesStr = allValues.join(","); + if (!alasql.sets[allValuesStr]) { + alasql.sets[allValuesStr] = new Set(allValues); + } + s = '!alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; } else if (Array.isArray(this.right)) { s = '([' + this.right.map(ref).join(',') + '].indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))<0)'; @@ -750,7 +772,7 @@ toString() { var s; - const {op, right} = this; + const { op, right } = this; const res = right.toString(); if (op === '~') { diff --git a/src/61date.js b/src/61date.js index f5c4101679..525e9d8672 100644 --- a/src/61date.js +++ b/src/61date.js @@ -43,27 +43,11 @@ stdfn.OBJECT_ID = function (objid) { }; stdfn.DATE = function (d) { - if (/\d{8}/.test(d)) return new Date(+d.substr(0, 4), +d.substr(4, 2) - 1, +d.substr(6, 2)); return newDate(d); }; stdfn.NOW = function () { - var d = new Date(); - var s = - d.getFullYear() + - '-' + - ('0' + (d.getMonth() + 1)).substr(-2) + - '-' + - ('0' + d.getDate()).substr(-2); - s += - ' ' + - ('0' + d.getHours()).substr(-2) + - ':' + - ('0' + d.getMinutes()).substr(-2) + - ':' + - ('0' + d.getSeconds()).substr(-2); - s += '.' + ('00' + d.getMilliseconds()).substr(-3); - return s; + return new Date(); }; stdfn.GETDATE = stdfn.NOW; From 5a4ea47d67376315de9272d7bacee6cd032a58f6 Mon Sep 17 00:00:00 2001 From: Paul Rutter Date: Wed, 25 Oct 2023 14:00:24 +0200 Subject: [PATCH 2/9] Performance improvements for AlaSQL - Add AST cache --- src/17alasql.js | 10 +++++++--- src/20database.js | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/17alasql.js b/src/17alasql.js index bfa1447708..58bebe884e 100755 --- a/src/17alasql.js +++ b/src/17alasql.js @@ -246,10 +246,9 @@ alasql.dexec = function (databaseid, sql, params, cb, scope) { // if(db.databaseid != databaseid) console.trace('got!'); // console.log(3,db.databaseid,databaseid); - var hh; + var hh = hash(sql); // Create hash if (alasql.options.cache) { - hh = hash(sql); var statement = db.sqlCache[hh]; // If database structure was not changed since last time return cache if (statement && db.dbversion === statement.dbversion) { @@ -258,7 +257,12 @@ alasql.dexec = function (databaseid, sql, params, cb, scope) { } // Create AST - var ast = alasql.parse(sql); + var ast = db.astCache[hh]; + if (!ast) { + ast = alasql.parse(sql); + // add to AST cache + db.astCache[hh]= ast; + } if (!ast.statements) { return; } diff --git a/src/20database.js b/src/20database.js index 5488ae5aa9..c295744c1d 100755 --- a/src/20database.js +++ b/src/20database.js @@ -67,6 +67,7 @@ var Database = (alasql.Database = function (databaseid) { Database.prototype.resetSqlCache = function () { this.sqlCache = {}; // Cache for compiled SQL statements this.sqlCacheSize = 0; + this.astCache = {}; // Cache for AST objects }; // Main SQL function From b97071f98d89cf09051882bc3d83e3a7a7536ef7 Mon Sep 17 00:00:00 2001 From: Paul Rutter Date: Wed, 25 Oct 2023 15:31:35 +0200 Subject: [PATCH 3/9] Performance improvements for AlaSQL - Adjust or skip unit tests --- src/17alasql.js | 47 +++++++++++++++++++++++++-------------------- src/20database.js | 1 + src/50expression.js | 7 +++---- src/61date.js | 1 + test/test202.js | 10 ++++++++-- test/test811.js | 2 +- test/test845.js | 5 ++++- 7 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/17alasql.js b/src/17alasql.js index 58bebe884e..1a1c47bd84 100755 --- a/src/17alasql.js +++ b/src/17alasql.js @@ -15,32 +15,32 @@ alasql.parser.parseError = function (str, hash) { }; /** - Jison parser - @param {string} sql SQL statement - @return {object} AST (Abstract Syntax Tree) + Jison parser + @param {string} sql SQL statement + @return {object} AST (Abstract Syntax Tree) - @todo Create class AST - @todo Add other parsers + @todo Create class AST + @todo Add other parsers - @example - alasql.parse = function(sql) { + @example + alasql.parse = function(sql) { // My own parser here - } + } */ alasql.parse = function (sql) { return alasqlparser.parse(alasql.utils.uncomment(sql)); }; /** - List of engines of external databases - @type {object} - @todo Create collection type + List of engines of external databases + @type {object} + @todo Create collection type */ alasql.engines = {}; /** - List of databases - @type {object} + List of databases + @type {object} */ alasql.databases = {}; @@ -51,7 +51,7 @@ alasql.databases = {}; alasql.databasenum = 0; /** - Alasql options object + Alasql options object */ alasql.options = { /** Log or throw error */ @@ -119,7 +119,7 @@ alasql.options = { /** Check for NaN and convert it to undefined */ nan: false, - excel: {cellDates: true}, + excel: { cellDates: true }, /** Option for SELECT * FROM a,b */ joinstar: 'overwrite', @@ -207,7 +207,7 @@ alasql.autoval = function (tablename, colname, getNext, databaseid) { return ( db.tables[tablename].identities[colname].value - - db.tables[tablename].identities[colname].step || null + db.tables[tablename].identities[colname].step || null ); }; @@ -247,6 +247,7 @@ alasql.dexec = function (databaseid, sql, params, cb, scope) { // console.log(3,db.databaseid,databaseid); var hh = hash(sql); + // Create hash if (alasql.options.cache) { var statement = db.sqlCache[hh]; @@ -256,13 +257,17 @@ alasql.dexec = function (databaseid, sql, params, cb, scope) { } } - // Create AST var ast = db.astCache[hh]; - if (!ast) { + if (alasql.options.cache && !ast) { + // Create AST cache ast = alasql.parse(sql); - // add to AST cache - db.astCache[hh]= ast; - } + if (ast) { + // add to AST cache + db.astCache[hh] = ast; + } + } else { + ast = alasql.parse(sql); + } if (!ast.statements) { return; } diff --git a/src/20database.js b/src/20database.js index c295744c1d..8bc3bef7a8 100755 --- a/src/20database.js +++ b/src/20database.js @@ -68,6 +68,7 @@ Database.prototype.resetSqlCache = function () { this.sqlCache = {}; // Cache for compiled SQL statements this.sqlCacheSize = 0; this.astCache = {}; // Cache for AST objects + this.setCache = {}; // Cache for WHERE (NOT) IN statements }; // Main SQL function diff --git a/src/50expression.js b/src/50expression.js index cad6484cd3..78854ba4f4 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -490,6 +490,7 @@ const allValues = this.right.map((value) => value.value); const allValuesStr = allValues.join(","); if (!alasql.sets[allValuesStr]) { + // leverage JS Set, which is faster for lookups than arrays alasql.sets[allValuesStr] = new Set(allValues); } s = 'alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; @@ -516,7 +517,7 @@ s += 'alasql.utils.flatArray(this.queriesfn[' + this.queriesidx + '](params,null,p))'; s += '.indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))<0)'; - } else if (Array.isArray(this.right) && this.right.every((value) => value.value)) { + } else if (Array.isArray(this.right)) { // Added patch to have a better performance for when you have a lot of entries in a NOT IN statement if (!alasql.sets) { alasql.sets = {}; @@ -524,12 +525,10 @@ const allValues = this.right.map((value) => value.value); const allValuesStr = allValues.join(","); if (!alasql.sets[allValuesStr]) { + // leverage JS Set, which is faster for lookups than arrays alasql.sets[allValuesStr] = new Set(allValues); } s = '!alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; - } else if (Array.isArray(this.right)) { - s = '([' + this.right.map(ref).join(',') + '].indexOf('; - s += 'alasql.utils.getValueOf(' + leftJS() + '))<0)'; } else { s = '(' + rightJS() + '.indexOf('; s += leftJS() + ')==-1)'; diff --git a/src/61date.js b/src/61date.js index 525e9d8672..b155bcbe76 100644 --- a/src/61date.js +++ b/src/61date.js @@ -43,6 +43,7 @@ stdfn.OBJECT_ID = function (objid) { }; stdfn.DATE = function (d) { + if (!isNaN(d) && d.length === 8) return new Date(+d.substr(0, 4), +d.substr(4, 2) - 1, +d.substr(6, 2)); return newDate(d); }; diff --git a/test/test202.js b/test/test202.js index c8f0db5228..ba713c01f3 100644 --- a/test/test202.js +++ b/test/test202.js @@ -6,14 +6,20 @@ if (typeof exports === 'object') { } describe('Test 202 GETTIME and CAST', function () { - it('1. GETDATE()', function (done) { + /** + * Why do we need the date to a string? Skipped test for now. + */ + it.skip('1. GETDATE()', function (done) { var res = alasql('SELECT ROW NOW(),GETDATE()'); // console.log(res); assert(res[0].substr(0, 20) == res[1].substr(0, 20)); done(); }); - it('2. CONVERT(,,110)', function (done) { + /** + * Why do we need the date to a string? Skipped test for now. + */ + it.skip('2. CONVERT(,,110)', function (done) { var res = alasql('SELECT VALUE CONVERT(NVARCHAR(10),GETDATE(),110)'); // console.log(res); assert(res.substr(-4) == new Date().getFullYear()); diff --git a/test/test811.js b/test/test811.js index 2e51806461..f9710dc22c 100644 --- a/test/test811.js +++ b/test/test811.js @@ -102,7 +102,7 @@ describe('Test 811 - String / Number objects', function () { }); it('5. Where In', function (done) { - var t1 = [{ID: new String('s1')}, {ID: new String('s2')}, {ID: new String('s3')}]; + var t1 = [{ID: "s1"}, {ID: "s2"}, {ID: "s3"}]; var res = alasql('SELECT * FROM ? WHERE ID IN("s1", "s3")', [t1]); diff --git a/test/test845.js b/test/test845.js index df7d3507cd..e159a58bb5 100644 --- a/test/test845.js +++ b/test/test845.js @@ -10,7 +10,10 @@ if (typeof exports === 'object') { var test = '845'; // insert test file number describe('Test ' + test + ' - use NOW() function', function () { - it('1. NOW()', function () { + /** + * Why do we need the date to a string? Skipped test for now. + */ + it.skip('1. NOW()', function () { var res = alasql('SELECT NOW() AS now'); //2022-02-25 19:21:27.839 assert(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/.test(res[0].now)); From 390d1e39e6861f5db78286604aa9453ec8efbd24 Mon Sep 17 00:00:00 2001 From: Paul Rutter Date: Wed, 25 Oct 2023 15:35:20 +0200 Subject: [PATCH 4/9] Performance improvements for AlaSQL - Remove cache property --- src/20database.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/20database.js b/src/20database.js index 8bc3bef7a8..c295744c1d 100755 --- a/src/20database.js +++ b/src/20database.js @@ -68,7 +68,6 @@ Database.prototype.resetSqlCache = function () { this.sqlCache = {}; // Cache for compiled SQL statements this.sqlCacheSize = 0; this.astCache = {}; // Cache for AST objects - this.setCache = {}; // Cache for WHERE (NOT) IN statements }; // Main SQL function From 52bc322efd19177157e5c1249afa5030bd64c799 Mon Sep 17 00:00:00 2001 From: Paul Rutter Date: Mon, 30 Oct 2023 10:50:13 +0100 Subject: [PATCH 5/9] Performance improvements for AlaSQL - Fix LIKE unit tests by not using toUpperCase() anymore; use same code as before, but now with a patternCache --- src/15utility.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/15utility.js b/src/15utility.js index eb370243b8..de833da0d8 100755 --- a/src/15utility.js +++ b/src/15utility.js @@ -1212,10 +1212,9 @@ var like = (utils.like = function (pattern, value, escape) { s += '$'; // if(value == undefined) return false; //console.log(s,value,(value||'').search(RegExp(s))>-1); - patternCache[pattern] = RegExp(s.toUpperCase()) + patternCache[pattern] = RegExp(s, 'i'); } - return ('' + (value || '')).toUpperCase().search(patternCache[pattern]) > -1; - + return ('' + (value ?? '')).search(patternCache[pattern]) > -1; }); utils.glob = function (value, pattern) { From be32ef515205300e69d6848bbf555224d2ffb3ee Mon Sep 17 00:00:00 2001 From: Paul Rutter Date: Mon, 30 Oct 2023 11:03:37 +0100 Subject: [PATCH 6/9] Performance improvements for AlaSQL - Always use SET, remove old code --- src/50expression.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/50expression.js b/src/50expression.js index 78854ba4f4..016238de48 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -482,7 +482,7 @@ '))'; s += '.indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))>-1)'; - } else if (Array.isArray(this.right) && this.right.every((value) => value.value)) { + } else if (Array.isArray(this.right)) { // Added patch to have a better performance for when you have a lot of entries in an IN statement if (!alasql.sets) { alasql.sets = {}; @@ -494,15 +494,6 @@ alasql.sets[allValuesStr] = new Set(allValues); } s = 'alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; - } else if (Array.isArray(this.right)) { - // if(this.right.length == 0) return 'false'; - s = - '([' + - this.right.map(ref).join(',') + - '].indexOf(alasql.utils.getValueOf(' + - leftJS() + - '))>-1)'; - //console.log(s); } else { s = '(' + rightJS() + '.indexOf(' + leftJS() + ')>-1)'; //console.log('expression',350,s); From b6ef4f8734dfd09d65872340f01e6b0424eec599 Mon Sep 17 00:00:00 2001 From: bc-paulrutter Date: Wed, 1 Nov 2023 14:33:54 +0100 Subject: [PATCH 7/9] Performance improvements for AlaSQL - Made returning Date objects configurable as option, to remain backwards compatible - Added unit test for Date objects in relevant test cases --- src/17alasql.js | 3 +++ src/61date.js | 18 ++++++++++++++++++ test/test202.js | 22 +++++++++++++--------- test/test845.js | 14 ++++++++++---- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/17alasql.js b/src/17alasql.js index 1a1c47bd84..199c489ded 100755 --- a/src/17alasql.js +++ b/src/17alasql.js @@ -125,6 +125,9 @@ alasql.options = { joinstar: 'overwrite', loopbreak: 100000, + + /** Whether GETDATE() and NOW() return dates as string. If false, then a Date object is returned */ + dateAsString: true, }; //alasql.options.worker = false; diff --git a/src/61date.js b/src/61date.js index b155bcbe76..a2e7316e0b 100644 --- a/src/61date.js +++ b/src/61date.js @@ -48,6 +48,24 @@ stdfn.DATE = function (d) { }; stdfn.NOW = function () { + if (alasql.options.dateAsString) { + var d = new Date(); + var s = + d.getFullYear() + + '-' + + ('0' + (d.getMonth() + 1)).substr(-2) + + '-' + + ('0' + d.getDate()).substr(-2); + s += + ' ' + + ('0' + d.getHours()).substr(-2) + + ':' + + ('0' + d.getMinutes()).substr(-2) + + ':' + + ('0' + d.getSeconds()).substr(-2); + s += '.' + ('00' + d.getMilliseconds()).substr(-3); + return s; + } return new Date(); }; diff --git a/test/test202.js b/test/test202.js index ba713c01f3..d9adeba073 100644 --- a/test/test202.js +++ b/test/test202.js @@ -6,20 +6,24 @@ if (typeof exports === 'object') { } describe('Test 202 GETTIME and CAST', function () { - /** - * Why do we need the date to a string? Skipped test for now. - */ - it.skip('1. GETDATE()', function (done) { + it('1a. GETDATE() as String', function (done) { var res = alasql('SELECT ROW NOW(),GETDATE()'); // console.log(res); - assert(res[0].substr(0, 20) == res[1].substr(0, 20)); + assert(res[0].substr(0, 20) === res[1].substr(0, 20)); done(); }); - /** - * Why do we need the date to a string? Skipped test for now. - */ - it.skip('2. CONVERT(,,110)', function (done) { + it('1b. GETDATE() as Date', function (done) { + alasql.options.dateAsString = false; + var res = alasql('SELECT ROW NOW(),GETDATE()'); + // console.log(res); + assert(res[0] instanceof Date); + assert(res[1] instanceof Date); + assert(res[1].toISOString() === res[0].toISOString()); + done(); + }); + + it('2. CONVERT(,,110) as String', function (done) { var res = alasql('SELECT VALUE CONVERT(NVARCHAR(10),GETDATE(),110)'); // console.log(res); assert(res.substr(-4) == new Date().getFullYear()); diff --git a/test/test845.js b/test/test845.js index e159a58bb5..1a6307642e 100644 --- a/test/test845.js +++ b/test/test845.js @@ -10,18 +10,24 @@ if (typeof exports === 'object') { var test = '845'; // insert test file number describe('Test ' + test + ' - use NOW() function', function () { - /** - * Why do we need the date to a string? Skipped test for now. - */ - it.skip('1. NOW()', function () { + + it('1a. NOW() as String', function () { var res = alasql('SELECT NOW() AS now'); //2022-02-25 19:21:27.839 assert(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/.test(res[0].now)); }); + it('1b. NOW() as Date', function () { + alasql.options.dateAsString = false; + var res = alasql('SELECT NOW() AS now'); + //2022-02-25 19:21:27.839 + assert(res[0].now instanceof Date); + }); + it('2. CONVERT with NOW() as an argument', function () { var res = alasql('SELECT CONVERT(STRING,NOW(),1) AS conv'); //02/25/22 assert(/\d{2}\/\d{2}\/\d{2}/.test(res[0].conv)); }); + }); From 547d17ed0c8fd39ebc8375f9db7d3cec4c4c944f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Thu, 9 Nov 2023 10:04:58 +0100 Subject: [PATCH 8/9] Performance improvements for AlaSQL - Fix using new String in IN statement - Add new test cases for using plain string literals in both IN and NOT IN --- src/50expression.js | 4 ++-- test/test811.js | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/50expression.js b/src/50expression.js index 016238de48..f60baa2250 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -493,7 +493,7 @@ // leverage JS Set, which is faster for lookups than arrays alasql.sets[allValuesStr] = new Set(allValues); } - s = 'alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; + s = 'alasql.sets["' + allValuesStr + '"].has(alasql.utils.getValueOf(' + leftJS() + '))'; } else { s = '(' + rightJS() + '.indexOf(' + leftJS() + ')>-1)'; //console.log('expression',350,s); @@ -519,7 +519,7 @@ // leverage JS Set, which is faster for lookups than arrays alasql.sets[allValuesStr] = new Set(allValues); } - s = '!alasql.sets["' + allValuesStr + '"].has(' + leftJS() + ')'; + s = '!alasql.sets["' + allValuesStr + '"].has(alasql.utils.getValueOf(' + leftJS() + '))'; } else { s = '(' + rightJS() + '.indexOf('; s += leftJS() + ')==-1)'; diff --git a/test/test811.js b/test/test811.js index f9710dc22c..dd766c23a3 100644 --- a/test/test811.js +++ b/test/test811.js @@ -101,12 +101,48 @@ describe('Test 811 - String / Number objects', function () { done(); }); - it('5. Where In', function (done) { + it('5a. Where In', function (done) { + var t1 = [{ID: new String("s1")}, {ID: new String("s2")}, {ID: new String("s3")}]; + + var res = alasql('SELECT * FROM ? WHERE ID IN("s1", "s3")', [t1]); + + assert.equal(res.length, 2); + assert.equal(res[0].ID, "s1"); + assert.equal(res[1].ID, "s3"); + + done(); + }); + + it('5b. Where In (literals)', function (done) { var t1 = [{ID: "s1"}, {ID: "s2"}, {ID: "s3"}]; var res = alasql('SELECT * FROM ? WHERE ID IN("s1", "s3")', [t1]); assert.equal(res.length, 2); + assert.equal(res[0].ID, "s1"); + assert.equal(res[1].ID, "s3"); + + done(); + }); + + it('5c. Where NOT In', function (done) { + var t1 = [{ID: new String("s1")}, {ID: new String("s2")}, {ID: new String("s3")}]; + + var res = alasql('SELECT * FROM ? WHERE ID NOT IN("s1", "s3")', [t1]); + + assert.equal(res.length, 1); + assert.equal(res[0].ID, "s2"); + + done(); + }); + + it('5d. Where NOT In (literals)', function (done) { + var t1 = [{ID: "s1"}, {ID: "s2"}, {ID: "s3"}]; + + var res = alasql('SELECT * FROM ? WHERE ID NOT IN("s1", "s3")', [t1]); + + assert.equal(res.length, 1); + assert.equal(res[0].ID, "s2"); done(); }); From e79527eae5ec76eb75a5eb977a2dcf5393cdab9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20R=C3=BCtter?= Date: Fri, 10 Nov 2023 18:30:20 +0100 Subject: [PATCH 9/9] https://github.com/AlaSQL/alasql/issues/1829 - Fix regression when using IN / NOT IN - Using refs didn't work anymore - Dropped set cache, as it's not really needed unless the same queries are performed often - Added unit test to guard behavior --- src/50expression.js | 34 +++++++++-------------- test/test1829.js | 66 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 test/test1829.js diff --git a/src/50expression.js b/src/50expression.js index f60baa2250..40b742141f 100755 --- a/src/50expression.js +++ b/src/50expression.js @@ -483,17 +483,12 @@ s += '.indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))>-1)'; } else if (Array.isArray(this.right)) { - // Added patch to have a better performance for when you have a lot of entries in an IN statement - if (!alasql.sets) { - alasql.sets = {}; - } - const allValues = this.right.map((value) => value.value); - const allValuesStr = allValues.join(","); - if (!alasql.sets[allValuesStr]) { - // leverage JS Set, which is faster for lookups than arrays - alasql.sets[allValuesStr] = new Set(allValues); - } - s = 'alasql.sets["' + allValuesStr + '"].has(alasql.utils.getValueOf(' + leftJS() + '))'; + // leverage JS Set, which is faster for lookups than arrays + s = '(new Set([' + + this.right.map(ref).join(',') + + ']).has(alasql.utils.getValueOf(' + + leftJS() + + ')))'; } else { s = '(' + rightJS() + '.indexOf(' + leftJS() + ')>-1)'; //console.log('expression',350,s); @@ -509,17 +504,12 @@ s += '.indexOf('; s += 'alasql.utils.getValueOf(' + leftJS() + '))<0)'; } else if (Array.isArray(this.right)) { - // Added patch to have a better performance for when you have a lot of entries in a NOT IN statement - if (!alasql.sets) { - alasql.sets = {}; - } - const allValues = this.right.map((value) => value.value); - const allValuesStr = allValues.join(","); - if (!alasql.sets[allValuesStr]) { - // leverage JS Set, which is faster for lookups than arrays - alasql.sets[allValuesStr] = new Set(allValues); - } - s = '!alasql.sets["' + allValuesStr + '"].has(alasql.utils.getValueOf(' + leftJS() + '))'; + // leverage JS Set, which is faster for lookups than arrays + s = '(!(new Set([' + + this.right.map(ref).join(',') + + ']).has(alasql.utils.getValueOf(' + + leftJS() + + '))))'; } else { s = '(' + rightJS() + '.indexOf('; s += leftJS() + ')==-1)'; diff --git a/test/test1829.js b/test/test1829.js new file mode 100644 index 0000000000..dbcb3c7733 --- /dev/null +++ b/test/test1829.js @@ -0,0 +1,66 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} else { + __dirname = '.'; +} + +describe('Test 1829 - WHERE (NOT) IN Regression when using refs', function () { + beforeEach(function () { + alasql(`CREATE TABLE test1829 ( + id varchar(50) NOT NULL, + text varchar(10) NOT NULL, + PRIMARY KEY (id) + )`); + }); + + afterEach(function () { + alasql('DROP TABLE test1829'); + }); + + it('1. Where IN with refs', function (done) { + const rowId1 = 'id#1'; + const rowId2 = 'id#2'; + + alasql('insert into test1829(id, text) values (?, ?)', [ + rowId1, + 'first text', + ]); + alasql('insert into test1829(id, text) values (?, ?)', [ + rowId2, + 'second text', + ]); + + const selectedByIdRows = alasql(`select entity.id, entity.text from test1829 as entity where entity.id IN (?,?)`, [ + rowId1, + rowId2 + ]); + assert.equal(selectedByIdRows.length, 2); + assert.equal(selectedByIdRows[0].id, rowId1); + assert.equal(selectedByIdRows[1].id, rowId2); + + done(); + }); + + it('2. Where NOT IN with refs', function (done) { + const rowId1 = 'id#1'; + const rowId2 = 'id#2'; + + alasql('insert into test1829(id, text) values (?, ?)', [ + rowId1, + 'first text', + ]); + alasql('insert into test1829(id, text) values (?, ?)', [ + rowId2, + 'second text', + ]); + + const selectedByIdRows = alasql(`select entity.id, entity.text from test1829 as entity where entity.id NOT IN (?)`, [ + rowId1 + ]); + assert.equal(selectedByIdRows.length, 1); + assert.equal(selectedByIdRows[0].id, rowId2); + done(); + }); + +});