Skip to content

Commit ea35f7a

Browse files
committed
Allow SELECT * for pivot to fix #490
1 parent 5217f44 commit ea35f7a

File tree

4 files changed

+227
-112
lines changed

4 files changed

+227
-112
lines changed

src/427pivot.js

Lines changed: 188 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -30,105 +30,220 @@ yy.Select.prototype.compilePivot = function (query) {
3030
});
3131
}
3232

33-
// Function for PIVOT post production
33+
// Function for PIVOT post production (returned by compilePivot)
3434
return function () {
35-
var query = this;
36-
var cols = query.columns
37-
.filter(function (col) {
38-
return col.columnid != columnid && col.columnid != exprcolid;
39-
})
40-
.map(function (col) {
41-
return col.columnid;
42-
});
35+
var query = this; // Keep reference to the query object
36+
37+
// Exit if no data to pivot
38+
if (!query.data || query.data.length === 0) {
39+
query.columns = []; // Reset columns if no data
40+
return;
41+
}
42+
43+
// --- Determine grouping columns robustly ---
44+
var firstRowKeys = Object.keys(query.data[0]);
45+
var cols = firstRowKeys.filter(function (key) {
46+
return key !== columnid && key !== exprcolid;
47+
});
48+
// -----------------------------------------
49+
50+
var newcols = []; // Stores the new column headers (e.g., 'MON', 'TUE')
51+
var gnewcols = {}; // Tracks unique new columns encountered
52+
var gr = {}; // Stores the grouped & pivoted results (key: gx, value: g)
53+
var ga = {}; // Stores counts for AVG calculation (key: gx, value: {day: count})
54+
var data = []; // The final pivoted data array
4355

44-
var newcols = [];
45-
var gnewcols = {};
46-
var gr = {};
47-
var ga = {};
48-
var data = [];
4956
query.data.forEach(function (d) {
50-
if (!inlist || inlist.indexOf(d[columnid]) > -1) {
51-
var gx = cols
52-
.map(function (colid) {
53-
return d[colid];
54-
})
55-
.join('`');
56-
var g = gr[gx];
57-
if (!g) {
58-
g = {};
59-
gr[gx] = g;
60-
data.push(g);
61-
cols.forEach(function (colid) {
62-
g[colid] = d[colid];
63-
});
64-
}
57+
// Skip row if the pivot column value is not in the IN list (if provided)
58+
if (inlist && inlist.indexOf(d[columnid]) === -1) {
59+
return;
60+
}
6561

66-
if (!ga[gx]) {
67-
ga[gx] = {};
68-
}
62+
var gx = cols
63+
.map(function (colid) {
64+
return d[colid] === undefined || d[colid] === null ? '' : d[colid];
65+
})
66+
.join('`');
6967

70-
if (ga[gx][d[columnid]]) {
71-
ga[gx][d[columnid]]++;
72-
} else {
73-
ga[gx][d[columnid]] = 1;
74-
}
68+
var g = gr[gx];
69+
if (!g) {
70+
g = {};
71+
gr[gx] = g;
72+
data.push(g);
73+
cols.forEach(function (colid) {
74+
g[colid] = d[colid];
75+
});
76+
}
7577

76-
if (!gnewcols[d[columnid]]) {
77-
gnewcols[d[columnid]] = true;
78-
newcols.push(d[columnid]);
78+
if (!ga[gx]) {
79+
ga[gx] = {};
80+
}
81+
82+
var pivotColValue = d[columnid];
83+
var aggValue = d[exprcolid];
84+
85+
// Increment count only for non-null/undefined values when calculating AVG
86+
if (ga[gx][pivotColValue]) {
87+
if (aggValue !== null && typeof aggValue !== 'undefined') {
88+
ga[gx][pivotColValue]++;
7989
}
90+
} else {
91+
ga[gx][pivotColValue] = aggValue !== null && typeof aggValue !== 'undefined' ? 1 : 0;
92+
}
93+
94+
if (!gnewcols[pivotColValue]) {
95+
gnewcols[pivotColValue] = true;
96+
newcols.push(pivotColValue);
97+
}
8098

81-
if (aggr == 'SUM' || aggr == 'AVG' || aggr == 'TOTAL') {
82-
if (typeof g[d[columnid]] == 'undefined') g[d[columnid]] = 0;
83-
g[d[columnid]] += +d[exprcolid];
84-
} else if (aggr == 'COUNT') {
85-
if (typeof g[d[columnid]] == 'undefined') g[d[columnid]] = 0;
86-
g[d[columnid]]++;
87-
} else if (aggr == 'MIN') {
88-
if (typeof g[d[columnid]] == 'undefined') g[d[columnid]] = d[exprcolid];
89-
if (d[exprcolid] < g[d[columnid]]) g[d[columnid]] = d[exprcolid];
90-
} else if (aggr == 'MAX') {
91-
if (typeof g[d[columnid]] == 'undefined') g[d[columnid]] = d[exprcolid];
92-
if (d[exprcolid] > g[d[columnid]]) g[d[columnid]] = d[exprcolid];
93-
} else if (aggr == 'FIRST') {
94-
if (typeof g[d[columnid]] == 'undefined') g[d[columnid]] = d[exprcolid];
95-
} else if (aggr == 'LAST') {
96-
g[d[columnid]] = d[exprcolid];
97-
} else if (alasql.aggr[aggr]) {
98-
// Custom aggregator
99-
alasql.aggr[aggr](g[d[columnid]], d[exprcolid]);
99+
// Apply the specified aggregation
100+
if (aggr == 'SUM' || aggr == 'AVG' || aggr == 'TOTAL') {
101+
if (aggValue !== null && typeof aggValue !== 'undefined') {
102+
g[pivotColValue] =
103+
typeof g[pivotColValue] === 'undefined' || g[pivotColValue] === null
104+
? Number(aggValue)
105+
: g[pivotColValue] + Number(aggValue);
106+
} else if (typeof g[pivotColValue] === 'undefined') {
107+
g[pivotColValue] = null;
108+
}
109+
} else if (aggr == 'COUNT') {
110+
if (exprcolid === '*' || (aggValue !== null && typeof aggValue !== 'undefined')) {
111+
g[pivotColValue] = (g[pivotColValue] || 0) + 1;
112+
} else if (typeof g[pivotColValue] === 'undefined') {
113+
g[pivotColValue] = 0;
114+
}
115+
} else if (aggr == 'MIN') {
116+
if (aggValue !== null && typeof aggValue !== 'undefined') {
117+
if (
118+
typeof g[pivotColValue] === 'undefined' ||
119+
g[pivotColValue] === null ||
120+
aggValue < g[pivotColValue]
121+
) {
122+
g[pivotColValue] = aggValue;
123+
}
124+
} else if (typeof g[pivotColValue] === 'undefined') {
125+
g[pivotColValue] = null;
126+
}
127+
} else if (aggr == 'MAX') {
128+
if (aggValue !== null && typeof aggValue !== 'undefined') {
129+
if (
130+
typeof g[pivotColValue] === 'undefined' ||
131+
g[pivotColValue] === null ||
132+
aggValue > g[pivotColValue]
133+
) {
134+
g[pivotColValue] = aggValue;
135+
}
136+
} else if (typeof g[pivotColValue] === 'undefined') {
137+
g[pivotColValue] = null;
138+
}
139+
} else if (aggr == 'FIRST') {
140+
if (typeof g[pivotColValue] === 'undefined') {
141+
g[pivotColValue] = aggValue;
142+
}
143+
} else if (aggr == 'LAST') {
144+
g[pivotColValue] = aggValue;
145+
} else if (alasql.aggr[aggr]) {
146+
if (typeof g[pivotColValue] === 'undefined') {
147+
g[pivotColValue] = alasql.aggr[aggr](aggValue, undefined, 1);
100148
} else {
101-
throw new Error('Wrong aggregator in PIVOT clause');
149+
g[pivotColValue] = alasql.aggr[aggr](aggValue, g[pivotColValue], 2);
102150
}
151+
} else {
152+
throw new Error('Unknown aggregator in PIVOT clause: ' + aggr);
103153
}
104154
});
105155

156+
// Finalize AVG calculation
106157
if (aggr == 'AVG') {
107158
for (var gx in gr) {
108159
var d = gr[gx];
109-
for (var colid in d) {
110-
if (cols.indexOf(colid) == -1 && colid != exprcolid) {
111-
d[colid] = d[colid] / ga[gx][colid];
160+
for (var pivotColValue in ga[gx]) {
161+
if (d.hasOwnProperty(pivotColValue) && d[pivotColValue] !== null) {
162+
var count = ga[gx][pivotColValue];
163+
if (count > 0) {
164+
d[pivotColValue] = d[pivotColValue] / count;
165+
} else {
166+
d[pivotColValue] = null;
167+
}
112168
}
113169
}
114170
}
115171
}
116172

117-
// console.log(query.columns,newcols);
118-
// columns
173+
// --- Rebuild query.columns ---
119174
query.data = data;
120175

121-
if (inlist) newcols = inlist;
176+
if (inlist) {
177+
newcols = inlist;
178+
} else {
179+
newcols.sort();
180+
}
181+
182+
// Find original column definition - might be basic if SELECT * was used
183+
let aggColDef = query.columns.find(col => col.columnid === exprcolid);
184+
// If SELECT * was used, aggColDef might be missing, find it from the table definition if possible
185+
if (!aggColDef && query.sources && query.sources.length > 0) {
186+
let sourceTableId = query.sources[0].tableid;
187+
let sourceDbId = query.sources[0].databaseid;
188+
if (
189+
sourceTableId &&
190+
sourceDbId &&
191+
alasql.databases[sourceDbId]?.tables?.[sourceTableId]?.xcolumns
192+
) {
193+
aggColDef = alasql.databases[sourceDbId].tables[sourceTableId].xcolumns[exprcolid];
194+
}
195+
}
196+
// Provide a fallback if still not found
197+
aggColDef = aggColDef || { columnid: exprcolid, dbtypeid: 'OBJECT' };
122198

123-
var ncol = query.columns.filter(function (col) {
124-
return col.columnid == exprcolid;
125-
})[0];
199+
// Keep only the grouping columns initially
126200
query.columns = query.columns.filter(function (col) {
127-
return !(col.columnid == columnid || col.columnid == exprcolid);
201+
return cols.includes(col.columnid);
128202
});
129-
newcols.forEach(function (colid) {
130-
var nc = cloneDeep(ncol);
131-
nc.columnid = colid;
203+
204+
// Add the new pivoted columns
205+
newcols.forEach(function (newColId) {
206+
var nc = cloneDeep(aggColDef);
207+
nc.columnid = newColId;
208+
209+
// ---- Final Refined Type Logic ----
210+
const originalType = (aggColDef.dbtypeid || 'OBJECT').toUpperCase();
211+
const integerTypes = [
212+
'INT',
213+
'INTEGER',
214+
'SMALLINT',
215+
'BIGINT',
216+
'SERIAL',
217+
'SMALLSERIAL',
218+
'BIGSERIAL',
219+
];
220+
const numericTypes = [...integerTypes, 'NUMBER', 'FLOAT', 'DECIMAL', 'NUMERIC', 'MONEY'];
221+
222+
if (aggr === 'COUNT') {
223+
nc.dbtypeid = 'INT';
224+
} else if (aggr === 'AVG') {
225+
// Keep INT type if original was INT-like, otherwise use FLOAT
226+
if (integerTypes.includes(originalType)) {
227+
nc.dbtypeid = aggColDef.dbtypeid; // Preserve original INT-like type
228+
} else {
229+
nc.dbtypeid = 'FLOAT';
230+
}
231+
} else if (aggr === 'SUM' || aggr === 'TOTAL') {
232+
// Preserve numeric types, default to FLOAT otherwise
233+
if (numericTypes.includes(originalType)) {
234+
nc.dbtypeid = aggColDef.dbtypeid;
235+
} else {
236+
nc.dbtypeid = 'FLOAT'; // Default for non-numeric or unknown sums
237+
}
238+
} else if (aggr === 'MIN' || aggr === 'MAX' || aggr === 'FIRST' || aggr === 'LAST') {
239+
// Preserve original type as comparisons work across types
240+
nc.dbtypeid = aggColDef.dbtypeid;
241+
}
242+
// For custom aggregators (AGGR, REDUCE), inherit type from clone, ensure fallback
243+
else if (!nc.dbtypeid) {
244+
nc.dbtypeid = 'OBJECT';
245+
}
246+
132247
query.columns.push(nc);
133248
});
134249
};

src/84from.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ function XLSXLSX(X, filename, opts, cb, idx, query) {
522522
}
523523
},
524524
function (err) {
525-
if(query && query.cb) {
525+
if (query && query.cb) {
526526
query.cb(null, err);
527527
return;
528528
}

test/test2027.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ 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+
assert.deepEqual(alasql.databases['test'].sqlCache['-169125189'].query.data, []);
2323
assert.equal(res.length, 3);
2424

2525
// Delete all rows
2626
alasql('DELETE FROM osoby');
2727

2828
// Assert that the cache is still empty for "data"
2929
// 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, []);
30+
assert.deepEqual(alasql.databases['test'].sqlCache['-169125189'].query.data, []);
3131

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

3838
// Cache should still be empty for "data"
39-
assert.deepEqual(alasql.databases["test"].sqlCache["-169125189"].query.data, []);
39+
assert.deepEqual(alasql.databases['test'].sqlCache['-169125189'].query.data, []);
4040
assert.equal(res2.length, 2);
4141
});
4242
});

0 commit comments

Comments
 (0)