UNPKG

@electric-sql/d2ts

Version:

D2TS is a TypeScript implementation of Differential Dataflow.

126 lines 5.57 kB
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