Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New method accumulate #800

Merged
merged 3 commits into from
Feb 18, 2025
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
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@
"deno.ns"
]
}
}
}
41 changes: 41 additions & 0 deletions src/class/SimpleWebTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import capitalizeQuery from "../methods/capitalizeQuery.ts";
import logDataWeb from "../helpers/logDataWeb.ts";
import getProjectionParquet from "../helpers/getProjectionParquet.ts";
import unifyColumns from "../helpers/unifyColumns.ts";
import accumulateQuery from "../helpers/accumulateQuery.ts";
// Not working for now
// import getProjection from "../helpers/getProjection.js"

Expand Down Expand Up @@ -2350,6 +2351,46 @@ export default class SimpleWebTable extends Simple {
}
}

/**
* Computes the cumulative sum of values in a column. Don't forget to sort your data first.
*
* @example
* Basic usage
* ```ts
* // Computes the cumulative sum of values in column1 in a new column cumulative.
* await table.accumulate("column1", "cumulative")
* ```
*
* @example
* With categories
* ```ts
* // Computes the cumulative sum of values in column1 in a new column cumulative. Using values in column2 as categories.
* await table.accumulate("column1", "cumulative", { categories: "column2" })
* ```
*
* @param column - The name of the column storing the values to be accumulated.
* @param newColumn - The name of the new column in which the computed values will be stored.
* @param options - An optional object with configuration options:
* @param options.categories - The category or categories to be used for the accumulation.
*/
async accumulate(
column: string,
newColumn: string,
options: {
categories?: string | string[];
} = {},
) {
await queryDB(
this,
accumulateQuery(this.name, column, newColumn, options),
mergeOptions(this, {
table: this.name,
method: "accumulate()",
parameters: { column, newColumn, options },
}),
);
}

/**
* Computes rolling aggregations, like a rolling average. For rows without enough preceding or following rows, returns NULL. For this method to work properly, don't forget to sort your data first.
*
Expand Down
26 changes: 26 additions & 0 deletions src/helpers/accumulateQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import stringToArray from "./stringToArray.ts";

export default function accumulateQuery(
table: string,
column: string,
newColumn: string,
options: {
categories?: string | string[];
} = {},
) {
const categories = options.categories
? stringToArray(options.categories)
: [];
const partition = categories.length > 0
? `PARTITION BY ${categories.map((d) => `"${d}"`).join(", ")} `
: "";

const query =
`CREATE OR REPLACE TABLE data AS SELECT *, ROW_NUMBER() OVER() AS idForAccumulate FROM data;
CREATE OR REPLACE TABLE ${table} AS SELECT *, SUM("${column}") OVER (${partition}ORDER BY idForAccumulate) AS "${newColumn}"
FROM ${table}
ORDER BY idForAccumulate;
ALTER TABLE data DROP "idForAccumulate";`;

return query;
}
10 changes: 5 additions & 5 deletions src/methods/rollingQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function rollingQuery(
? stringToArray(options.categories)
: [];
const partition = categories.length > 0
? `PARTITION BY ${categories.map((d) => `${d}`).join(", ")}`
? `PARTITION BY ${categories.map((d) => `"${d}"`).join(", ")}`
: "";

const tempQuery = `${aggregates[summary]}(${column}) OVER (${partition}
Expand All @@ -36,15 +36,15 @@ export default function rollingQuery(
typeof options.decimals === "number"
? `ROUND(${tempQuery}, ${options.decimals})`
: tempQuery
} AS ${newColumn},
COUNT(${column}) OVER (${partition}
} AS "${newColumn}",
COUNT("${column}") OVER (${partition}
ROWS BETWEEN ${preceding} PRECEDING AND ${following} FOLLOWING) as tempCountForRolling
FROM ${table};
UPDATE ${table} SET ${newColumn} = CASE
UPDATE ${table} SET "${newColumn}" = CASE
WHEN tempCountForRolling != ${preceding + following + 1} THEN NULL
ELSE ${newColumn}
END;
ALTER TABLE ${table} DROP COLUMN tempCountForRolling;
ALTER TABLE ${table} DROP COLUMN "tempCountForRolling";
`;

return query;
Expand Down
85 changes: 85 additions & 0 deletions test/unit/methods/accumulate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { assertEquals } from "jsr:@std/assert";
import SimpleDB from "../../../src/class/SimpleDB.ts";

Deno.test("should add the cumulative sum in a new column", async () => {
const sdb = new SimpleDB();
const table = sdb.newTable("data");
await table.loadArray([
{ key1: 1 },
{ key1: 2 },
{ key1: 3 },
]);
await table.accumulate("key1", "cumulative");
const data = await table.getData();
assertEquals(data, [
{ key1: 1, cumulative: 1 },
{ key1: 2, cumulative: 3 },
{ key1: 3, cumulative: 6 },
]);
await sdb.done();
});
Deno.test("should add the cumulative sum in a new column without reordering the rows", async () => {
const sdb = new SimpleDB();
const table = sdb.newTable("data");
await table.loadArray([
{ key1: 3 },
{ key1: 1 },
{ key1: 2 },
]);
await table.accumulate("key1", "cumulative");
const data = await table.getData();
assertEquals(data, [
{ key1: 3, cumulative: 3 },
{ key1: 1, cumulative: 4 },
{ key1: 2, cumulative: 6 },
]);
await sdb.done();
});
Deno.test("should add the cumulative sum in a new column with categories", async () => {
const sdb = new SimpleDB();
const table = sdb.newTable("data");
await table.loadArray([
{ key1: 6, key2: "b" },
{ key1: 1, key2: "a" },
{ key1: 4, key2: "b" },
{ key1: 2, key2: "a" },
{ key1: 3, key2: "a" },
{ key1: 5, key2: "b" },
]);
await table.accumulate("key1", "cumulative", { categories: "key2" });
const data = await table.getData();
assertEquals(data, [
{ key1: 6, key2: "b", cumulative: 6 },
{ key1: 1, key2: "a", cumulative: 1 },
{ key1: 4, key2: "b", cumulative: 10 },
{ key1: 2, key2: "a", cumulative: 3 },
{ key1: 3, key2: "a", cumulative: 6 },
{ key1: 5, key2: "b", cumulative: 15 },
]);
await sdb.done();
});
Deno.test("should add the cumulative sum in a new column with multiple categories", async () => {
const sdb = new SimpleDB();
const table = sdb.newTable("data");
await table.loadArray([
{ key1: 6, key2: "b", key3: "c" },
{ key1: 1, key2: "a", key3: "c" },
{ key1: 4, key2: "b", key3: "c" },
{ key1: 2, key2: "a", key3: "d" },
{ key1: 3, key2: "a", key3: "c" },
{ key1: 5, key2: "b", key3: "d" },
]);
await table.accumulate("key1", "cumulative", {
categories: ["key2", "key3"],
});
const data = await table.getData();
assertEquals(data, [
{ key1: 6, key2: "b", key3: "c", cumulative: 6 },
{ key1: 1, key2: "a", key3: "c", cumulative: 1 },
{ key1: 4, key2: "b", key3: "c", cumulative: 10 },
{ key1: 2, key2: "a", key3: "d", cumulative: 2 },
{ key1: 3, key2: "a", key3: "c", cumulative: 4 },
{ key1: 5, key2: "b", key3: "d", cumulative: 5 },
]);
await sdb.done();
});
Loading