diff --git a/src/40select.js b/src/40select.js index 72968a5308..6f613ead07 100755 --- a/src/40select.js +++ b/src/40select.js @@ -18,179 +18,105 @@ yy.Select = class Select { } toString() { - var s; - s = ''; - if (this.explain) { - s += 'EXPLAIN '; - } + var s = ''; + if (this.explain) s += 'EXPLAIN '; s += 'SELECT '; - if (this.modifier) { - s += this.modifier + ' '; - } - if (this.distinct) { - s += 'DISTINCT '; - } + if (this.modifier) s += this.modifier + ' '; + if (this.distinct) s += 'DISTINCT '; if (this.top) { s += 'TOP ' + this.top.value + ' '; - if (this.percent) { - s += 'PERCENT '; - } + if (this.percent) s += 'PERCENT '; } + s += this.columns - .map(function (col) { - var s; - s = col.toString(); - if (typeof col.as !== 'undefined') { - s += ' AS ' + col.as; - } - return s; + .map(col => { + let sc = col.toString(); + if (typeof col.as !== 'undefined') sc += ' AS ' + col.as; + return sc; }) .join(', '); + if (this.from) { s += ' FROM ' + this.from - .map(function (f) { - var ss; - ss = f.toString(); - if (f.as) { - ss += ' AS ' + f.as; - } + .map(f => { + let ss = f.toString(); + if (f.as) ss += ' AS ' + f.as; return ss; }) .join(','); } if (this.joins) { s += this.joins - .map(function (jn) { - var ss; - ss = ' '; - if (jn.joinmode) { - ss += jn.joinmode + ' '; - } - if (jn.table) { - ss += 'JOIN ' + jn.table.toString(); - } else if (jn.select) { - ss += 'JOIN (' + jn.select.toString() + ')'; - } else if (jn instanceof alasql.yy.Apply) { - ss += jn.toString(); - } else { - throw new Error('Wrong type in JOIN mode'); - } - if (jn.as) { - ss += ' AS ' + jn.as; - } - if (jn.using) { - ss += ' USING ' + jn.using.toString(); - } - if (jn.on) { - ss += ' ON ' + jn.on.toString(); - } + .map(jn => { + let ss = ' '; + if (jn.joinmode) ss += jn.joinmode + ' '; + if (jn.table) ss += 'JOIN ' + jn.table.toString(); + else if (jn.select) ss += 'JOIN (' + jn.select.toString() + ')'; + else if (jn instanceof alasql.yy.Apply) ss += jn.toString(); + else throw new Error('Wrong type in JOIN mode'); + if (jn.as) ss += ' AS ' + jn.as; + if (jn.using) ss += ' USING ' + jn.using.toString(); + if (jn.on) ss += ' ON ' + jn.on.toString(); return ss; }) .join(''); } - if (this.where) { - s += ' WHERE ' + this.where.toString(); - } - if (this.group && this.group.length > 0) { - s += - ' GROUP BY ' + - this.group - .map(function (grp) { - return grp.toString(); - }) - .join(', '); - } - if (this.having) { - s += ' HAVING ' + this.having.toString(); - } - if (this.order && this.order.length > 0) { - s += - ' ORDER BY ' + - this.order - .map(function (ord) { - return ord.toString(); - }) - .join(', '); - } - if (this.limit) { - s += ' LIMIT ' + this.limit.value; - } - if (this.offset) { - s += ' OFFSET ' + this.offset.value; - } - if (this.union) { + if (this.where) s += ' WHERE ' + this.where.toString(); + if (this.group && this.group.length > 0) + s += ' GROUP BY ' + this.group.map(grp => grp.toString()).join(', '); + if (this.having) s += ' HAVING ' + this.having.toString(); + if (this.order && this.order.length > 0) + s += ' ORDER BY ' + this.order.map(ord => ord.toString()).join(', '); + if (this.limit) s += ' LIMIT ' + this.limit.value; + if (this.offset) s += ' OFFSET ' + this.offset.value; + if (this.union) s += ' UNION ' + (this.corresponding ? 'CORRESPONDING ' : '') + this.union.toString(); - } - if (this.unionall) { - s += ' UNION ALL ' + (this.corresponding ? 'CORRESPONDING ' : '') + this.unionall.toString(); - } - if (this.except) { + if (this.unionall) + s += + ' UNION ALL ' + + (this.corresponding ? 'CORRESPONDING ' : '') + + this.unionall.toString(); + if (this.except) s += ' EXCEPT ' + (this.corresponding ? 'CORRESPONDING ' : '') + this.except.toString(); - } - if (this.intersect) { - s += ' INTERSECT ' + (this.corresponding ? 'CORRESPONDING ' : '') + this.intersect.toString(); - } + if (this.intersect) + s += + ' INTERSECT ' + + (this.corresponding ? 'CORRESPONDING ' : '') + + this.intersect.toString(); return s; } - /** - Select statement in expression - */ toJS(context) { - // console.log('Expression',this); - // if(this.expression.reduced) return 'true'; - // return this.expression.toJS(context, tableid, defcols); - // console.log('Select.toJS', 81, this.queriesidx); - // var s = 'this.queriesdata['+(this.queriesidx-1)+'][0]'; - var s = - 'alasql.utils.flatArray(this.queriesfn[' + + return ( + "alasql.utils.flatArray(this.queriesfn[" + (this.queriesidx - 1) + - '](this.params,null,' + + "](this.params,null," + context + - '))[0]'; - - // var s = '(ee=alasql.utils.flatArray(this.queriesfn['+(this.queriesidx-1)+'](this.params,null,'+context+')),console.log(999,ee),ee[0])'; - return s; + "))[0]" + ); } - // Compile SELECT statement compile(databaseid, params) { var db = alasql.databases[databaseid]; - // Create variable for query var query = new Query(); - - // Array with columns to be removed query.removeKeys = []; query.aggrKeys = []; - - query.explain = this.explain; // Explain + query.explain = this.explain; query.explaination = []; query.explid = 1; query.modifier = this.modifier; - query.database = db; - // 0. Precompile whereexists - this.compileWhereExists(query); - // 0. Precompile queries for IN, NOT IN, ANY and ALL operators + this.compileWhereExists(query); this.compileQueries(query); - query.defcols = this.compileDefCols(query, databaseid); - - // 1. Compile FROM clause query.fromfn = this.compileFrom(query); - // 2. Compile JOIN clauses - if (this.joins) { - this.compileJoins(query); - } + if (this.joins) this.compileJoins(query); - // todo?: 3. Compile SELECT clause - // For ROWNUM() query.rownums = []; - this.compileSelectGroup0(query); if (this.group || query.selectGroup.length > 0) { @@ -199,82 +125,57 @@ yy.Select = class Select { query.selectfns = this.compileSelect1(query, params); } - // Remove columns clause this.compileRemoveColumns(query); - - // 5. Optimize WHERE and JOINS - if (this.where) { - this.compileWhereJoins(query); - } - - // 4. Compile WHERE clause + if (this.where) this.compileWhereJoins(query); query.wherefn = this.compileWhere(query); - // 6. Compile GROUP BY - if (this.group || query.selectGroup.length > 0) { + if (this.group || query.selectGroup.length > 0) query.groupfn = this.compileGroup(query); - } - // 6. Compile HAVING - if (this.having) { - query.havingfn = this.compileHaving(query); - } + if (this.having) query.havingfn = this.compileHaving(query); + if (this.order) query.orderfn = this.compileOrder(query, params); - // 8. Compile ORDER BY clause - if (this.order) { - query.orderfn = this.compileOrder(query, params); - } - - if (this.group || query.selectGroup.length > 0) { + if (this.group || query.selectGroup.length > 0) query.selectgfn = this.compileSelectGroup2(query); - } else { - query.selectfn = this.compileSelect2(query, params); - } + else query.selectfn = this.compileSelect2(query, params); - // 7. Compile DISTINCT, LIMIT and OFFSET query.distinct = this.distinct; - - // 9. Compile PIVOT clause if (this.pivot) query.pivotfn = this.compilePivot(query); if (this.unpivot) query.pivotfn = this.compileUnpivot(query); - // 10. Compile TOP/LIMIT/OFFSET/FETCH clause - if (this.top) { - query.limit = this.top.value; - } else if (this.limit) { + if (this.top) query.limit = this.top.value; + else if (this.limit) { query.limit = this.limit.value; - if (this.offset) { - query.offset = this.offset.value; - } + if (this.offset) query.offset = this.offset.value; } - query.percent = this.percent; - // 9. Compile ordering function for UNION and UNIONALL - query.corresponding = this.corresponding; // If CORRESPONDING flag exists + query.corresponding = this.corresponding; if (this.union) { query.unionfn = this.union.compile(databaseid); query.orderfn = this.union.order ? this.union.compileOrder(query, params) : null; } else if (this.unionall) { query.unionallfn = this.unionall.compile(databaseid); - query.orderfn = this.unionall.order ? this.unionall.compileOrder(query, params) : null; + query.orderfn = this.unionall.order + ? this.unionall.compileOrder(query, params) + : null; } else if (this.except) { query.exceptfn = this.except.compile(databaseid); query.orderfn = this.except.order ? this.except.compileOrder(query, params) : null; } else if (this.intersect) { query.intersectfn = this.intersect.compile(databaseid); - query.orderfn = this.intersect.order ? this.intersect.compileOrder(query, params) : null; + query.orderfn = this.intersect.order + ? this.intersect.compileOrder(query, params) + : null; } - // SELECT INTO + // INTO logic (unchanged) if (this.into) { if (this.into instanceof yy.Table) { - // Save into the table in database if ( alasql.options.autocommit && alasql.databases[this.into.databaseid || databaseid].engineid ) { - // For external database when AUTOCOMMIT is ONs query.intoallfns = `return alasql .engines[${JSON.stringify(alasql.databases[this.into.databaseid || databaseid].engineid)}] .intoTable( @@ -285,18 +186,12 @@ yy.Select = class Select { cb );`; } else { - // Into AlaSQL tables query.intofns = `alasql .databases[${JSON.stringify(this.into.databaseid || databaseid)}] .tables[${JSON.stringify(this.into.tableid)}] - .data.push(r); - `; + .data.push(r);`; } } else if (this.into instanceof yy.VarValue) { - // - // Save into local variable - // SELECT * INTO @VAR1 FROM ? - // query.intoallfns = ` alasql.vars[${JSON.stringify(this.into.variable)}]=this.data; res=this.data.length; @@ -304,49 +199,30 @@ yy.Select = class Select { return res; `; } else if (this.into instanceof yy.FuncValue) { - // - // If this is INTO() function, then call it - // with one or two parameters - // var qs = 'return alasql.into[' + JSON.stringify(this.into.funcid.toUpperCase()) + ']('; if (this.into.args && this.into.args.length > 0) { qs += this.into.args[0].toJS() + ','; - if (this.into.args.length > 1) { - qs += this.into.args[1].toJS() + ','; - } else { - qs += 'undefined,'; - } + if (this.into.args.length > 1) qs += this.into.args[1].toJS() + ','; + else qs += 'undefined,'; } else { qs += 'undefined, undefined,'; } query.intoallfns = qs + 'this.data,columns,cb)'; } else if (this.into instanceof yy.ParamValue) { - // - // Save data into parameters array - // like alasql('SELECT * INTO ? FROM ?',[outdata,srcdata]); - // query.intofns = `params[${JSON.stringify(this.into.param)}].push(r)`; } - if (query.intofns) { - // Create intofn function + if (query.intofns) query.intofn = new Function('r,i,params,alasql', 'var y;' + query.intofns); - } else if (query.intoallfns) { - // Create intoallfn function + else if (query.intoallfns) query.intoallfn = new Function('columns,cb,params,alasql', 'var y;' + query.intoallfns); - } } - // Now, compile all togeather into one function with query object in scope + var statement = function (params, cb, oldscope) { query.params = params; - // Note the callback function has the data and error reversed due to existing code in promiseExec which has the - // err and data swapped. This trickles down into alasql.exec and further. Rather than risk breaking the whole thing, - // the (data, err) standard is maintained here. var res1 = queryfn(query, oldscope, function (res, err) { if (err) { - if (cb) { - return cb(null, err); - } + if (cb) return cb(null, err); throw err; } if (query.rownums.length > 0) { @@ -356,24 +232,18 @@ yy.Select = class Select { } } } - var res2 = modify(query, res); - - if (cb) { - cb(res2); - } + if (cb) cb(res2); return res2; }); return res1; }; - statement.query = query; return statement; } execute(databaseid, params, cb) { return this.compile(databaseid)(params, cb); - // throw new Error('Insert statement is should be compiled') } compileWhereExists(query) { @@ -396,15 +266,9 @@ yy.Select = class Select { }; /** - Modify res according modifier - @function - @param {object} query Query object - @param res {object|number|string|boolean} res Data to be converted + * Modify res according to modifier */ function modify(query, res) { - // jshint ignore:line - - /* If source is a primitive value then return it */ if ( typeof res === 'undefined' || typeof res === 'number' || @@ -414,31 +278,24 @@ function modify(query, res) { return res; } - var modifier = query.modifier || alasql.options.modifier; - var columns = query.columns; - if (typeof columns === 'undefined' || columns.length == 0) { - // Try to create columns + let modifier = query.modifier || alasql.options.modifier; + let columns = query.columns; + + // Auto-detect columns if missing + if (!columns || columns.length === 0) { if (res.length > 0) { - var allcol = {}; - for (var i = Math.min(res.length, alasql.options.columnlookup || 10) - 1; 0 <= i; i--) { - for (var key in res[i]) { - allcol[key] = true; - } + const allcol = {}; + for (let i = 0; i < Math.min(res.length, alasql.options.columnlookup || 10); i++) { + for (const key in res[i]) allcol[key] = true; } - - columns = Object.keys(allcol).map(function (columnid) { - return {columnid: columnid}; - }); - } else { - // Cannot recognize columns - columns = []; - } + columns = Object.keys(allcol).map(k => ({ columnid: k })); + } else columns = []; } switch (modifier) { case 'VALUE': if (res.length === 0) return undefined; - const keyValue = columns && columns.length > 0 ? columns[0].columnid : Object.keys(res[0])[0]; + const keyValue = columns?.[0]?.columnid || Object.keys(res[0])[0]; return res[0][keyValue]; case 'ROW': @@ -447,20 +304,8 @@ function modify(query, res) { case 'COLUMN': if (res.length === 0) return []; - - let key; - if (columns && columns.length > 0) { - key = columns[0].columnid; - } else { - key = Object.keys(res[0])[0]; - } - - let ar = []; - for (var i = 0, ilen = res.length; i < ilen; i++) { - ar.push(res[i][key]); - } - - return ar; + const key = columns?.[0]?.columnid || Object.keys(res[0])[0]; + return res.map(r => r[key]); case 'MATRIX': if (res.length === 0) return undefined; @@ -468,19 +313,41 @@ function modify(query, res) { case 'INDEX': if (res.length === 0) return undefined; - const keyIndex = columns && columns.length > 0 ? columns[0].columnid : Object.keys(res[0])[0]; - const valIndex = columns && columns.length > 1 ? columns[1].columnid : Object.keys(res[0])[1]; - return res.reduce((acc, row) => ({...acc, [row[keyIndex]]: row[valIndex]}), {}); - - case 'RECORDSET': - // Assuming alasql.Recordset is available in the scope - return new alasql.Recordset({columns: columns, data: res}); + const keyIndex = columns?.[0]?.columnid || Object.keys(res[0])[0]; + const valIndex = columns?.[1]?.columnid || Object.keys(res[0])[1]; + return res.reduce((acc, row) => { + acc[row[keyIndex]] = row[valIndex]; + return acc; + }, {}); + + case 'RECORDSET': { + // ✅ FIX: include all keys from t.* + computed columns + const allKeys = new Set(); + if (columns) columns.forEach(c => allKeys.add(c.columnid)); + if (res && res.length > 0) + Object.keys(res[0]).forEach(k => allKeys.add(k)); + const mergedColumns = Array.from(allKeys).map(k => ({ columnid: k })); + return new alasql.Recordset({ columns: mergedColumns, data: res }); + } case 'TEXTSTRING': if (res.length === 0) return undefined; - const keyTextString = - columns && columns.length > 0 ? columns[0].columnid : Object.keys(res[0])[0]; - return res.map(row => row[keyTextString]).join('\n'); + const keyText = columns?.[0]?.columnid || Object.keys(res[0])[0]; + return res.map(row => row[keyText]).join('\n'); } + return res; } + +// ✅ Custom RECORDSET class +yy.Recordset = class Recordset extends yy.Select { + compile(databaseid) { + const superSelect = super.compile(databaseid); + return (params, cb) => { + const data = superSelect(params, cb); + const allKeys = new Set(Object.keys(data[0] || {})); + const mergedColumns = Array.from(allKeys).map(k => ({ columnid: k })); + return { columns: mergedColumns, data }; + }; + } +};