Skip to content

Commit 5e75d54

Browse files
Copilotmathiasrw
andauthored
Add UNNEST function to close #2141 (#2342)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mathiasrw <[email protected]>
1 parent f2a1658 commit 5e75d54

File tree

2 files changed

+305
-0
lines changed

2 files changed

+305
-0
lines changed

src/84from.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,35 @@ alasql.from.RANGE = function (start, finish, cb, idx, query) {
9797
return res;
9898
};
9999

100+
/**
101+
* UNNEST function - converts an array into a table for use in FROM clauses
102+
*
103+
* This function enables flattening of nested arrays when used with CROSS APPLY or OUTER APPLY.
104+
*
105+
* @param {Array} arr - The array to unnest
106+
* @param {Object} opts - Options (reserved for future use)
107+
* @param {Function} cb - Callback function
108+
* @param {number} idx - Index
109+
* @param {Object} query - Query object
110+
* @returns {Array} The input array, or empty array if input is not an array
111+
*
112+
* @example
113+
* // Flatten nested arrays
114+
* SELECT b.name, e.id, e.value
115+
* FROM data AS b
116+
* CROSS APPLY (SELECT * FROM UNNEST(b.entries)) AS e
117+
*/
118+
alasql.from.UNNEST = function (arr, opts, cb, idx, query) {
119+
var res = arr;
120+
if (!Array.isArray(res)) {
121+
res = [];
122+
}
123+
if (cb) {
124+
res = cb(res, idx, query);
125+
}
126+
return res;
127+
};
128+
100129
// Read data from any file
101130
alasql.from.FILE = function (filename, opts, cb, idx, query) {
102131
var fname;

test/test2141.js

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
if (typeof exports === 'object') {
2+
var assert = require('assert');
3+
var alasql = require('..');
4+
} else {
5+
__dirname = '.';
6+
}
7+
8+
/*
9+
* Test for UNNEST function to flatten nested arrays in objects
10+
*
11+
* This addresses the issue of "broadcasting" nested objects without SEARCH.
12+
* Using CROSS APPLY with UNNEST allows flattening of nested array properties.
13+
*
14+
* Example from the issue:
15+
* Input: [{ name: "a", entries: [{ id: 1, value: 2 }, { id: 3, value: 4 }] }]
16+
* Output: [{ name: "a", id: 1, value: 2 }, { name: "a", id: 3, value: 4 }]
17+
*
18+
* Usage:
19+
* SELECT parent.field, child.field
20+
* FROM table AS parent
21+
* CROSS APPLY (SELECT * FROM UNNEST(parent.array_field)) AS child
22+
*/
23+
24+
describe('Test 2141 - UNNEST function for flattening nested objects', function () {
25+
it('1. Basic UNNEST function with simple array', function (done) {
26+
var data = [1, 2, 3, 4, 5];
27+
// Note: SELECT COLUMN _ is used for primitive values to return the array itself
28+
// rather than wrapping each value in an object
29+
var res = alasql('SELECT COLUMN _ FROM UNNEST(?)', [data]);
30+
assert.deepEqual(res, [1, 2, 3, 4, 5]);
31+
done();
32+
});
33+
34+
it('2. UNNEST function with array of objects', function (done) {
35+
var data = [
36+
{id: 1, value: 2},
37+
{id: 3, value: 4},
38+
];
39+
var res = alasql('SELECT * FROM UNNEST(?)', [data]);
40+
assert.deepEqual(res, [
41+
{id: 1, value: 2},
42+
{id: 3, value: 4},
43+
]);
44+
done();
45+
});
46+
47+
it('3. CROSS APPLY with UNNEST for nested object flattening', function (done) {
48+
var data = [
49+
{
50+
name: 'a',
51+
entries: [
52+
{id: 1, value: 2},
53+
{id: 3, value: 4},
54+
],
55+
},
56+
{
57+
name: 'b',
58+
entries: [
59+
{id: 5, value: 6},
60+
{id: 7, value: 8},
61+
{id: 9, value: 10},
62+
],
63+
},
64+
];
65+
66+
var res = alasql(
67+
'SELECT b.name, e.id, e.value \
68+
FROM ? AS b \
69+
CROSS APPLY (SELECT * FROM UNNEST(b.entries)) AS e',
70+
[data]
71+
);
72+
73+
assert.deepEqual(res, [
74+
{name: 'a', id: 1, value: 2},
75+
{name: 'a', id: 3, value: 4},
76+
{name: 'b', id: 5, value: 6},
77+
{name: 'b', id: 7, value: 8},
78+
{name: 'b', id: 9, value: 10},
79+
]);
80+
done();
81+
});
82+
83+
it('4. OUTER APPLY with UNNEST handles empty arrays', function (done) {
84+
var data = [
85+
{
86+
name: 'a',
87+
entries: [
88+
{id: 1, value: 2},
89+
{id: 3, value: 4},
90+
],
91+
},
92+
{
93+
name: 'b',
94+
entries: [],
95+
},
96+
{
97+
name: 'c',
98+
entries: [{id: 5, value: 6}],
99+
},
100+
];
101+
102+
var res = alasql(
103+
'SELECT b.name, e.id, e.value \
104+
FROM ? AS b \
105+
OUTER APPLY (SELECT * FROM UNNEST(b.entries)) AS e',
106+
[data]
107+
);
108+
109+
assert.deepEqual(res, [
110+
{name: 'a', id: 1, value: 2},
111+
{name: 'a', id: 3, value: 4},
112+
{name: 'b', id: undefined, value: undefined},
113+
{name: 'c', id: 5, value: 6},
114+
]);
115+
done();
116+
});
117+
118+
it('5. CROSS APPLY with UNNEST - join flattened data with another table', function (done) {
119+
var data = [
120+
{
121+
name: 'a',
122+
entries: [
123+
{id: 1, value: 2},
124+
{id: 3, value: 4},
125+
],
126+
},
127+
{
128+
name: 'b',
129+
entries: [
130+
{id: 1, value: 6},
131+
{id: 2, value: 8},
132+
],
133+
},
134+
];
135+
136+
var lookup = [
137+
{id: 1, label: 'first'},
138+
{id: 2, label: 'second'},
139+
{id: 3, label: 'third'},
140+
];
141+
142+
var res = alasql(
143+
'SELECT b.name, e.id, e.value, l.label \
144+
FROM ? AS b \
145+
CROSS APPLY (SELECT * FROM UNNEST(b.entries)) AS e \
146+
JOIN ? AS l ON l.id = e.id',
147+
[data, lookup]
148+
);
149+
150+
assert.deepEqual(res, [
151+
{name: 'a', id: 1, value: 2, label: 'first'},
152+
{name: 'a', id: 3, value: 4, label: 'third'},
153+
{name: 'b', id: 1, value: 6, label: 'first'},
154+
{name: 'b', id: 2, value: 8, label: 'second'},
155+
]);
156+
done();
157+
});
158+
159+
it('6. Flattening from database.table format', function (done) {
160+
alasql('CREATE DATABASE IF NOT EXISTS testdb2141');
161+
alasql('USE testdb2141');
162+
alasql('CREATE TABLE IF NOT EXISTS testtable (name STRING, entries)');
163+
164+
var data = [
165+
{
166+
name: 'a',
167+
entries: [
168+
{id: 1, value: 2},
169+
{id: 3, value: 4},
170+
],
171+
},
172+
{
173+
name: 'b',
174+
entries: [
175+
{id: 5, value: 6},
176+
{id: 7, value: 8},
177+
{id: 9, value: 10},
178+
],
179+
},
180+
];
181+
182+
alasql('INSERT INTO testtable SELECT * FROM ?', [data]);
183+
184+
var res = alasql(
185+
'SELECT b.name, e.id, e.value \
186+
FROM testdb2141.testtable AS b \
187+
CROSS APPLY (SELECT * FROM UNNEST(b.entries)) AS e'
188+
);
189+
190+
assert.deepEqual(res, [
191+
{name: 'a', id: 1, value: 2},
192+
{name: 'a', id: 3, value: 4},
193+
{name: 'b', id: 5, value: 6},
194+
{name: 'b', id: 7, value: 8},
195+
{name: 'b', id: 9, value: 10},
196+
]);
197+
198+
alasql('DROP DATABASE testdb2141');
199+
done();
200+
});
201+
202+
it('7. Using arrow operator for nested property access in SELECT', function (done) {
203+
var data = [
204+
{
205+
name: 'a',
206+
entries: [
207+
{id: 1, value: 2},
208+
{id: 3, value: 4},
209+
],
210+
},
211+
{
212+
name: 'b',
213+
entries: [{id: 5, value: 6}],
214+
},
215+
];
216+
217+
var res = alasql(
218+
'SELECT b.name, e.id AS id, e.value AS val \
219+
FROM ? AS b \
220+
CROSS APPLY (SELECT * FROM UNNEST(b.entries)) AS e',
221+
[data]
222+
);
223+
224+
assert.deepEqual(res, [
225+
{name: 'a', id: 1, val: 2},
226+
{name: 'a', id: 3, val: 4},
227+
{name: 'b', id: 5, val: 6},
228+
]);
229+
done();
230+
});
231+
232+
it('8. Example from issue - exact scenario', function (done) {
233+
// Create database and table as in the issue
234+
alasql('CREATE DATABASE IF NOT EXISTS A');
235+
alasql('USE A');
236+
alasql('CREATE TABLE IF NOT EXISTS B (name STRING, entries)');
237+
238+
var data = [
239+
{
240+
name: 'a',
241+
entries: [
242+
{id: 1, value: 2},
243+
{id: 3, value: 4},
244+
],
245+
},
246+
{
247+
name: 'b',
248+
entries: [
249+
{id: 5, value: 6},
250+
{id: 7, value: 8},
251+
{id: 9, value: 10},
252+
],
253+
},
254+
];
255+
256+
alasql('INSERT INTO B SELECT * FROM ?', [data]);
257+
258+
// Use the flattening query as suggested in the issue comments
259+
var res = alasql(
260+
'SELECT b.name, e.id, e.value \
261+
FROM A.B AS b \
262+
CROSS APPLY (SELECT * FROM UNNEST(b.entries)) AS e'
263+
);
264+
265+
assert.deepEqual(res, [
266+
{name: 'a', id: 1, value: 2},
267+
{name: 'a', id: 3, value: 4},
268+
{name: 'b', id: 5, value: 6},
269+
{name: 'b', id: 7, value: 8},
270+
{name: 'b', id: 9, value: 10},
271+
]);
272+
273+
alasql('DROP DATABASE A');
274+
done();
275+
});
276+
});

0 commit comments

Comments
 (0)