diff --git a/src/10start.js b/src/10start.js index b0a7a17833..f7373707b0 100644 --- a/src/10start.js +++ b/src/10start.js @@ -2,6 +2,24 @@ "use strict"; +const isBacktickQuery = (arg) => Array.isArray(arg.raw); + +const formatQueryParams = (params) => (queryStr, index) => { + const param = params[index + 1]; + return queryStr + (typeof param === 'undefined' ? '' : param); +}; + +const normalizeBacktickQuery = (args) => { + const stringFormatted = args[0] + .map(formatQueryParams(args)) + .join('') + // Remove breakline in case of characters in same line | optional + .replace(/[\r\n]/g, '') + .replace(/\s+/g, ' ') // Remove extras + .trim(); // Remove extras + return stringFormatted; +}; + /** @fileoverview AlaSQL JavaScript SQL library @see http://github.com/alasql/alasql @@ -57,7 +75,9 @@ alasql().From(data).Where(function(x){return x.a == 10}).exec(); */ -var alasql = function(sql, params, cb, scope) { +var alasql = function(...args) { + var [sqlQuery, params, cb, scope] = args; + var sql = isBacktickQuery(sqlQuery) ? normalizeBacktickQuery(args) : sqlQuery; params = params||[]; diff --git a/src/17alasql.js b/src/17alasql.js index 3fd59e1dba..3d1af8caee 100755 --- a/src/17alasql.js +++ b/src/17alasql.js @@ -27,8 +27,8 @@ alasql.parser.parseError = function (str, hash) { // My own parser here } */ -alasql.parse = function (sql) { - return alasqlparser.parse(alasql.utils.uncomment(sql)); +alasql.parse = function (command) { + return alasqlparser.parse(alasql.utils.uncomment(command)); }; /** diff --git a/test/test000.js b/test/test000.js index 63894b3fc2..a756fc2cd1 100644 --- a/test/test000.js +++ b/test/test000.js @@ -3,20 +3,21 @@ if (typeof exports === 'object') { var alasql = require('..'); } -describe('Test 000 - multiple statements', function () { - const test = '000'; // insert test file number +// Please consider using the github issue number as test ID +describe(`Test 000 - multiple statements`, function () { + const testID = '000'; - before(function () { - alasql('create database test' + test); - alasql('use test' + test); + before(function () { + alasql('create database test' + testID); + alasql('use test' + testID); }); after(function () { - alasql('drop database test' + test); + alasql('drop database test' + testID); }); it('A) From single lines', function () { - var res = []; + const res = []; res.push(alasql('create table one (a int)')); res.push(alasql('insert into one values (1),(2),(3),(4),(5)')); res.push(alasql('select * from one')); @@ -25,19 +26,19 @@ describe('Test 000 - multiple statements', function () { it('B) Multiple statements in one string', function () { // - var sql = 'create table two (a int);'; + let sql = 'create table two (a int);'; sql += 'insert into two values (1),(2),(3),(4),(5);'; sql += 'select * from two;'; - var res = alasql(sql); + let res = alasql(sql); assert.deepEqual(res, [1, 5, [{a: 1}, {a: 2}, {a: 3}, {a: 4}, {a: 5}]]); }); it('C) Multiple statements in one string with callback', function (done) { // Please note that first parameter (here `done`) must be called if defined - and is needed when testing async code - var sql = 'create table three (a int);'; + let sql = 'create table three (a int);'; sql += 'insert into three values (1),(2),(3),(4),(5);'; sql += 'select * from three;'; - alasql(sql, function (res) { + alasql.promise(sql).then(function (res) { assert.deepEqual(res, [1, 5, [{a: 1}, {a: 2}, {a: 3}, {a: 4}, {a: 5}]]); done(); }); diff --git a/test/test1723a.js b/test/test1723a.js new file mode 100644 index 0000000000..a1a4628d40 --- /dev/null +++ b/test/test1723a.js @@ -0,0 +1,161 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('../dist/alasql'); +} + +describe('Issue #1723 - tagFunction for template strings', function () { + it('Will mark free fields as parameters', function (done) { + assert.deepEqual(tagBraid`SELECT 123 as abc`, ['SELECT 123 as abc']); + assert.deepEqual(tagBraid`SELECT ${123} as abc`, ['SELECT ? as abc', [123]]); + assert.deepEqual(tagBraid`${'SELECT'} ${123} as abc`, ['? ? as abc', ['SELECT', 123]]); + assert.deepEqual(tagBraid`${'SELECT'} ${123} as ${'abc'}`, [ + '? ? as ?', + ['SELECT', 123, 'abc'], + ]); + done(); + }); + + it('Will work second time when data is fetched from the cache', function (done) { + assert.deepEqual(tagBraid`SELECT 123 as abc`, ['SELECT 123 as abc']); + assert.deepEqual(tagBraid`SELECT ${123} as abc`, ['SELECT ? as abc', [123]]); + assert.deepEqual(tagBraid`${'SELECT'} ${123} as abc`, ['? ? as abc', ['SELECT', 123]]); + assert.deepEqual(tagBraid`${'SELECT'} ${123} as ${'abc'}`, [ + '? ? as ?', + ['SELECT', 123, 'abc'], + ]); + done(); + }); + + it('Will inline connected fields', function (done) { + assert.deepEqual(tagBraid`S${'ELECT'} 1${23} as ab${'c'}`, ['SELECT 123 as abc', []]); + assert.deepEqual(tagBraid`SELECT 123 as ${'ab'}${'c'}`, ['SELECT 123 as abc', []]); + done(); + }); + + it('Will treat "()," as free space and become parameter', function (done) { + assert.deepEqual(tagBraid`SELECT AVG(${1},${2},${3}) as abc`, [ + 'SELECT AVG(?,?,?) as abc', + [1, 2, 3], + ]); + done(); + }); + + it('Can force free fields as inline', function (done) { + assert.deepEqual(tagBraid`~${'SELECT'} ~${123} as abc`, ['SELECT 123 as abc', []]); + assert.deepEqual(tagBraid`~${'SELECT'} ~${123} as ${'abc'}`, ['SELECT 123 as ?', ['abc']]); + assert.deepEqual(tagBraid`${'SELECT'} ${123} as ~${'abc'}`, ['? ? as abc', ['SELECT', 123]]); + assert.deepEqual(tagBraid`${'SELECT'} ${123} as ~${'abc'}~`, ['? ? as ~abc~', ['SELECT', 123]]); + assert.deepEqual(tagBraid`SELECT AVG(~${1},~${2},${3}) as abc`, [ + 'SELECT AVG(1,2,?) as abc', + [3], + ]); + done(); + }); + + it('Default to markring as parameter (option B from PR #1512)', function (done) { + let items = `toys`; + let type = 'Montessori'; + let item = 'batman'; + let orderBy = `ORDER BY x desc, y asc`; + + let res = tagBraid` +SELECT author +FROM ${items} +WHERE +AND type = ${type}_v2 +AND name = ${item} +~${orderBy} +`; + + let expected = ` +SELECT author +FROM ? +WHERE +AND type = Montessori_v2 +AND name = ? +ORDER BY x desc, y asc +`; + assert.deepEqual(res, [expected, [`toys`, 'batman']]); + done(); + }); + + /*it('Will return a promise', async function (done) { + let res = alasql`SELECT 123`; + assert(typeof res.then === 'function'); + assert.equal(await alasql`SELECT 123`.then((x) => 555), 555); + done(); + }); + + it('Will return the data from the query', async function (done) { + assert.equal(await alasql`VAlUE OF SELECT 123`, 123); + assert.deepEqual(await alasql`SELECT 123 as abc`, [{abc: 123}]); + done(); + }); + + it('Will inline string connected to other areas', async function (done) { + assert.deepEqual(await alasql`SELECT 123 as abc`, [{abc: 123}]); + done(); + });*/ +}); + +const re = { + preFree: /[\(,\s]~?$/, + postFree: /^[\),\s]/, +}; + +const cache = new Map(); + +function tagBraid(template, ...params) { + if ( + !Array.isArray(template) || + !Array.isArray(template.raw) || + template.length - 1 != params.length + ) + throw 'Please use as tagfunction to provide the right arguments'; + + if (1 == template.length) return [template[0]]; + + let sql = ''; + + let paramsIDs = []; + if (cache[template.raw]) { + ({sql, paramsIDs} = cache.get(template.raw)); + } else { + for (let i = 0; i <= params.length; i++) { + sql += template[i]; + + if (i === params.length) break; + + let inline = true; + + // if the field is "free" and not connected to other texts + if ( + (re.preFree.test(template[i]) || + (0 === i && ('' === template[i] || '~' === template[i]))) && + (re.postFree.test(template[i + 1]) || (params.length - 1 === i && '' === template[i + 1])) + ) { + inline = false; + // force inline if prepended with ~ + if ('~'.charCodeAt(0) === template[i].charCodeAt(template[i].length - 1)) { + sql = sql.slice(0, -1); + inline = true; + } + } + + if (inline) { + if (typeof params[i] !== 'number' && typeof params[i] !== 'string') + console.error( + 'You are inlining a value that is not a string or a number so it might not work. Will proceed with the .toString() value but consider making space around the value so it can be provided as a parameter.', + {parameter: params[i], template: template.raw} + ); + sql += params[i].toString(); + } else { + sql += '?'; + paramsIDs.push(i); + } + } + cache.set(template.raw, {sql, paramsIDs}); + } + + return [sql, [...paramsIDs.map((x) => params[x])]]; +} diff --git a/test/test1723b.js b/test/test1723b.js new file mode 100644 index 0000000000..d9ecb93431 --- /dev/null +++ b/test/test1723b.js @@ -0,0 +1,60 @@ +if (typeof exports === 'object') { + var alasql = require('../dist/alasql'); +} + +const table = { + name: 'midnightcalls', + columns: [ + {name: 'track_name', type: 'string'}, + {name: 'author', type: 'string'}, + {name: 'views', type: 'int'}, + ], +}; + +describe('Issue #1723 - Testing backtick call function', function () { + it('1. Create table', function () { + alasql`DROP TABLE IF EXISTS test`; + alasql`CREATE TABLE test (a int, b int)`; + }); + + it('2. Insert values ', function () { + alasql`INSERT INTO test VALUES (1,1)`; + alasql`INSERT INTO test VALUES (1,7)`; + alasql`INSERT INTO test VALUES (2,2)`; + alasql`INSERT INTO test VALUES (3,3)`; + }); + + it('3. Create a new table', function () { + alasql`DROP TABLE IF EXISTS ${table.name}`; + + alasql(` + CREATE TABLE ${table.name} (${table.columns + .map((item) => ` ${item.name} ${item.type.toUpperCase()}`) + .join(', ') + .toString()}) + `); + }); + + it('4. Insert values', function () { + const values = [ + ['qhAfaWdLbIE', 'Baby bi', 'Yunk Vino', 72], + ['YA-db3f8Ak4', 'Sonar', 'Yunk Vino', 809], + ]; + const valuesToInsert = values + .map( + (item, i) => + `('${item[0]}', '${item[1]}', '${item[2]}', ${item[3]})${ + i + 1 === values.length ? '' : ', ' + }` + ) + .join(''); + + //console.log(valuesToInsert); + + alasql(` + INSERT INTO ${table.name} + VALUES + ${valuesToInsert} + `); + }); +});