Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 29 additions & 22 deletions src/423groupby.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ yy.Select.prototype.compileGroup = function (query) {
if (col.aggregatorid === 'SUM') {
if ('funcid' in col.expression) {
let colexp1 = colExpIfFunIdExists(col.expression);
return `'${colas}':(${colexp1})|| typeof ${colexp1} == 'number' ? ${colexp} : null,`;
return `'${colas}':(__alasql_tmp = ${colexp}, (__alasql_tmp instanceof Date) ? null : ((__alasql_tmp || typeof __alasql_tmp == 'number') ? __alasql_tmp : null)),`;
}
return `'${colas}':(${colexp})|| typeof ${colexp} == 'number' ? ${colexp} : null,`;
return `'${colas}':(__alasql_tmp = ${colexp}, (__alasql_tmp instanceof Date) ? null : ((__alasql_tmp || typeof __alasql_tmp == 'number') ? __alasql_tmp : null)),`;
} else if (col.aggregatorid === 'TOTAL') {
if ('funcid' in col.expression) {
let colexp1 = colExpIfFunIdExists(col.expression);
Expand All @@ -117,16 +117,13 @@ yy.Select.prototype.compileGroup = function (query) {
if ('funcid' in col.expression) {
let colexp1 = colExpIfFunIdExists(col.expression);

return `'${colas}': (typeof ${colexp1} == 'number' || typeof ${colexp1} == 'bigint' ? ${colexp} : typeof ${colexp1} == 'object' ?
typeof Number(${colexp1}) == 'number' && ${colexp1}!== null? ${colexp} : null : null),`;
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),`;
}
return `'${colas}': (typeof ${colexp} == 'number' || typeof ${colexp} == 'bigint' ? ${colexp} : typeof ${colexp} == 'object' ?
typeof Number(${colexp}) == 'number' && ${colexp}!== null? ${colexp} : null : null),`;
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),`;
} else if (col.aggregatorid === 'MAX') {
if ('funcid' in col.expression) {
let colexp1 = colExpIfFunIdExists(col.expression);
return `'${colas}' : (typeof ${colexp1} == 'number' || typeof ${colexp1} == 'bigint' ? ${colexp} : typeof ${colexp1} == 'object' ?
typeof Number(${colexp1}) == 'number' ? ${colexp} : null : null),`;
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),`;
}
return `'${colas}' : (typeof ${colexp} == 'number' || typeof ${colexp} == 'bigint' ? ${colexp} : typeof ${colexp} == 'object' ?
typeof Number(${colexp}) == 'number' ? ${colexp} : null : null),`;
Expand All @@ -142,7 +139,7 @@ yy.Select.prototype.compileGroup = function (query) {
query.removeKeys.push(`_SUM_${colas}`);
query.removeKeys.push(`_COUNT_${colas}`);

return `'${colas}':${colexp},'_SUM_${colas}':(${colexp})||0,'_COUNT_${colas}':(typeof ${colexp} == "undefined" || ${colexp} === null) ? 0 : 1,`;
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,`;
} else if (col.aggregatorid === 'AGGR') {
aft += `,g['${colas}']=${col.expression.toJS('g', -1)}`;
return '';
Expand Down Expand Up @@ -190,13 +187,16 @@ yy.Select.prototype.compileGroup = function (query) {
} else if (typeof __g_colas === 'bigint' || typeof __colexp1 === 'bigint') {
g['${colas}'] = BigInt(__g_colas) + BigInt(__colexp);
} else if ((typeof __g_colas !== 'object' && typeof __g_colas !== 'number' && __typeof_colexp1 !== 'object' && __typeof_colexp1 !== 'number') ||
(__g_colas == null || (typeof __g_colas !== 'number' && typeof __g_colas !== 'object')) && (${colexp1} == null || (__typeof_colexp1 !== 'number' && __typeof_colexp1 !== 'object'))) {
(__g_colas == null || (typeof __g_colas !== 'number' && typeof __g_colas !== 'object')) && (${colexp1} == null || (__typeof_colexp1 !== 'number' && __typeof_colexp1 !== 'object'))) {
g['${colas}'] = null;
} else if ((typeof __g_colas !== 'object' && typeof __g_colas !== 'number' && __typeof_colexp1 == 'number') ||
(__g_colas == null && __typeof_colexp1 == 'number')) {
g['${colas}'] = ${colexp};
} else if (typeof __g_colas == 'number' && ${colexp1} == null) {
g['${colas}'] = __g_colas;
} else if (__g_colas instanceof Date || __colexp1 instanceof Date) {
// Date objects cause string concatenation with +=, return null instead
g['${colas}'] = null;
} else {
g['${colas}'] += ${colexp} || 0;
}
Expand Down Expand Up @@ -226,6 +226,9 @@ yy.Select.prototype.compileGroup = function (query) {
g['${colas}'] = __g_colas;
} else if (__g_colas == null && __typeof_colexp == 'number') {
g['${colas}'] = ${colexp};
} else if (__g_colas instanceof Date || __colexp instanceof Date) {
// Date objects cause string concatenation with +=, return null instead
g['${colas}'] = null;
} else {
g['${colas}'] += ${colexp} || 0;
}
Expand Down Expand Up @@ -369,19 +372,19 @@ yy.Select.prototype.compileGroup = function (query) {
}
return (
pre +
`if((g['${colas}'] == null && ${colexp}!== null) ? y = ${colexp} :
(g['${colas}']!== null && ${colexp} == null) ? y = g['${colas}'] :
((y=${colexp}) > g['${colas}'])) {
if(typeof y == 'number' || typeof y == 'bigint') {
g['${colas}'] = y;
} else if(typeof y == 'object' && y instanceof Date) {
g['${colas}'] = y;
} else if(typeof y == 'object' && typeof Number(y) == 'number') {
g['${colas}'] = Number(y);
`if ((g['${colas}'] == null && ${colexp} !== null) ? y = ${colexp} :
(g['${colas}'] !== null && ${colexp} == null) ? y = g['${colas}'] :
((y = ${colexp}) > g['${colas}'])) {
if (typeof y == 'number' || typeof y == 'bigint') {
g['${colas}'] = y;
} else if (typeof y == 'object' && y instanceof Date) {
g['${colas}'] = y;
} else if (typeof y == 'object' && typeof Number(y) == 'number') {
g['${colas}'] = Number(y);
}
} else if(g['${colas}']!== null && typeof g['${colas}'] == 'object' && y instanceof Date) {
} else if (g['${colas}'] !== null && typeof g['${colas}'] == 'object' && y instanceof Date) {
g['${colas}'] = g['${colas}'];
} else if(g['${colas}']!== null && typeof g['${colas}'] == 'object') {
} else if (g['${colas}'] !== null && typeof g['${colas}'] == 'object') {
g['${colas}'] = Number(g['${colas}']);
}` +
post
Expand All @@ -394,7 +397,11 @@ yy.Select.prototype.compileGroup = function (query) {
return `${pre}
y= (${colexp});
g['_COUNT_${colas}'] += (typeof y == "undefined" || y === null) ? 0 : 1;
if (typeof g['_SUM_${colas}'] === 'bigint' || typeof y === 'bigint') {
if (y instanceof Date || (g['_SUM_${colas}'] && g['_SUM_${colas}'] instanceof Date)) {
// AVG on Date objects doesn't make semantic sense - return null
g['_SUM_${colas}'] = null;
g['${colas}'] = null;
} else if (typeof g['_SUM_${colas}'] === 'bigint' || typeof y === 'bigint') {
g['_SUM_${colas}'] = BigInt(g['_SUM_${colas}']);
g['_SUM_${colas}'] += BigInt(y || 0);
g['${colas}'] = BigInt(g['_SUM_${colas}']) / BigInt(g['_COUNT_${colas}']);
Expand Down
16 changes: 14 additions & 2 deletions src/55functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,21 @@ stdlib.RANDOM = function (r) {
};
stdlib.ROUND = function (s, d) {
if (arguments.length == 2) {
return 'Math.round((' + s + ')*Math.pow(10,(' + d + ')))/Math.pow(10,(' + d + '))';
return (
'(__alasql_tmp = (' +
s +
'), (__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,(' +
d +
')))/Math.pow(10,(' +
d +
'))))'
);
} else {
return 'Math.round(' + s + ')';
return (
'(__alasql_tmp = (' +
s +
'), (__alasql_tmp == null || (typeof __alasql_tmp === "string" && __alasql_tmp.trim() === "")) ? null : ((__alasql_tmp = Number(__alasql_tmp)), isNaN(__alasql_tmp) ? null : Math.round(__alasql_tmp)))'
);
}
};
stdlib.CEIL = stdlib.CEILING = function (s) {
Expand Down
123 changes: 123 additions & 0 deletions test/test2147.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
var alasql = require('../dist/alasql.js');
alasql.options.errorlog = true;
var assert = require('assert');

describe('Test 2147 - Aggregate functions on DATETIME', function () {
before(function () {
alasql.fn.DATETIME = function (date) {
return new Date(date);
};
});

var data = [
{id: 1, date: '2025-01-01T01:00:00.000Z'},
{id: 1, date: '2025-01-02T01:00:00.000Z'},
{id: 1, date: '2025-01-03T01:00:00.000Z'},
{id: 2, date: '2025-02-01T01:00:00.000Z'},
{id: 2, date: '2025-02-02T01:00:00.000Z'},
{id: 3, date: '2025-03-01T01:00:00.000Z'},
];

it('MAX on DATETIME', function (done) {
var res = alasql(
'SELECT id, MAX(DATETIME(date)) as maxDate, COUNT(*) as cnt FROM ? GROUP BY id;',
[data]
);

var expected = [
{id: 1, maxDate: new Date('2025-01-03T01:00:00.000Z'), cnt: 3},
{id: 2, maxDate: new Date('2025-02-02T01:00:00.000Z'), cnt: 2},
{id: 3, maxDate: new Date('2025-03-01T01:00:00.000Z'), cnt: 1},
];

assert.deepEqual(res, expected);
done();
});

it('MIN on DATETIME', function (done) {
var res = alasql(
'SELECT id, MIN(DATETIME(date)) as minDate, COUNT(*) as cnt FROM ? GROUP BY id;',
[data]
);

var expected = [
{id: 1, minDate: new Date('2025-01-01T01:00:00.000Z'), cnt: 3},
{id: 2, minDate: new Date('2025-02-01T01:00:00.000Z'), cnt: 2},
{id: 3, minDate: new Date('2025-03-01T01:00:00.000Z'), cnt: 1},
];

assert.deepEqual(res, expected);
done();
});

it('MIN and MAX together on DATETIME', function (done) {
// Both MIN and MAX now work correctly with Date objects
var res = alasql(
'SELECT id, MIN(DATETIME(date)) as minDate, MAX(DATETIME(date)) as maxDate FROM ? GROUP BY id;',
[data]
);

var expected = [
{
id: 1,
minDate: new Date('2025-01-01T01:00:00.000Z'),
maxDate: new Date('2025-01-03T01:00:00.000Z'),
},
{
id: 2,
minDate: new Date('2025-02-01T01:00:00.000Z'),
maxDate: new Date('2025-02-02T01:00:00.000Z'),
},
{
id: 3,
minDate: new Date('2025-03-01T01:00:00.000Z'),
maxDate: new Date('2025-03-01T01:00:00.000Z'),
},
];

assert.deepEqual(res, expected);
done();
});

it('COUNT on DATETIME - natural behavior', function (done) {
// COUNT should work naturally with dates
var res = alasql('SELECT id, COUNT(DATETIME(date)) as dateCount FROM ? GROUP BY id;', [data]);

var expected = [
{id: 1, dateCount: 3},
{id: 2, dateCount: 2},
{id: 3, dateCount: 1},
];

assert.deepEqual(res, expected);
done();
});

it('SUM on DATETIME - returns null for semantic correctness', function (done) {
// SUM on Date objects doesn't make semantic sense, so it returns null
var res = alasql('SELECT id, SUM(DATETIME(date)) as sumTimestamps FROM ? GROUP BY id;', [data]);

var expected = [
{id: 1, sumTimestamps: null},
{id: 2, sumTimestamps: null},
{id: 3, sumTimestamps: null},
];

assert.deepEqual(res, expected);
done();
});

it('AVG on DATETIME - returns null for semantic correctness', function (done) {
// AVG on Date objects doesn't make semantic sense, so it returns null
var res = alasql('SELECT id, AVG(DATETIME(date)) as avgTimestamp FROM ? GROUP BY id;', [data]);

var expected = [
{id: 1, avgTimestamp: null},
{id: 2, avgTimestamp: null},
{id: 3, avgTimestamp: null},
];

assert.deepEqual(res, expected);
done();
});
});
105 changes: 105 additions & 0 deletions test/test2155.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
describe.skip('Test 2155 - ROUND should return null for null input', function () {
it('ROUND(null) should return null, not undefined or 0', function (done) {
var res = alasql('SELECT ROUND(null) as r FROM ?', [[{id: 1}]]);

assert.strictEqual(res[0].r, null, 'ROUND(null) should return null');
done();
});

it('ROUND("123.4") should return 123', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['123.4']);

assert.strictEqual(res[0].r, 123, 'ROUND("123.4") should round to 123');
done();
});

it('ROUND("abc") should return null', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['abc']);

assert.strictEqual(res[0].r, null, 'ROUND("abc") should return null for non-numeric');
done();
});

it('ROUND("") should return null', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['']);

assert.strictEqual(res[0].r, null, 'ROUND("") should return null for empty string');
done();
});

it('ROUND("0") should return 0', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['0']);

assert.strictEqual(res[0].r, 0, 'ROUND("0") should return 0');
done();
});

it('ROUND("null") should return null', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['null']);

assert.strictEqual(res[0].r, null, 'ROUND("null") should return null for string "null"');
done();
});

