Skip to content

Commit 676798b

Browse files
Copilotmathiasrw
andauthored
Let INTO OBJECT() output nested JSON objects from arrow notation to close #1278 (#2276)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mathiasrw <1063454+mathiasrw@users.noreply.github.com> Co-authored-by: Mathias Wulff <m@rawu.dk>
1 parent b891e4f commit 676798b

File tree

6 files changed

+383
-188
lines changed

6 files changed

+383
-188
lines changed

README.md

Lines changed: 162 additions & 167 deletions
Large diffs are not rendered by default.

src/38query.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function queryfn(query, oldscope, cb, A, B) {
3131
var rs = source.datafn(query, query.params, queryfn2, idx, alasql);
3232
if (typeof rs !== 'undefined') {
3333
// TODO - this is a hack: check if result is array - check all cases and make it more logical
34-
if ((query.intofn || query.intoallfn) && Array.isArray(rs)) {
34+
if ((query.intofn || query.intoallfn) && Array.isArray(rs) && !query.preserveArrayResult) {
3535
rs = rs.length;
3636
}
3737
result = rs;

src/40select.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ yy.Select = class Select {
192192
query.rownums = [];
193193
query.grouprownums = [];
194194

195+
// Check if INTO OBJECT() is used - this affects how arrow expressions are compiled
196+
if (this.into instanceof yy.FuncValue && this.into.funcid.toUpperCase() === 'OBJECT') {
197+
query.intoObject = true;
198+
}
199+
195200
this.compileSelectGroup0(query);
196201

197202
if (this.group || query.selectGroup.length > 0) {
@@ -323,7 +328,8 @@ yy.Select = class Select {
323328
// If this is INTO() function, then call it
324329
// with one or two parameters
325330
//
326-
var qs = 'return alasql.into[' + JSON.stringify(this.into.funcid.toUpperCase()) + '](';
331+
var funcid = this.into.funcid.toUpperCase();
332+
var qs = 'return alasql.into[' + JSON.stringify(funcid) + '](';
327333
if (this.into.args && this.into.args.length > 0) {
328334
qs += this.into.args[0].toJS() + ',';
329335
if (this.into.args.length > 1) {
@@ -335,6 +341,10 @@ yy.Select = class Select {
335341
qs += 'undefined, undefined,';
336342
}
337343
query.intoallfns = qs + 'this.data,columns,cb)';
344+
// Mark that OBJECT should preserve array results
345+
if (funcid === 'OBJECT') {
346+
query.preserveArrayResult = true;
347+
}
338348
} else if (this.into instanceof yy.ParamValue) {
339349
//
340350
// Save data into parameters array

src/424select.js

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,34 @@ function compileSelectStar(query, aliases, joinstar) {
113113
return {s: ss.join(','), sp: sp};
114114
}
115115

116+
// Helper function to check if an expression is an arrow operation and extract its path
117+
// Returns null if not an arrow op, or an array of path parts if it is
118+
function getArrowPath(expr) {
119+
if (!expr || expr.op !== '->') {
120+
return null;
121+
}
122+
var path = [];
123+
var current = expr;
124+
while (current && current.op === '->') {
125+
// The right side is the property name
126+
if (typeof current.right === 'string') {
127+
path.unshift(current.right);
128+
} else if (typeof current.right === 'number') {
129+
path.unshift(current.right);
130+
} else {
131+
// Complex expression on right side, can't extract path
132+
return null;
133+
}
134+
current = current.left;
135+
}
136+
// The leftmost should be a column
137+
if (current && current.columnid) {
138+
path.unshift(current.columnid);
139+
return path;
140+
}
141+
return null;
142+
}
143+
116144
yy.Select.prototype.compileSelect1 = function (query, params) {
117145
var self = this;
118146
query.columns = [];
@@ -326,26 +354,63 @@ yy.Select.prototype.compileSelect1 = function (query, params) {
326354
// }
327355
} else {
328356
// console.log(203,col.as,col.columnid,col.toString());
329-
ss.push(
330-
"'" +
331-
escapeq(col.as || col.columnid || col.toString()) +
332-
"':" +
333-
n2u(col.toJS('p', query.defaultTableid, query.defcols))
334-
);
335-
// ss.push('\''+escapeq(col.toString())+'\':'+col.toJS("p",query.defaultTableid));
336-
//if(col instanceof yy.Expression) {
337-
query.selectColumns[escapeq(col.as || col.columnid || col.toString())] = true;
357+
// Check if this is an arrow expression and we're outputting to OBJECT
358+
var arrowPath = query.intoObject && !col.as ? getArrowPath(col) : null;
359+
if (arrowPath && arrowPath.length > 1) {
360+
// For arrow expressions in INTO OBJECT(), generate nested object assignment
361+
// This will be added to sp (post-processing) instead of ss (inline object)
362+
var valueJs = n2u(col.toJS('p', query.defaultTableid, query.defcols));
363+
// Generate code to create nested structure
364+
// e.g., for path ['details', 'stock']: r['details'] = r['details'] || {}; r['details']['stock'] = value;
365+
for (var i = 0; i < arrowPath.length - 1; i++) {
366+
var pathSoFar = arrowPath.slice(0, i + 1);
367+
var accessor = pathSoFar
368+
.map(function (p) {
369+
return "['" + escapeq(p) + "']";
370+
})
371+
.join('');
372+
sp += 'r' + accessor + ' = r' + accessor + ' || {};';
373+
}
374+
var fullAccessor = arrowPath
375+
.map(function (p) {
376+
return "['" + escapeq(p) + "']";
377+
})
378+
.join('');
379+
sp += 'r' + fullAccessor + ' = ' + valueJs + ';';
380+
381+
// Use the first part of the path as the column name for metadata
382+
var colName = arrowPath[0];
383+
query.selectColumns[escapeq(colName)] = true;
384+
var coldef = {
385+
columnid: colName,
386+
};
387+
// Only add if not already added
388+
if (!query.xcolumns[coldef.columnid]) {
389+
query.columns.push(coldef);
390+
query.xcolumns[coldef.columnid] = coldef;
391+
}
392+
} else {
393+
ss.push(
394+
"'" +
395+
escapeq(col.as || col.columnid || col.toString()) +
396+
"':" +
397+
n2u(col.toJS('p', query.defaultTableid, query.defcols))
398+
);
399+
// ss.push('\''+escapeq(col.toString())+'\':'+col.toJS("p",query.defaultTableid));
400+
//if(col instanceof yy.Expression) {
401+
query.selectColumns[escapeq(col.as || col.columnid || col.toString())] = true;
338402

339-
var coldef = {
340-
columnid: col.as || col.columnid || col.toString(),
341-
// dbtypeid:tcol.dbtypeid,
342-
// dbsize:tcol.dbsize,
343-
// dbpecision:tcol.dbprecision,
344-
// dbenum: tcol.dbenum,
345-
};
346-
// console.log(2);
347-
query.columns.push(coldef);
348-
query.xcolumns[coldef.columnid] = coldef;
403+
var coldef = {
404+
columnid: col.as || col.columnid || col.toString(),
405+
// dbtypeid:tcol.dbtypeid,
406+
// dbsize:tcol.dbsize,
407+
// dbpecision:tcol.dbprecision,
408+
// dbenum: tcol.dbenum,
409+
};
410+
// console.log(2);
411+
query.columns.push(coldef);
412+
query.xcolumns[coldef.columnid] = coldef;
413+
}
349414
}
350415
});
351416
s += ss.join(',') + '};' + sp;

src/830into.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,30 @@ alasql.into.JSON = function (filename, opts, data, columns, cb) {
144144
return res;
145145
};
146146

147+
alasql.into.OBJECT = function (filename, opts, data, columns, cb) {
148+
var res;
149+
if (typeof filename === 'object') {
150+
opts = filename;
151+
filename = undefined;
152+
}
153+
154+
// Data is already in nested object format from core compilation
155+
// If filename is provided, save to file like JSON
156+
if (filename) {
157+
var s = JSON.stringify(data);
158+
filename = alasql.utils.autoExtFilename(filename, 'json', opts);
159+
res = alasql.utils.saveFile(filename, s);
160+
} else {
161+
// Return the data directly
162+
res = data;
163+
}
164+
165+
if (cb) {
166+
res = cb(res);
167+
}
168+
return res;
169+
};
170+
147171
alasql.into.TXT = function (filename, opts, data, columns, cb) {
148172
// If columns is empty
149173
if (columns.length === 0 && data.length > 0) {

test/test1278.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
if (typeof exports === 'object') {
2+
var assert = require('assert');
3+
var alasql = require('..');
4+
}
5+
6+
describe('Test 1278 - Output JSON object as nested objects with INTO OBJECT', function () {
7+
const data = [
8+
{
9+
name: 'item1',
10+
details: {
11+
stock: 10,
12+
purchased: 100,
13+
},
14+
},
15+
{
16+
name: 'item2',
17+
details: {
18+
stock: 20,
19+
purchased: 200,
20+
},
21+
},
22+
{
23+
name: 'item3',
24+
details: {
25+
stock: 30,
26+
purchased: 300,
27+
},
28+
},
29+
];
30+
31+
it('1. Current behavior - INTO JSON() returns flattened column names', function () {
32+
// This demonstrates the current (flattened) behavior
33+
var res = alasql('SELECT name, details->stock FROM ? WHERE details->stock > 11', [data]);
34+
// Currently returns: [{"name":"item2","details->stock":20},{"name":"item3","details->stock":30}]
35+
assert.deepEqual(res, [
36+
{name: 'item2', 'details->stock': 20},
37+
{name: 'item3', 'details->stock': 30},
38+
]);
39+
});
40+
41+
it('2. INTO OBJECT() returns nested objects', function () {
42+
var res = alasql('SELECT name, details->stock INTO OBJECT() FROM ? WHERE details->stock > 11', [
43+
data,
44+
]);
45+
// Expected: [{"name":"item2","details":{"stock":20}},{"name":"item3","details":{"stock":30}}]
46+
assert.deepEqual(res, [
47+
{name: 'item2', details: {stock: 20}},
48+
{name: 'item3', details: {stock: 30}},
49+
]);
50+
});
51+
52+
it('3. INTO OBJECT() with multiple nested levels', function () {
53+
const nestedData = [
54+
{
55+
id: 1,
56+
config: {
57+
settings: {
58+
enabled: true,
59+
count: 5,
60+
},
61+
name: 'test',
62+
},
63+
},
64+
{
65+
id: 2,
66+
config: {
67+
settings: {
68+
enabled: false,
69+
count: 10,
70+
},
71+
name: 'prod',
72+
},
73+
},
74+
];
75+
76+
var res = alasql('SELECT id, config->settings->enabled, config->name INTO OBJECT() FROM ?', [
77+
nestedData,
78+
]);
79+
assert.deepEqual(res, [
80+
{id: 1, config: {settings: {enabled: true}, name: 'test'}},
81+
{id: 2, config: {settings: {enabled: false}, name: 'prod'}},
82+
]);
83+
});
84+
85+
it('4. INTO OBJECT() with column alias still works', function () {
86+
var res = alasql(
87+
'SELECT name, details->stock AS stockLevel INTO OBJECT() FROM ? WHERE details->stock > 11',
88+
[data]
89+
);
90+
// When using AS, it should use the alias as the column name
91+
assert.deepEqual(res, [
92+
{name: 'item2', stockLevel: 20},
93+
{name: 'item3', stockLevel: 30},
94+
]);
95+
});
96+
97+
it('5. INTO OBJECT() with no arrow notation behaves like regular output', function () {
98+
var res = alasql('SELECT name INTO OBJECT() FROM ? WHERE details->stock > 11', [data]);
99+
assert.deepEqual(res, [{name: 'item2'}, {name: 'item3'}]);
100+
});
101+
});

0 commit comments

Comments
 (0)