Skip to content

Commit f8ed656

Browse files
Copilotmathiasrw
andauthored
Fix joinstar options for inline data to closer #1004 (#2312)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mathiasrw <[email protected]> Co-authored-by: Mathias Wulff <[email protected]>
1 parent fcc3e21 commit f8ed656

File tree

4 files changed

+181
-6
lines changed

4 files changed

+181
-6
lines changed

src/17alasql.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ alasql.dexec = function (databaseid, sql, params, cb, scope) {
272272
// if(db.databaseid != databaseid) console.trace('got!');
273273
// console.log(3,db.databaseid,databaseid);
274274

275-
var hh = hash(sql);
275+
// Include joinstar option in cache key because it affects how SELECT * compiles
276+
// Without this, changing joinstar would use stale cached queries compiled with old option
277+
var hh = hash(sql + '|joinstar:' + alasql.options.joinstar);
276278

277279
// Create hash
278280
if (alasql.options.cache) {

src/424select.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,22 @@ function compileSelectStar(query, aliases, joinstar) {
101101
} else {
102102
// console.log(60,alias,columns);
103103

104-
// if column not exist, then copy all
105-
sp += 'var w=p["' + alias + '"];for(var k in w){r[k]=w[k]};';
104+
// If columns are not known (e.g., with inline data using ? placeholders),
105+
// copy all properties dynamically respecting the joinstar option:
106+
// - 'json': Nested objects by alias (e.g., {a: {col: val}, b: {col: val}})
107+
// - 'underscore': Prefix columns with alias (e.g., {a_col: val, b_col: val})
108+
// - 'overwrite': Later columns overwrite earlier ones (default)
109+
if (joinstar && alasql.options.joinstar == 'json') {
110+
// For json mode, create nested object with alias as key
111+
sp += "r['" + escapeq(alias) + "']=p['" + escapeq(alias) + "'];";
112+
} else if (joinstar && alasql.options.joinstar == 'underscore') {
113+
// For underscore mode, prefix each key with alias_
114+
sp +=
115+
'var w=p["' + escapeq(alias) + '"];for(var k in w){r["' + escapeq(alias) + '_"+k]=w[k]};';
116+
} else {
117+
// Default overwrite mode
118+
sp += 'var w=p["' + escapeq(alias) + '"];for(var k in w){r[k]=w[k]};';
119+
}
106120
//console.log(777, sp);
107121
query.dirtyColumns = true;
108122
}

test/test1004.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
if (typeof exports === 'object') {
2+
var assert = require('assert');
3+
var alasql = require('..');
4+
}
5+
6+
// Test for issue #1004 - joinstar option not working with inline data
7+
// Related to issue #547
8+
//
9+
// The joinstar option controls how columns are named when using SELECT * with JOINs:
10+
// - 'overwrite' (default): Later columns overwrite earlier ones with same name
11+
// - 'underscore': Columns are prefixed with table alias (e.g., a_col, b_col)
12+
// - 'json': Results are nested objects by table alias (e.g., {a: {...}, b: {...}})
13+
14+
describe('Test 1004 - JOINSTAR with inline data (FROM ?)', function () {
15+
var test = 1004;
16+
17+
after(function () {
18+
alasql.options.joinstar = 'overwrite';
19+
});
20+
21+
it('1. UNDERSCORE JOINSTAR with inline data', () => {
22+
var data = [{dep: 'A', qt: 10, price: 5, extra: 1}];
23+
var data2 = [{dep: 'B', qt: 2, price: 5}];
24+
alasql.options.joinstar = 'underscore';
25+
var res = alasql('SELECT * FROM ? as a JOIN ? as b', [data, data2]);
26+
// Expected: columns prefixed with table aliases
27+
assert.deepEqual(res, [
28+
{a_dep: 'A', a_qt: 10, a_price: 5, a_extra: 1, b_dep: 'B', b_qt: 2, b_price: 5},
29+
]);
30+
});
31+
32+
it('2. JSON JOINSTAR with inline data', () => {
33+
var data = [{dep: 'A', qt: 10, price: 5, extra: 1}];
34+
var data2 = [{dep: 'B', qt: 2, price: 5}];
35+
alasql.options.joinstar = 'json';
36+
var res = alasql('SELECT * FROM ? as a JOIN ? as b', [data, data2]);
37+
// Expected: nested objects by table alias
38+
assert.deepEqual(res, [
39+
{
40+
a: {dep: 'A', qt: 10, price: 5, extra: 1},
41+
b: {dep: 'B', qt: 2, price: 5},
42+
},
43+
]);
44+
});
45+
46+
it('3. OVERWRITE JOINSTAR with inline data (default behavior)', () => {
47+
var data = [{dep: 'A', qt: 10, price: 5, extra: 1}];
48+
var data2 = [{dep: 'B', qt: 2, price: 5}];
49+
alasql.options.joinstar = 'overwrite';
50+
var res = alasql('SELECT * FROM ? as a JOIN ? as b', [data, data2]);
51+
// Expected: later columns overwrite earlier ones
52+
assert.deepEqual(res, [{dep: 'B', qt: 2, price: 5, extra: 1}]);
53+
});
54+
55+
it('4. UNDERSCORE JOINSTAR with multiple rows', () => {
56+
var data = [
57+
{id: 1, name: 'Alice'},
58+
{id: 2, name: 'Bob'},
59+
];
60+
var data2 = [
61+
{id: 10, dept: 'Sales'},
62+
{id: 20, dept: 'IT'},
63+
];
64+
alasql.options.joinstar = 'underscore';
65+
var res = alasql('SELECT * FROM ? as users JOIN ? as depts', [data, data2]);
66+
// Cartesian product with underscore prefixes
67+
assert.deepEqual(res, [
68+
{users_id: 1, users_name: 'Alice', depts_id: 10, depts_dept: 'Sales'},
69+
{users_id: 1, users_name: 'Alice', depts_id: 20, depts_dept: 'IT'},
70+
{users_id: 2, users_name: 'Bob', depts_id: 10, depts_dept: 'Sales'},
71+
{users_id: 2, users_name: 'Bob', depts_id: 20, depts_dept: 'IT'},
72+
]);
73+
});
74+
75+
it('5. JSON JOINSTAR with multiple rows', () => {
76+
var data = [
77+
{id: 1, name: 'Alice'},
78+
{id: 2, name: 'Bob'},
79+
];
80+
var data2 = [
81+
{id: 10, dept: 'Sales'},
82+
{id: 20, dept: 'IT'},
83+
];
84+
alasql.options.joinstar = 'json';
85+
var res = alasql('SELECT * FROM ? as users JOIN ? as depts', [data, data2]);
86+
// Cartesian product with nested objects
87+
assert.deepEqual(res, [
88+
{users: {id: 1, name: 'Alice'}, depts: {id: 10, dept: 'Sales'}},
89+
{users: {id: 1, name: 'Alice'}, depts: {id: 20, dept: 'IT'}},
90+
{users: {id: 2, name: 'Bob'}, depts: {id: 10, dept: 'Sales'}},
91+
{users: {id: 2, name: 'Bob'}, depts: {id: 20, dept: 'IT'}},
92+
]);
93+
});
94+
95+
it('6. UNDERSCORE JOINSTAR with three-way join', () => {
96+
var data1 = [{id: 1}];
97+
var data2 = [{val: 'A'}];
98+
var data3 = [{num: 100}];
99+
alasql.options.joinstar = 'underscore';
100+
var res = alasql('SELECT * FROM ? as t1 JOIN ? as t2 JOIN ? as t3', [data1, data2, data3]);
101+
assert.deepEqual(res, [{t1_id: 1, t2_val: 'A', t3_num: 100}]);
102+
});
103+
104+
it('7. JSON JOINSTAR with three-way join', () => {
105+
var data1 = [{id: 1}];
106+
var data2 = [{val: 'A'}];
107+
var data3 = [{num: 100}];
108+
alasql.options.joinstar = 'json';
109+
var res = alasql('SELECT * FROM ? as t1 JOIN ? as t2 JOIN ? as t3', [data1, data2, data3]);
110+
assert.deepEqual(res, [{t1: {id: 1}, t2: {val: 'A'}, t3: {num: 100}}]);
111+
});
112+
113+
it('8. Cache invalidation when switching joinstar modes', () => {
114+
var data = [{a: 1}];
115+
var data2 = [{b: 2}];
116+
117+
// First query with underscore mode
118+
alasql.options.joinstar = 'underscore';
119+
var res1 = alasql('SELECT * FROM ? as x JOIN ? as y', [data, data2]);
120+
assert.deepEqual(res1, [{x_a: 1, y_b: 2}]);
121+
122+
// Same query with json mode should not use cached version
123+
alasql.options.joinstar = 'json';
124+
var res2 = alasql('SELECT * FROM ? as x JOIN ? as y', [data, data2]);
125+
assert.deepEqual(res2, [{x: {a: 1}, y: {b: 2}}]);
126+
127+
// Same query with overwrite mode
128+
alasql.options.joinstar = 'overwrite';
129+
var res3 = alasql('SELECT * FROM ? as x JOIN ? as y', [data, data2]);
130+
assert.deepEqual(res3, [{a: 1, b: 2}]);
131+
});
132+
133+
it('9. UNDERSCORE JOINSTAR with empty result', () => {
134+
var data = [{id: 1}];
135+
var data2 = [];
136+
alasql.options.joinstar = 'underscore';
137+
var res = alasql('SELECT * FROM ? as a JOIN ? as b', [data, data2]);
138+
assert.deepEqual(res, []);
139+
});
140+
141+
it('10. JSON JOINSTAR with special characters in column names', () => {
142+
var data = [{'col-name': 'A', 'col.name': 'B'}];
143+
var data2 = [{col_name: 'C'}];
144+
alasql.options.joinstar = 'json';
145+
var res = alasql('SELECT * FROM ? as t1 JOIN ? as t2', [data, data2]);
146+
assert.deepEqual(res, [{t1: {'col-name': 'A', 'col.name': 'B'}, t2: {col_name: 'C'}}]);
147+
});
148+
});

test/test2027.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,26 @@ describe('Test 2007 - SQL cache', function () {
1919
alasql('INSERT INTO osoby VALUES (1, "John"), (2, "Jane"), (3, "Jake")');
2020
var res = alasql('SELECT * FROM osoby');
2121

22-
assert.deepEqual(alasql.databases['test'].sqlCache['-169125189'].query.data, []);
22+
// Find the cache key for the SELECT query
23+
var cacheKeys = Object.keys(alasql.databases['test'].sqlCache);
24+
var selectCacheKey = null;
25+
for (var key of cacheKeys) {
26+
if (alasql.databases['test'].sqlCache[key].sql === 'SELECT * FROM osoby') {
27+
selectCacheKey = key;
28+
break;
29+
}
30+
}
31+
32+
assert.ok(selectCacheKey, 'Cache key for SELECT query should exist');
33+
assert.deepEqual(alasql.databases['test'].sqlCache[selectCacheKey].query.data, []);
2334
assert.equal(res.length, 3);
2435

2536
// Delete all rows
2637
alasql('DELETE FROM osoby');
2738

2839
// Assert that the cache is still empty for "data"
2940
// Without the fix, the cache would still contain the data from the previous query even though all rows were deleted
30-
assert.deepEqual(alasql.databases['test'].sqlCache['-169125189'].query.data, []);
41+
assert.deepEqual(alasql.databases['test'].sqlCache[selectCacheKey].query.data, []);
3142

3243
// Insert more rows
3344
alasql('INSERT INTO osoby VALUES (4, "Jack"), (5, "Paul")');
@@ -36,7 +47,7 @@ describe('Test 2007 - SQL cache', function () {
3647
var res2 = alasql('SELECT * FROM osoby');
3748

3849
// Cache should still be empty for "data"
39-
assert.deepEqual(alasql.databases['test'].sqlCache['-169125189'].query.data, []);
50+
assert.deepEqual(alasql.databases['test'].sqlCache[selectCacheKey].query.data, []);
4051
assert.equal(res2.length, 2);
4152
});
4253
});

0 commit comments

Comments
 (0)