it('ROUND(" ") should return null', function (done) {
var res = alasql('SELECT ROUND(?) as r', [' ']);

assert.strictEqual(res[0].r, null, 'ROUND(" ") should return null for whitespace');
done();
});

it('ROUND("00") should return 0', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['00']);

assert.strictEqual(res[0].r, 0, 'ROUND("00") should return 0 for variant zero');
done();
});

it('ROUND("0.0") should return 0', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['0.0']);

assert.strictEqual(res[0].r, 0, 'ROUND("0.0") should return 0 for decimal zero');
done();
});

it('ROUND("0 ") should return 0', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['0 ']);

assert.strictEqual(res[0].r, 0, 'ROUND("0 ") should return 0 for spaced zero');
done();
});

it('SUM(ROUND(null)) should return null when all values are null', function (done) {
var data = [{a: null}, {a: null}];

var res = alasql('SELECT SUM(ROUND(a)) as sum_a FROM ?', [data]);

assert.strictEqual(res[0].sum_a, null, 'SUM of all ROUND(null) should be null');
done();
});

it('ROUND with mix of null and numbers', function (done) {
var data = [{a: null}, {a: 5.7}, {a: null}, {a: 3.2}];

var res = alasql('SELECT SUM(ROUND(a)) as sum_a FROM ?', [data]);

assert.strictEqual(res[0].sum_a, 9, 'SUM(ROUND(a)) should sum only non-null values');
done();
});

it('ROUND(string) should return null', function (done) {
var res = alasql('SELECT ROUND(?) as r', ['XYZ']);

assert.strictEqual(res[0].r, null, 'ROUND of non-numeric string should return null');
done();
});

it('SUM(ROUND(string)) should return null when all values are strings', function (done) {
var data = [{e: 'XYZ1'}, {e: 'XYZ2'}];

var res = alasql('SELECT SUM(ROUND(e)) as sum_e FROM ?', [data]);

assert.strictEqual(res[0].sum_e, null, 'SUM of all ROUND(string) should be null');
done();
});
});
3 changes: 3 additions & 0 deletions test/test814.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ describe('Test 814 - XXS or RCE from BRALITERAL', function () {
alasql('CREATE table i_am_a_table;');
//alasql(`INSERT INTO i_am_a_table VALUES (1337);`);
//alasql('INSERT INTO i_am_a_table VALUES (1337);')
// Reset errorlog to ensure security tests throw exceptions
alasql.options.errorlog = false;
});

after(function () {
alasql('drop database test' + test);
alasql.options.errorlog = false;
});

const genPayload = command => `
Expand Down
Loading