@electric-sql/d2ts
Version:
D2TS is a TypeScript implementation of Differential Dataflow.
126 lines • 5.57 kB
JavaScript
import { map } from '../../operators/map.js';
import { reduce } from './reduce.js';
import { SQLiteContext } from '../context.js';
// Re-export most of the aggregate functions from the main implementation
// since they are compatible with both versions
export { sum, count, avg, min, max, median } from '../../operators/groupBy.js';
/**
* Checks if an aggregate function is a piped aggregate function
*/
function isPipedAggregateFunction(aggregate) {
return 'pipe' in aggregate;
}
/**
* Creates a mode aggregate function that computes the most frequent value in a group
* SQLite-compatible version that uses a serializable object instead of Map
* @param valueExtractor Function to extract a value from each data entry
*/
export function mode(valueExtractor = (v) => v) {
return {
preMap: (data) => {
const value = valueExtractor(data);
// Use a plain object instead of Map for better serialization
const frequencyObj = {};
frequencyObj[value.toString()] = 1;
return frequencyObj;
},
reduce: (values) => {
// Combine all frequency objects
const combinedFrequencies = {};
for (const [freqObj, multiplicity] of values) {
for (const [valueStr, count] of Object.entries(freqObj)) {
const currentCount = combinedFrequencies[valueStr] || 0;
combinedFrequencies[valueStr] = currentCount + count * multiplicity;
}
}
return combinedFrequencies;
},
postMap: (result) => {
const entries = Object.entries(result);
if (entries.length === 0)
return 0;
let modeValue = 0;
let maxFrequency = 0;
for (const [valueStr, frequency] of entries) {
const value = Number(valueStr);
if (frequency > maxFrequency) {
maxFrequency = frequency;
modeValue = value;
}
}
return modeValue;
},
};
}
/**
* Groups data by key and applies multiple aggregate operations
* SQLite version that persists state to a database
*
* @param keyExtractor Function to extract grouping key from data
* @param aggregates Object mapping aggregate names to aggregate functions
* @param db Optional SQLite database (can be injected via context)
*/
export function groupBy(keyExtractor, aggregates, db) {
// Get database from context if not provided explicitly
const database = db || SQLiteContext.getDb();
if (!database) {
throw new Error('SQLite database is required for groupBy operator. ' +
'Provide it as a parameter or use withSQLite() to inject it.');
}
const basicAggregates = Object.fromEntries(Object.entries(aggregates).filter(([_, aggregate]) => !isPipedAggregateFunction(aggregate)));
// @ts-expect-error - TODO: we don't use this yet, but we will
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const pipedAggregates = Object.fromEntries(Object.entries(aggregates).filter(([_, aggregate]) => isPipedAggregateFunction(aggregate)));
return (stream) => {
// Special key to store the original key object
const KEY_SENTINEL = '__original_key__';
// First map to extract keys and pre-aggregate values
const withKeysAndValues = stream.pipe(map((data) => {
const key = keyExtractor(data);
const keyString = JSON.stringify(key);
// Create values object with pre-aggregated values
const values = {};
// Store the original key object
values[KEY_SENTINEL] = key;
// Add pre-aggregated values
for (const [name, aggregate] of Object.entries(basicAggregates)) {
values[name] = aggregate.preMap(data);
}
return [keyString, values];
}));
// Then reduce to compute aggregates, using the SQLite version of reduce
const reduced = withKeysAndValues.pipe(reduce((values) => {
const result = {};
// Get the original key from first value in group
const originalKey = values[0][0][KEY_SENTINEL];
result[KEY_SENTINEL] = originalKey;
// Apply each aggregate function
for (const [name, aggregate] of Object.entries(basicAggregates)) {
const preValues = values.map(([v, m]) => [v[name], m]);
result[name] = aggregate.reduce(preValues);
}
return [[result, 1]];
}, database));
// Finally map to extract the key and include all values
return reduced.pipe(map(([keyString, values]) => {
// Extract the original key
const key = values[KEY_SENTINEL];
// Create intermediate result with key values and aggregate results
const result = {};
// Add key properties to result
Object.assign(result, key);
// Apply postMap if provided
for (const [name, aggregate] of Object.entries(basicAggregates)) {
if (aggregate.postMap) {
result[name] = aggregate.postMap(values[name]);
}
else {
result[name] = values[name];
}
}
// Return with the string key instead of the object
return [keyString, result];
}));
};
}
//# sourceMappingURL=groupBy.js.map