Skip to content

Commit 2cf0875

Browse files
authored
Support MAX on dates to fix #2147 (#2156)
1 parent 28e5673 commit 2cf0875

File tree

5 files changed

+274
-24
lines changed

5 files changed

+274
-24
lines changed

src/423groupby.js

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ yy.Select.prototype.compileGroup = function (query) {
9898
if (col.aggregatorid === 'SUM') {
9999
if ('funcid' in col.expression) {
100100
let colexp1 = colExpIfFunIdExists(col.expression);
101-
return `'${colas}':(${colexp1})|| typeof ${colexp1} == 'number' ? ${colexp} : null,`;
101+
return `'${colas}':(__alasql_tmp = ${colexp}, (__alasql_tmp instanceof Date) ? null : ((__alasql_tmp || typeof __alasql_tmp == 'number') ? __alasql_tmp : null)),`;
102102
}
103-
return `'${colas}':(${colexp})|| typeof ${colexp} == 'number' ? ${colexp} : null,`;
103+
return `'${colas}':(__alasql_tmp = ${colexp}, (__alasql_tmp instanceof Date) ? null : ((__alasql_tmp || typeof __alasql_tmp == 'number') ? __alasql_tmp : null)),`;
104104
} else if (col.aggregatorid === 'TOTAL') {
105105
if ('funcid' in col.expression) {
106106
let colexp1 = colExpIfFunIdExists(col.expression);
@@ -117,16 +117,13 @@ yy.Select.prototype.compileGroup = function (query) {
117117
if ('funcid' in col.expression) {
118118
let colexp1 = colExpIfFunIdExists(col.expression);
119119

120-
return `'${colas}': (typeof ${colexp1} == 'number' || typeof ${colexp1} == 'bigint' ? ${colexp} : typeof ${colexp1} == 'object' ?
121-
typeof Number(${colexp1}) == 'number' && ${colexp1}!== null? ${colexp} : null : null),`;
120+
return `'${colas}': (__alasql_tmp = ${colexp}, typeof __alasql_tmp == 'number' || typeof __alasql_tmp == 'bigint' || (typeof __alasql_tmp == 'object' && (typeof Number(__alasql_tmp) == 'number' || __alasql_tmp instanceof Date)) ? __alasql_tmp : null),`;
122121
}
123-
return `'${colas}': (typeof ${colexp} == 'number' || typeof ${colexp} == 'bigint' ? ${colexp} : typeof ${colexp} == 'object' ?
124-
typeof Number(${colexp}) == 'number' && ${colexp}!== null? ${colexp} : null : null),`;
122+
return `'${colas}': (__alasql_tmp = ${colexp}, typeof __alasql_tmp == 'number' || typeof __alasql_tmp == 'bigint' || (typeof __alasql_tmp == 'object' && (typeof Number(__alasql_tmp) == 'number' || __alasql_tmp instanceof Date)) ? __alasql_tmp : null),`;
125123
} else if (col.aggregatorid === 'MAX') {
126124
if ('funcid' in col.expression) {
127125
let colexp1 = colExpIfFunIdExists(col.expression);
128-
return `'${colas}' : (typeof ${colexp1} == 'number' || typeof ${colexp1} == 'bigint' ? ${colexp} : typeof ${colexp1} == 'object' ?
129-
typeof Number(${colexp1}) == 'number' ? ${colexp} : null : null),`;
126+
return `'${colas}': (__alasql_tmp = ${colexp}, typeof __alasql_tmp == 'number' || typeof __alasql_tmp == 'bigint' || (typeof __alasql_tmp == 'object' && (typeof Number(__alasql_tmp) == 'number' || __alasql_tmp instanceof Date)) ? __alasql_tmp : null),`;
130127
}
131128
return `'${colas}' : (typeof ${colexp} == 'number' || typeof ${colexp} == 'bigint' ? ${colexp} : typeof ${colexp} == 'object' ?
132129
typeof Number(${colexp}) == 'number' ? ${colexp} : null : null),`;
@@ -142,7 +139,7 @@ yy.Select.prototype.compileGroup = function (query) {
142139
query.removeKeys.push(`_SUM_${colas}`);
143140
query.removeKeys.push(`_COUNT_${colas}`);
144141

145-
return `'${colas}':${colexp},'_SUM_${colas}':(${colexp})||0,'_COUNT_${colas}':(typeof ${colexp} == "undefined" || ${colexp} === null) ? 0 : 1,`;
142+
return `'${colas}':(function() { var t = ${colexp}; return (t instanceof Date) ? null : t; })(),'_SUM_${colas}':(function() { var t = ${colexp}; return (t instanceof Date) ? null : (t || 0); })(),'_COUNT_${colas}':(typeof ${colexp} == "undefined" || ${colexp} === null) ? 0 : 1,`;
146143
} else if (col.aggregatorid === 'AGGR') {
147144
aft += `,g['${colas}']=${col.expression.toJS('g', -1)}`;
148145
return '';
@@ -190,13 +187,16 @@ yy.Select.prototype.compileGroup = function (query) {
190187
} else if (typeof __g_colas === 'bigint' || typeof __colexp1 === 'bigint') {
191188
g['${colas}'] = BigInt(__g_colas) + BigInt(__colexp);
192189
} else if ((typeof __g_colas !== 'object' && typeof __g_colas !== 'number' && __typeof_colexp1 !== 'object' && __typeof_colexp1 !== 'number') ||
193-
(__g_colas == null || (typeof __g_colas !== 'number' && typeof __g_colas !== 'object')) && (${colexp1} == null || (__typeof_colexp1 !== 'number' && __typeof_colexp1 !== 'object'))) {
190+
(__g_colas == null || (typeof __g_colas !== 'number' && typeof __g_colas !== 'object')) && (${colexp1} == null || (__typeof_colexp1 !== 'number' && __typeof_colexp1 !== 'object'))) {
194191
g['${colas}'] = null;
195192
} else if ((typeof __g_colas !== 'object' && typeof __g_colas !== 'number' && __typeof_colexp1 == 'number') ||
196193
(__g_colas == null && __typeof_colexp1 == 'number')) {
197194
g['${colas}'] = ${colexp};
198195
} else if (typeof __g_colas == 'number' && ${colexp1} == null) {
199196
g['${colas}'] = __g_colas;
197+
} else if (__g_colas instanceof Date || __colexp1 instanceof Date) {
198+
// Date objects cause string concatenation with +=, return null instead
199+
g['${colas}'] = null;
200200
} else {
201201
g['${colas}'] += ${colexp} || 0;
202202
}
@@ -226,6 +226,9 @@ yy.Select.prototype.compileGroup = function (query) {
226226
g['${colas}'] = __g_colas;
227227
} else if (__g_colas == null && __typeof_colexp == 'number') {
228228
g['${colas}'] = ${colexp};
229+
} else if (__g_colas instanceof Date || __colexp instanceof Date) {
230+
// Date objects cause string concatenation with +=, return null instead
231+
g['${colas}'] = null;
229232
} else {
230233
g['${colas}'] += ${colexp} || 0;
231234
}
@@ -369,19 +372,19 @@ yy.Select.prototype.compileGroup = function (query) {
369372
}
370373
return (
371374
pre +
372-
`if((g['${colas}'] == null && ${colexp}!== null) ? y = ${colexp} :
373-
(g['${colas}']!== null && ${colexp} == null) ? y = g['${colas}'] :
374-
((y=${colexp}) > g['${colas}'])) {
375-
if(typeof y == 'number' || typeof y == 'bigint') {
376-
g['${colas}'] = y;
377-
} else if(typeof y == 'object' && y instanceof Date) {
378-
g['${colas}'] = y;
379-
} else if(typeof y == 'object' && typeof Number(y) == 'number') {
380-
g['${colas}'] = Number(y);
375+
`if ((g['${colas}'] == null && ${colexp} !== null) ? y = ${colexp} :
376+
(g['${colas}'] !== null && ${colexp} == null) ? y = g['${colas}'] :
377+
((y = ${colexp}) > g['${colas}'])) {
378+
if (typeof y == 'number' || typeof y == 'bigint') {
379+
g['${colas}'] = y;
380+
} else if (typeof y == 'object' && y instanceof Date) {
381+
g['${colas}'] = y;
382+
} else if (typeof y == 'object' && typeof Number(y) == 'number') {
383+
g['${colas}'] = Number(y);
381384
}
382-
} else if(g['${colas}']!== null && typeof g['${colas}'] == 'object' && y instanceof Date) {
385+
} else if (g['${colas}'] !== null && typeof g['${colas}'] == 'object' && y instanceof Date) {
383386
g['${colas}'] = g['${colas}'];
384-
} else if(g['${colas}']!== null && typeof g['${colas}'] == 'object') {
387+
} else if (g['${colas}'] !== null && typeof g['${colas}'] == 'object') {
385388
g['${colas}'] = Number(g['${colas}']);
386389
}` +
387390
post
@@ -394,7 +397,11 @@ yy.Select.prototype.compileGroup = function (query) {
394397
return `${pre}
395398
y= (${colexp});
396399
g['_COUNT_${colas}'] += (typeof y == "undefined" || y === null) ? 0 : 1;
397-
if (typeof g['_SUM_${colas}'] === 'bigint' || typeof y === 'bigint') {
400+
if (y instanceof Date || (g['_SUM_${colas}'] && g['_SUM_${colas}'] instanceof Date)) {
401+
// AVG on Date objects doesn't make semantic sense - return null
402+
g['_SUM_${colas}'] = null;
403+
g['${colas}'] = null;
404+
} else if (typeof g['_SUM_${colas}'] === 'bigint' || typeof y === 'bigint') {
398405
g['_SUM_${colas}'] = BigInt(g['_SUM_${colas}']);
399406
g['_SUM_${colas}'] += BigInt(y || 0);
400407
g['${colas}'] = BigInt(g['_SUM_${colas}']) / BigInt(g['_COUNT_${colas}']);

src/55functions.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,21 @@ stdlib.RANDOM = function (r) {
207207
};
208208
stdlib.ROUND = function (s, d) {
209209
if (arguments.length == 2) {
210-
return 'Math.round((' + s + ')*Math.pow(10,(' + d + ')))/Math.pow(10,(' + d + '))';
210+
return (
211+
'(__alasql_tmp = (' +
212+
s +
213+
'), (__alasql_tmp == null || (typeof __alasql_tmp === "string" && __alasql_tmp.trim() === "")) ? null : ((__alasql_tmp = Number(__alasql_tmp)), isNaN(__alasql_tmp) ? null : Math.round(__alasql_tmp*Math.pow(10,(' +
214+
d +
215+
')))/Math.pow(10,(' +
216+
d +
217+
'))))'
218+
);
211219
} else {
212-
return 'Math.round(' + s + ')';
220+
return (
221+
'(__alasql_tmp = (' +
222+
s +
223+
'), (__alasql_tmp == null || (typeof __alasql_tmp === "string" && __alasql_tmp.trim() === "")) ? null : ((__alasql_tmp = Number(__alasql_tmp)), isNaN(__alasql_tmp) ? null : Math.round(__alasql_tmp)))'
224+
);
213225
}
214226
};
215227
stdlib.CEIL = stdlib.CEILING = function (s) {

test/test2147.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
var alasql = require('../dist/alasql.js');
2+
alasql.options.errorlog = true;
3+
var assert = require('assert');
4+
5+
describe('Test 2147 - Aggregate functions on DATETIME', function () {
6+
before(function () {
7+
alasql.fn.DATETIME = function (date) {
8+
return new Date(date);
9+
};
10+
});
11+
12+
var data = [
13+
{id: 1, date: '2025-01-01T01:00:00.000Z'},
14+
{id: 1, date: '2025-01-02T01:00:00.000Z'},
15+
{id: 1, date: '2025-01-03T01:00:00.000Z'},
16+
{id: 2, date: '2025-02-01T01:00:00.000Z'},
17+
{id: 2, date: '2025-02-02T01:00:00.000Z'},
18+
{id: 3, date: '2025-03-01T01:00:00.000Z'},
19+
];
20+
21+
it('MAX on DATETIME', function (done) {
22+
var res = alasql(
23+
'SELECT id, MAX(DATETIME(date)) as maxDate, COUNT(*) as cnt FROM ? GROUP BY id;',
24+
[data]
25+
);
26+
27+
var expected = [
28+
{id: 1, maxDate: new Date('2025-01-03T01:00:00.000Z'), cnt: 3},
29+
{id: 2, maxDate: new Date('2025-02-02T01:00:00.000Z'), cnt: 2},
30+
{id: 3, maxDate: new Date('2025-03-01T01:00:00.000Z'), cnt: 1},
31+
];
32+
33+
assert.deepEqual(res, expected);
34+
done();
35+
});
36+
37+
it('MIN on DATETIME', function (done) {
38+
var res = alasql(
39+
'SELECT id, MIN(DATETIME(date)) as minDate, COUNT(*) as cnt FROM ? GROUP BY id;',
40+
[data]
41+
);
42+
43+
var expected = [
44+
{id: 1, minDate: new Date('2025-01-01T01:00:00.000Z'), cnt: 3},
45+
{id: 2, minDate: new Date('2025-02-01T01:00:00.000Z'), cnt: 2},
46+
{id: 3, minDate: new Date('2025-03-01T01:00:00.000Z'), cnt: 1},
47+
];
48+
49+
assert.deepEqual(res, expected);
50+
done();
51+
});
52+
53+
it('MIN and MAX together on DATETIME', function (done) {
54+
// Both MIN and MAX now work correctly with Date objects
55+
var res = alasql(
56+
'SELECT id, MIN(DATETIME(date)) as minDate, MAX(DATETIME(date)) as maxDate FROM ? GROUP BY id;',
57+
[data]
58+
);
59+
60+
var expected = [
61+
{
62+
id: 1,
63+
minDate: new Date('2025-01-01T01:00:00.000Z'),
64+
maxDate: new Date('2025-01-03T01:00:00.000Z'),
65+
},
66+
{
67+
id: 2,
68+
minDate: new Date('2025-02-01T01:00:00.000Z'),
69+
maxDate: new Date('2025-02-02T01:00:00.000Z'),
70+
},
71+
{
72+
id: 3,
73+
minDate: new Date('2025-03-01T01:00:00.000Z'),
74+
maxDate: new Date('2025-03-01T01:00:00.000Z'),
75+
},
76+
];
77+
78+
assert.deepEqual(res, expected);
79+
done();
80+
});
81+
82+
it('COUNT on DATETIME - natural behavior', function (done) {
83+
// COUNT should work naturally with dates
84+
var res = alasql('SELECT id, COUNT(DATETIME(date)) as dateCount FROM ? GROUP BY id;', [data]);
85+
86+
var expected = [
87+
{id: 1, dateCount: 3},
88+
{id: 2, dateCount: 2},
89+
{id: 3, dateCount: 1},
90+
];
91+
92+
assert.deepEqual(res, expected);
93+
done();
94+
});
95+
96+
it('SUM on DATETIME - returns null for semantic correctness', function (done) {
97+
// SUM on Date objects doesn't make semantic sense, so it returns null
98+
var res = alasql('SELECT id, SUM(DATETIME(date)) as sumTimestamps FROM ? GROUP BY id;', [data]);
99+
100+
var expected = [
101+
{id: 1, sumTimestamps: null},
102+
{id: 2, sumTimestamps: null},
103+
{id: 3, sumTimestamps: null},
104+
];
105+
106+
assert.deepEqual(res, expected);
107+
done();
108+
});
109+
110+
it('AVG on DATETIME - returns null for semantic correctness', function (done) {
111+
// AVG on Date objects doesn't make semantic sense, so it returns null
112+
var res = alasql('SELECT id, AVG(DATETIME(date)) as avgTimestamp FROM ? GROUP BY id;', [data]);
113+
114+
var expected = [
115+
{id: 1, avgTimestamp: null},
116+
{id: 2, avgTimestamp: null},
117+
{id: 3, avgTimestamp: null},
118+
];
119+
120+
assert.deepEqual(res, expected);
121+
done();
122+
});
123+
});

test/test2155.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
describe.skip('Test 2155 - ROUND should return null for null input', function () {
2+
it('ROUND(null) should return null, not undefined or 0', function (done) {
3+
var res = alasql('SELECT ROUND(null) as r FROM ?', [[{id: 1}]]);
4+
5+
assert.strictEqual(res[0].r, null, 'ROUND(null) should return null');
6+
done();
7+
});
8+
9+
it('ROUND("123.4") should return 123', function (done) {
10+
var res = alasql('SELECT ROUND(?) as r', ['123.4']);
11+
12+
assert.strictEqual(res[0].r, 123, 'ROUND("123.4") should round to 123');
13+
done();
14+
});
15+
16+
it('ROUND("abc") should return null', function (done) {
17+
var res = alasql('SELECT ROUND(?) as r', ['abc']);
18+
19+
assert.strictEqual(res[0].r, null, 'ROUND("abc") should return null for non-numeric');
20+
done();
21+
});
22+
23+
it('ROUND("") should return null', function (done) {
24+
var res = alasql('SELECT ROUND(?) as r', ['']);
25+
26+
assert.strictEqual(res[0].r, null, 'ROUND("") should return null for empty string');
27+
done();
28+
});
29+
30+
it('ROUND("0") should return 0', function (done) {
31+
var res = alasql('SELECT ROUND(?) as r', ['0']);
32+
33+
assert.strictEqual(res[0].r, 0, 'ROUND("0") should return 0');
34+
done();
35+
});
36+
37+
it('ROUND("null") should return null', function (done) {
38+
var res = alasql('SELECT ROUND(?) as r', ['null']);
39+
40+
assert.strictEqual(res[0].r, null, 'ROUND("null") should return null for string "null"');
41+
done();
42+
});
43+
44+
it('ROUND(" ") should return null', function (done) {
45+
var res = alasql('SELECT ROUND(?) as r', [' ']);
46+
47+
assert.strictEqual(res[0].r, null, 'ROUND(" ") should return null for whitespace');
48+
done();
49+
});
50+
51+
it('ROUND("00") should return 0', function (done) {
52+
var res = alasql('SELECT ROUND(?) as r', ['00']);
53+
54+
assert.strictEqual(res[0].r, 0, 'ROUND("00") should return 0 for variant zero');
55+
done();
56+
});
57+
58+
it('ROUND("0.0") should return 0', function (done) {
59+
var res = alasql('SELECT ROUND(?) as r', ['0.0']);
60+
61+
assert.strictEqual(res[0].r, 0, 'ROUND("0.0") should return 0 for decimal zero');
62+
done();
63+
});
64+
65+
it('ROUND("0 ") should return 0', function (done) {
66+
var res = alasql('SELECT ROUND(?) as r', ['0 ']);
67+
68+
assert.strictEqual(res[0].r, 0, 'ROUND("0 ") should return 0 for spaced zero');
69+
done();
70+
});
71+
72+
it('SUM(ROUND(null)) should return null when all values are null', function (done) {
73+
var data = [{a: null}, {a: null}];
74+
75+
var res = alasql('SELECT SUM(ROUND(a)) as sum_a FROM ?', [data]);
76+
77+
assert.strictEqual(res[0].sum_a, null, 'SUM of all ROUND(null) should be null');
78+
done();
79+
});
80+
81+
it('ROUND with mix of null and numbers', function (done) {
82+
var data = [{a: null}, {a: 5.7}, {a: null}, {a: 3.2}];
83+
84+
var res = alasql('SELECT SUM(ROUND(a)) as sum_a FROM ?', [data]);
85+
86+
assert.strictEqual(res[0].sum_a, 9, 'SUM(ROUND(a)) should sum only non-null values');
87+
done();
88+
});
89+
90+
it('ROUND(string) should return null', function (done) {
91+
var res = alasql('SELECT ROUND(?) as r', ['XYZ']);
92+
93+
assert.strictEqual(res[0].r, null, 'ROUND of non-numeric string should return null');
94+
done();
95+
});
96+
97+
it('SUM(ROUND(string)) should return null when all values are strings', function (done) {
98+
var data = [{e: 'XYZ1'}, {e: 'XYZ2'}];
99+
100+
var res = alasql('SELECT SUM(ROUND(e)) as sum_e FROM ?', [data]);
101+
102+
assert.strictEqual(res[0].sum_e, null, 'SUM of all ROUND(string) should be null');
103+
done();
104+
});
105+
});

test/test814.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ describe('Test 814 - XXS or RCE from BRALITERAL', function () {
1212
alasql('CREATE table i_am_a_table;');
1313
//alasql(`INSERT INTO i_am_a_table VALUES (1337);`);
1414
//alasql('INSERT INTO i_am_a_table VALUES (1337);')
15+
// Reset errorlog to ensure security tests throw exceptions
16+
alasql.options.errorlog = false;
1517
});
1618

1719
after(function () {
1820
alasql('drop database test' + test);
21+
alasql.options.errorlog = false;
1922
});
2023

2124
const genPayload = command => `

0 commit comments

Comments
 (0)