mingo
Version:
MongoDB query language for in-memory objects
173 lines (172 loc) • 5.79 kB
JavaScript
import { concat, Lazy } from "../../lazy";
import {
assert,
HashMap,
isArray,
isDate,
isInteger,
isNil,
isNumber,
isObject,
isString,
resolve
} from "../../util";
import { TIME_UNITS } from "../expression/date/_internal";
import { $dateAdd } from "../expression/date/dateAdd";
import { $sort } from "./sort";
const OP = "$densify";
function $densify(coll, expr, options) {
const { step, bounds, unit } = expr.range;
if (unit) {
assert(
TIME_UNITS.includes(unit),
`${OP} 'range.unit' value is not supported.`
);
assert(
isInteger(step) && step > 0,
`${OP} 'range.step' must resolve to integer if 'range.unit' is specified.`
);
} else {
assert(isNumber(step), `${OP} 'range.step' must resolve to number.`);
}
if (isArray(bounds)) {
assert(
!!bounds && bounds.length === 2,
`${OP} 'range.bounds' must have exactly two elements.`
);
assert(
(bounds.every(isNumber) || bounds.every(isDate)) && bounds[0] < bounds[1],
`${OP} 'range.bounds' must be ordered lower then upper.`
);
if (unit) {
assert(
bounds.every(isDate),
`${OP} 'range.bounds' must be dates if 'range.unit' is specified.`
);
}
}
if (expr.partitionByFields) {
assert(
isArray(expr.partitionByFields),
`${OP} 'partitionByFields' must resolve to array of strings.`
);
}
const partitionByFields = expr.partitionByFields ?? [];
coll = $sort(coll, { [expr.field]: 1 }, options);
const computeNextValue = (value) => {
return isNumber(value) ? value + step : $dateAdd({}, { startDate: value, unit, amount: step }, options);
};
const isValidUnit = !!unit && TIME_UNITS.includes(unit);
const getFieldValue = (o) => {
const v = resolve(o, expr.field);
assert(
isNil(v) || isDate(v) && isValidUnit || isNumber(v) && !isValidUnit,
`${OP} Densify field type must be numeric with 'unit' unspecified, or a date with 'unit' specified.`
);
return v;
};
const peekItem = new Array();
const nilFieldsIterator = Lazy(() => {
const item = coll.next();
const fieldValue = getFieldValue(item.value);
if (isNil(fieldValue)) return item;
peekItem.push(item);
return { done: true };
});
const nextDensifyValueMap = HashMap.init();
const [lower, upper] = isArray(bounds) ? bounds : [bounds, bounds];
let maxFieldValue;
const updateMaxFieldValue = (value) => {
maxFieldValue = maxFieldValue === void 0 || maxFieldValue < value ? value : maxFieldValue;
};
const rootKey = [];
const densifyIterator = Lazy(() => {
const item = peekItem.pop() || coll.next();
if (item.done) return item;
let partitionKey = rootKey;
if (isArray(partitionByFields)) {
partitionKey = partitionByFields.map(
(k) => resolve(item.value, k)
);
assert(
partitionKey.every(isString),
"$densify: Partition fields must evaluate to string values."
);
}
assert(isObject(item.value), "$densify: collection must contain documents");
const itemValue = getFieldValue(item.value);
if (!nextDensifyValueMap.has(partitionKey)) {
if (lower == "full") {
if (!nextDensifyValueMap.has(rootKey)) {
nextDensifyValueMap.set(rootKey, itemValue);
}
nextDensifyValueMap.set(
partitionKey,
nextDensifyValueMap.get(rootKey)
);
} else if (lower == "partition") {
nextDensifyValueMap.set(partitionKey, itemValue);
} else {
nextDensifyValueMap.set(partitionKey, lower);
}
}
const densifyValue = nextDensifyValueMap.get(partitionKey);
if (
// current item field value is lower than current densify value.
itemValue <= densifyValue || // range value equals or exceeds upper bound
upper != "full" && upper != "partition" && densifyValue >= upper
) {
if (densifyValue <= itemValue) {
nextDensifyValueMap.set(partitionKey, computeNextValue(densifyValue));
}
updateMaxFieldValue(itemValue);
return item;
}
nextDensifyValueMap.set(partitionKey, computeNextValue(densifyValue));
updateMaxFieldValue(densifyValue);
const denseObj = { [expr.field]: densifyValue };
if (partitionKey) {
for (let i = 0; i < partitionKey.length; i++) {
denseObj[partitionByFields[i]] = partitionKey[i];
}
}
peekItem.push(item);
return { done: false, value: denseObj };
});
if (lower !== "full") return concat(nilFieldsIterator, densifyIterator);
let paritionIndex = -1;
let partitionKeysSet;
const fullBoundsIterator = Lazy(() => {
if (paritionIndex === -1) {
const fullDensifyValue = nextDensifyValueMap.get(rootKey);
nextDensifyValueMap.delete(rootKey);
partitionKeysSet = Array.from(nextDensifyValueMap.keys());
if (partitionKeysSet.length === 0) {
partitionKeysSet.push(rootKey);
nextDensifyValueMap.set(rootKey, fullDensifyValue);
}
paritionIndex++;
}
do {
const partitionKey = partitionKeysSet[paritionIndex];
const partitionMaxValue = nextDensifyValueMap.get(partitionKey);
if (partitionMaxValue < maxFieldValue) {
nextDensifyValueMap.set(
partitionKey,
computeNextValue(partitionMaxValue)
);
const denseObj = { [expr.field]: partitionMaxValue };
for (let i = 0; i < partitionKey.length; i++) {
denseObj[partitionByFields[i]] = partitionKey[i];
}
return { done: false, value: denseObj };
}
paritionIndex++;
} while (paritionIndex < partitionKeysSet.length);
return { done: true };
});
return concat(nilFieldsIterator, densifyIterator, fullBoundsIterator);
}
export {
$densify
};