mirror of https://github.com/mongodb/mongo
338 lines
14 KiB
JavaScript
338 lines
14 KiB
JavaScript
// SERVER-18427: Add $log, $log10, $ln, $pow, and $exp aggregation expressions.
|
|
|
|
import "jstests/libs/query/sbe_assert_error_override.js";
|
|
|
|
import {assertErrorCode} from "jstests/aggregation/extras/utils.js";
|
|
|
|
let coll = db.log_exponential_expressions;
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert({_id: 0, a: 8, b: 2}));
|
|
|
|
const doubleE = 2.7182818284590452;
|
|
const decimalE = NumberDecimal("2.718281828459045235360287471352662");
|
|
const decimal1overE = NumberDecimal("0.3678794411714423215955237701614609");
|
|
|
|
// Given a double, is it an integer?
|
|
function isInteger(n) {
|
|
return !n.toString().includes(".");
|
|
}
|
|
|
|
function isNumberDecimal(n) {
|
|
return n.toString().includes("NumberDecimal");
|
|
}
|
|
|
|
// Helper for testing that op returns expResult.
|
|
function testOp(op, expResult, failMsg) {
|
|
const pipeline = [{$project: {_id: 0, result: op}}];
|
|
const result = coll.aggregate(pipeline).toArray();
|
|
assert.eq(result.length, 1);
|
|
if (expResult === null || isNaN(expResult) || isNumberDecimal(expResult)) {
|
|
assert.eq(result[0].result, expResult, failMsg);
|
|
} else {
|
|
assert.close(result[0].result, expResult, failMsg, 12 /*places*/);
|
|
}
|
|
}
|
|
|
|
// $log, $log10, $ln.
|
|
|
|
// Valid input: numeric/null/NaN, base positive and not equal to 1, arg positive.
|
|
// - NumberDouble
|
|
testOp({$log: [10, 10]}, 1);
|
|
testOp({$log10: [10]}, 1);
|
|
testOp({$ln: [Math.E]}, 1);
|
|
|
|
// Different double and NumberDecimal inputs, verified manually.
|
|
const logTestCases = [
|
|
// Base 8
|
|
{input: 1, base: 8, doubleResult: 0, decResult: NumberDecimal("0E+33")},
|
|
{
|
|
input: 2.5,
|
|
base: 8,
|
|
doubleResult: 0.4406426982957875,
|
|
decResult: NumberDecimal("0.4406426982957874492901064764964633"),
|
|
},
|
|
{
|
|
input: 7,
|
|
base: 8,
|
|
doubleResult: 0.9357849740192015,
|
|
decResult: NumberDecimal("0.9357849740192013691473231057439436"),
|
|
},
|
|
{input: 8, base: 8, doubleResult: 1, decResult: NumberDecimal("1")},
|
|
{
|
|
input: 64,
|
|
base: 8,
|
|
doubleResult: 2,
|
|
decResult: NumberDecimal("2.000000000000000000000000000000000"),
|
|
},
|
|
{
|
|
input: 65,
|
|
base: 8,
|
|
doubleResult: 2.0074559376761516,
|
|
decResult: NumberDecimal("2.007455937676151502755710694582028"),
|
|
},
|
|
|
|
// Base 9, a more unusual base.
|
|
{input: 1, base: 9, doubleResult: 0, decResult: NumberDecimal("0E+33")},
|
|
{
|
|
input: 2.5,
|
|
base: 9,
|
|
doubleResult: 0.41702188357323483,
|
|
decResult: NumberDecimal("0.4170218835732348650487566466679398"),
|
|
},
|
|
{
|
|
input: 4,
|
|
base: 9,
|
|
doubleResult: 0.6309297535714574,
|
|
decResult: NumberDecimal("0.6309297535714574370995271143427609"),
|
|
},
|
|
{input: 9, base: 9, doubleResult: 1, decResult: NumberDecimal("1")},
|
|
{
|
|
input: 10,
|
|
base: 9,
|
|
doubleResult: 1.0479516371446924,
|
|
decResult: NumberDecimal("1.047951637144692302148283761010701"),
|
|
},
|
|
{input: 81, base: 9, doubleResult: 2, decResult: NumberDecimal("2")},
|
|
{
|
|
input: 82,
|
|
base: 9,
|
|
doubleResult: 2.0055843597957064,
|
|
decResult: NumberDecimal("2.005584359795706389324272570756155"),
|
|
},
|
|
|
|
// Base 12.77, an even MORE unusual base.
|
|
{input: 1, base: 12.77, doubleResult: 0, decResult: NumberDecimal("0E+33")},
|
|
{
|
|
input: 2.5,
|
|
base: 12.77,
|
|
doubleResult: 0.3597390013391846,
|
|
decResult: NumberDecimal("0.3597390013391846393309152124110717"),
|
|
},
|
|
{input: 12.77, base: 12.77, doubleResult: 1, decResult: NumberDecimal("1")},
|
|
{
|
|
input: 13,
|
|
base: 12.77,
|
|
doubleResult: 1.0070082433896357,
|
|
decResult: NumberDecimal("1.007008243389635671677137269228113"),
|
|
},
|
|
{
|
|
input: 163.0729,
|
|
base: 12.77,
|
|
doubleResult: 2,
|
|
decResult: NumberDecimal("2.000000000000000000000000000000000"),
|
|
},
|
|
{
|
|
input: 170,
|
|
base: 12.77,
|
|
doubleResult: 2.016332738676606,
|
|
decResult: NumberDecimal("2.016332738676605709114981994718173"),
|
|
},
|
|
];
|
|
for (const test of logTestCases) {
|
|
// If we can cast our input, base (or both) to integer types, test them as well.
|
|
const inputs = [test.input, NumberDecimal(test.input.toString())];
|
|
if (isInteger(test.input)) {
|
|
inputs.push(NumberInt(test.input), NumberLong(test.input));
|
|
}
|
|
const bases = [test.base, NumberDecimal(test.base.toString())];
|
|
if (isInteger(test.base)) {
|
|
bases.push(NumberInt(test.base), NumberLong(test.base));
|
|
}
|
|
for (const input of inputs) {
|
|
for (const base of bases) {
|
|
const hasDecimalInput = isNumberDecimal(input) || isNumberDecimal(base);
|
|
testOp({$log: [input, base]}, hasDecimalInput ? test.decResult : test.doubleResult, test);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Base 10, using $log10
|
|
const log10TestCases = [
|
|
{input: 1, doubleResult: 0, decResult: NumberDecimal("0")},
|
|
{
|
|
input: 2.5,
|
|
doubleResult: 0.3979400086720376,
|
|
decResult: NumberDecimal("0.3979400086720376095725222105510140"),
|
|
},
|
|
{input: 10, doubleResult: 1, decResult: NumberDecimal("1")},
|
|
{
|
|
input: 11,
|
|
doubleResult: 1.041392685158225,
|
|
decResult: NumberDecimal("1.041392685158225040750199971243024"),
|
|
},
|
|
{input: 100, doubleResult: 2, decResult: NumberDecimal("2")},
|
|
{
|
|
input: 101,
|
|
doubleResult: 2.0043213737826426,
|
|
decResult: NumberDecimal("2.004321373782642574275188178222938"),
|
|
},
|
|
];
|
|
for (const test of log10TestCases) {
|
|
// If the input is an integer anyway, test with our integer types as well.
|
|
if (isInteger(test.input)) {
|
|
testOp({$log10: NumberInt(test.input)}, test.doubleResult, test);
|
|
testOp({$log10: NumberLong(test.input)}, test.doubleResult, test);
|
|
}
|
|
testOp({$log10: test.input}, test.doubleResult, test);
|
|
testOp({$log10: NumberDecimal(test.input.toString())}, test.decResult, test);
|
|
}
|
|
|
|
// Base `e`, using $ln.
|
|
const lnTestCases = [
|
|
{input: 1, doubleResult: 0, decResult: NumberDecimal("0")},
|
|
// `e` is about 2.7, so this should be close to 1.
|
|
{
|
|
input: 2.5,
|
|
doubleResult: 0.9162907318741551,
|
|
decResult: NumberDecimal("0.9162907318741550651835272117680110"),
|
|
},
|
|
{
|
|
input: 7,
|
|
doubleResult: 1.9459101490553132,
|
|
decResult: NumberDecimal("1.945910149055313305105352743443180"),
|
|
},
|
|
{
|
|
input: 10,
|
|
doubleResult: 2.302585092994046,
|
|
decResult: NumberDecimal("2.302585092994045684017991454684364"),
|
|
},
|
|
];
|
|
for (const test of lnTestCases) {
|
|
if (isInteger(test.input)) {
|
|
testOp({$ln: NumberInt(test.input)}, test.doubleResult, test);
|
|
testOp({$ln: NumberLong(test.input)}, test.doubleResult, test);
|
|
}
|
|
testOp({$ln: test.input}, test.doubleResult, test);
|
|
testOp({$ln: NumberDecimal(test.input.toString())}, test.decResult, test);
|
|
}
|
|
|
|
// We represent `e` differently with double and NumberDecimal, so test that here.
|
|
testOp({$ln: doubleE}, 1);
|
|
testOp({$ln: 1 / doubleE}, -1);
|
|
// The below answer is actually correct: the input is an approximation of E.
|
|
testOp({$ln: decimalE}, NumberDecimal("0.9999999999999999999999999999999998"));
|
|
testOp({$ln: decimal1overE}, NumberDecimal("-0.9999999999999999999999999999999998"));
|
|
|
|
// All types converted to doubles.
|
|
testOp({$log: [NumberLong("10"), NumberLong("10")]}, 1);
|
|
testOp({$log10: [NumberLong("10")]}, 1);
|
|
testOp({$ln: [NumberLong("1")]}, 0);
|
|
// LLONG_MAX is converted to a double.
|
|
testOp({$log: [NumberLong("9223372036854775807"), 10]}, 18.964889726830812);
|
|
// Null inputs result in null.
|
|
testOp({$log: [null, 10]}, null);
|
|
testOp({$log: [10, null]}, null);
|
|
testOp({$log: [null, NumberDecimal(10)]}, null);
|
|
testOp({$log: [NumberDecimal(10), null]}, null);
|
|
testOp({$log10: [null]}, null);
|
|
testOp({$ln: [null]}, null);
|
|
// NaN inputs result in NaN.
|
|
testOp({$log: [NaN, 10]}, NaN);
|
|
testOp({$log: [10, NaN]}, NaN);
|
|
testOp({$log: [NaN, NumberDecimal(10)]}, NaN);
|
|
testOp({$log: [NumberDecimal(10), NaN]}, NaN);
|
|
testOp({$log10: [NaN]}, NaN);
|
|
testOp({$ln: [NaN]}, NaN);
|
|
|
|
// Test that $log still works when the inputs are field path expressions, meaning that the
|
|
// expression is not eligible for constant folding.
|
|
testOp({$log: ["$a", "$b"]}, 3);
|
|
|
|
// Invalid input: non-numeric/non-null, bases not positive or equal to 1, args not positive.
|
|
|
|
// Args/bases must be numeric or null.
|
|
assertErrorCode(coll, [{$project: {log: {$log: ["string", 5]}}}], 28756);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [5, "string"]}}}], 28757);
|
|
assertErrorCode(coll, [{$project: {log10: {$log10: ["string"]}}}], 28765);
|
|
assertErrorCode(coll, [{$project: {ln: {$ln: ["string"]}}}], 28765);
|
|
// Args/bases cannot equal 0.
|
|
assertErrorCode(coll, [{$project: {log: {$log: [0, 5]}}}], 28758);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [5, 0]}}}], 28759);
|
|
assertErrorCode(coll, [{$project: {log10: {$log10: [0]}}}], 28761);
|
|
assertErrorCode(coll, [{$project: {ln: {$ln: [0]}}}], 28766);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [NumberDecimal(0), NumberDecimal(5)]}}}], 28758);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [NumberDecimal(5), NumberDecimal(0)]}}}], 28759);
|
|
assertErrorCode(coll, [{$project: {log10: {$log10: [NumberDecimal(0)]}}}], 28761);
|
|
assertErrorCode(coll, [{$project: {ln: {$ln: [NumberDecimal(0)]}}}], 28766);
|
|
// Args/bases cannot be negative.
|
|
assertErrorCode(coll, [{$project: {log: {$log: [-1, 5]}}}], 28758);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [5, -1]}}}], 28759);
|
|
assertErrorCode(coll, [{$project: {log10: {$log10: [-1]}}}], 28761);
|
|
assertErrorCode(coll, [{$project: {ln: {$ln: [-1]}}}], 28766);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [NumberDecimal(-1), NumberDecimal(5)]}}}], 28758);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [NumberDecimal(5), NumberDecimal(-1)]}}}], 28759);
|
|
assertErrorCode(coll, [{$project: {log10: {$log10: [NumberDecimal(-1)]}}}], 28761);
|
|
assertErrorCode(coll, [{$project: {ln: {$ln: [NumberDecimal(-1)]}}}], 28766);
|
|
// Base can't equal 1.
|
|
assertErrorCode(coll, [{$project: {log: {$log: [5, 1]}}}], 28759);
|
|
assertErrorCode(coll, [{$project: {log: {$log: [NumberDecimal(5), NumberDecimal(1)]}}}], 28759);
|
|
|
|
// $pow, $exp.
|
|
|
|
// Valid input - numeric/null/NaN.
|
|
|
|
// $pow -- if either input is a double return a double.
|
|
testOp({$pow: [10, 2]}, 100);
|
|
testOp({$pow: [1 / 2, -1]}, 2);
|
|
testOp({$pow: [-2, 2]}, 4);
|
|
testOp({$pow: [NumberInt("2"), 2]}, 4);
|
|
testOp({$pow: [-2, NumberInt("2")]}, 4);
|
|
// $pow -- if either input is a NumberDecimal, return a NumberDecimal
|
|
testOp({$pow: [NumberDecimal("10.0"), -2]}, NumberDecimal("0.01000000000000000000000000000000000"));
|
|
testOp({$pow: [0.5, NumberDecimal("-1")]}, NumberDecimal("2.000000000000000000000000000000000"));
|
|
testOp({$pow: [-2, NumberDecimal("2")]}, NumberDecimal("4.000000000000000000000000000000000"));
|
|
testOp({$pow: [NumberInt("2"), NumberDecimal("2")]}, NumberDecimal("4.000000000000000000000000000000000"));
|
|
testOp({$pow: [NumberDecimal("-2.0"), NumberInt("2")]}, NumberDecimal("4.000000000000000000000000000000000"));
|
|
testOp({$pow: [NumberDecimal("10.0"), 2]}, NumberDecimal("100.0000000000000000000000000000000"));
|
|
|
|
// If exponent is negative and base not -1, 0, or 1, return a double.
|
|
testOp({$pow: [NumberLong("2"), NumberLong("-1")]}, 1 / 2);
|
|
testOp({$pow: [NumberInt("4"), NumberInt("-1")]}, 1 / 4);
|
|
testOp({$pow: [NumberInt("4"), NumberLong("-1")]}, 1 / 4);
|
|
testOp({$pow: [NumberInt("1"), NumberLong("-2")]}, NumberLong("1"));
|
|
testOp({$pow: [NumberInt("-1"), NumberLong("-2")]}, NumberLong("1"));
|
|
testOp({$pow: [NumberLong("-1"), NumberLong("-3")]}, NumberLong("-1"));
|
|
// If result would overflow a long, return a double.
|
|
testOp({$pow: [NumberInt("2"), NumberLong("63")]}, 9223372036854776000);
|
|
// Exact decimal result
|
|
testOp({$pow: [NumberInt("5"), NumberDecimal("-112")]}, NumberDecimal("5192296858534827628530496329220096E-112"));
|
|
|
|
// Result would be incorrect if double were returned.
|
|
testOp({$pow: [NumberInt("3"), NumberInt("35")]}, NumberLong("50031545098999707"));
|
|
|
|
// Else if either input is a long, return a long.
|
|
testOp({$pow: [NumberInt("-2"), NumberLong("63")]}, NumberLong("-9223372036854775808"));
|
|
testOp({$pow: [NumberInt("4"), NumberLong("2")]}, NumberLong("16"));
|
|
testOp({$pow: [NumberLong("4"), NumberInt("2")]}, NumberLong("16"));
|
|
testOp({$pow: [NumberLong("4"), NumberLong("2")]}, NumberLong("16"));
|
|
|
|
// Else return an int if it fits.
|
|
testOp({$pow: [NumberInt("4"), NumberInt("2")]}, 16);
|
|
|
|
// $exp always returns doubles for non-zero non-decimal inputs, since e is a double.
|
|
testOp({$exp: [NumberInt("-1")]}, 1 / Math.E);
|
|
testOp({$exp: [NumberLong("1")]}, Math.E);
|
|
// $exp returns decimal results for decimal inputs
|
|
testOp({$exp: [NumberDecimal("-1")]}, decimal1overE);
|
|
testOp({$exp: [NumberDecimal("1")]}, decimalE);
|
|
// Null input results in null.
|
|
testOp({$pow: [null, 2]}, null);
|
|
testOp({$pow: [1 / 2, null]}, null);
|
|
testOp({$pow: [null, NumberDecimal(2)]}, null);
|
|
testOp({$pow: [NumberDecimal("0.5"), null]}, null);
|
|
testOp({$exp: [null]}, null);
|
|
// NaN input results in NaN.
|
|
testOp({$pow: [NaN, 2]}, NaN);
|
|
testOp({$pow: [1 / 2, NaN]}, NaN);
|
|
testOp({$pow: [NaN, NumberDecimal(2)]}, NumberDecimal("NaN"));
|
|
testOp({$pow: [NumberDecimal("0.5"), NaN]}, NumberDecimal("NaN"));
|
|
testOp({$exp: [NaN]}, NaN);
|
|
|
|
// Invalid inputs - non-numeric/non-null types, or 0 to a negative exponent.
|
|
assertErrorCode(coll, [{$project: {pow: {$pow: [0, NumberLong("-1")]}}}], 28764);
|
|
assertErrorCode(coll, [{$project: {pow: {$pow: ["string", 5]}}}], 28762);
|
|
assertErrorCode(coll, [{$project: {pow: {$pow: [5, "string"]}}}], 28763);
|
|
assertErrorCode(coll, [{$project: {exp: {$exp: ["string"]}}}], 28765);
|
|
assertErrorCode(coll, [{$project: {pow: {$pow: [NumberDecimal(0), NumberLong("-1")]}}}], 28764);
|
|
assertErrorCode(coll, [{$project: {pow: {$pow: ["string", NumberDecimal(5)]}}}], 28762);
|