fix(dashboard): use an indexable UTC month-range predicate for used-this-month (#10381)

sumUsageTokensThisMonth() filtered usage_history with
substr(timestamp, 1, 7) = strftime('%Y-%m', 'now') — a substr() expression
SQLite cannot use a range index on, and fragile against any timestamp
that isn't exactly ISO-shaped. Replace with an indexable inclusive-start/
exclusive-end UTC range: timestamp >= <month start> AND timestamp <
<next month start>, matching the ISO 8601 format saveRequestUsage()
already writes.

Adds a boundary regression test: the first instant of the current month
is included, the last instant of the previous month is excluded, and a
next-month row is excluded too (covers the upper bound substr() could
never express).
This commit is contained in:
adevwithpurpose
2026-08-17 23:24:27 -03:00
parent d4fbd952b5
commit d357cacda6
2 changed files with 41 additions and 1 deletions

View File

@@ -25,7 +25,8 @@ export function sumUsageTokensThisMonth(db: SqliteAdapter = getDbInstance()): nu
COALESCE((
SELECT SUM(tokens_input + tokens_output)
FROM usage_history
WHERE substr(timestamp, 1, 7) = strftime('%Y-%m', 'now')
WHERE timestamp >= strftime('%Y-%m-01T00:00:00.000Z','now')
AND timestamp < strftime('%Y-%m-01T00:00:00.000Z','now','+1 month')
), 0) +
COALESCE((
SELECT SUM(total_input_tokens + total_output_tokens)

View File

@@ -60,3 +60,42 @@ test("sumUsageTokensThisMonth includes the current month's raw usage_history row
assert.equal(sumUsageTokensThisMonth(), 400 + 50);
});
// #10509 sweep: the `substr(timestamp, 1, 7) = strftime('%Y-%m', 'now')` predicate was
// fragile/non-indexable (SQLite cannot use a range index on a substr() expression, and a
// non-ISO-shaped timestamp string silently mismatches). Replaced with an indexable UTC
// month-range comparison (`timestamp >= <month start> AND timestamp < <next month start>`).
// This test pins the exact boundary: the first instant of the current month is INCLUDED,
// the last instant of the PREVIOUS month is EXCLUDED, and a NEXT-month row is EXCLUDED too
// (guards the upper-bound half of the range, which substr() could never express directly).
test("sumUsageTokensThisMonth uses an inclusive-start/exclusive-end UTC month range (#10509)", () => {
const db = getDbInstance();
db.exec(`CREATE TABLE IF NOT EXISTS usage_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, provider TEXT, model TEXT, connection_id TEXT,
api_key_id TEXT, api_key_name TEXT, tokens_input INTEGER DEFAULT 0, tokens_output INTEGER DEFAULT 0,
tokens_cache_read INTEGER DEFAULT 0, tokens_cache_creation INTEGER DEFAULT 0, tokens_reasoning INTEGER DEFAULT 0,
service_tier TEXT DEFAULT 'standard', status TEXT, success INTEGER DEFAULT 1, latency_ms INTEGER DEFAULT 0,
ttft_ms INTEGER DEFAULT 0, error_code TEXT, timestamp TEXT NOT NULL);`);
db.exec(`CREATE TABLE IF NOT EXISTS daily_usage_summary (id INTEGER PRIMARY KEY AUTOINCREMENT, provider TEXT NOT NULL, model TEXT NOT NULL, date TEXT NOT NULL, total_requests INTEGER NOT NULL DEFAULT 0, total_input_tokens INTEGER NOT NULL DEFAULT 0, total_output_tokens INTEGER NOT NULL DEFAULT 0, total_cost REAL NOT NULL DEFAULT 0.0, created_at TEXT NOT NULL DEFAULT (datetime('now')));`);
db.exec("DELETE FROM usage_history");
db.exec("DELETE FROM daily_usage_summary");
const monthStart = db
.prepare("SELECT strftime('%Y-%m-01T00:00:00.000Z','now') AS s")
.get() as { s: string };
const nextMonthStart = db
.prepare("SELECT strftime('%Y-%m-01T00:00:00.000Z','now','+1 month') AS s")
.get() as { s: string };
const lastInstantOfPrevMonth = new Date(
new Date(monthStart.s).getTime() - 1
).toISOString();
const insHistory = db.prepare(
"INSERT INTO usage_history (provider, model, tokens_input, tokens_output, timestamp) VALUES (?,?,?,?,?)"
);
insHistory.run("openai", "gpt-4.1", 10, 0, monthStart.s); // first instant of THIS month — included
insHistory.run("openai", "gpt-4.1", 9999, 0, lastInstantOfPrevMonth); // last ms of PREV month — excluded
insHistory.run("openai", "gpt-4.1", 9999, 0, nextMonthStart.s); // first instant of NEXT month — excluded
assert.equal(sumUsageTokensThisMonth(), 10);
});