From 41265334f0358efdebeb46e7979834fffad426ca Mon Sep 17 00:00:00 2001 From: BoxBoxJason Date: Fri, 26 Dec 2025 19:12:10 +0100 Subject: [PATCH] perf: optimize database & queries Signed-off-by: BoxBoxJason --- src/database/controller/progressions.ts | 55 ++++- src/database/controller/timespent.ts | 85 +++----- src/database/model/migrations.ts | 56 ++++- src/database/model/tables/Achievement.ts | 10 + src/database/model/tables/DailySession.ts | 37 ++++ src/listeners/files.ts | 54 +++-- src/test/controllers.test.ts | 26 +++ src/test/listeners/files.test.ts | 246 ++++++++++++++-------- src/test/migrations.test.ts | 58 +++++ src/test/tables.test.ts | 26 +++ 10 files changed, 467 insertions(+), 186 deletions(-) create mode 100644 src/test/migrations.test.ts diff --git a/src/database/controller/progressions.ts b/src/database/controller/progressions.ts index 88723b4..99e4806 100644 --- a/src/database/controller/progressions.ts +++ b/src/database/controller/progressions.ts @@ -62,9 +62,10 @@ export namespace ProgressionController { { name: criteriaName }, increase ); - const updatedAchievements = await Progression.achieveCompletedAchievements( - updatedProgressionsIds.map((progression) => progression.id) - ); + const updatedAchievements = + await Progression.achieveCompletedAchievements( + updatedProgressionsIds.map((progression) => progression.id) + ); let awardedPoints = 0; for (let achievement of updatedAchievements) { // Notify the user of the unlocked achievement @@ -98,9 +99,10 @@ export namespace ProgressionController { value.toString(), maximize ); - const updatedAchievements = await Progression.achieveCompletedAchievements( - updatedProgressionsIds.map((progression) => progression.id) - ); + const updatedAchievements = + await Progression.achieveCompletedAchievements( + updatedProgressionsIds.map((progression) => progression.id) + ); for (let achievement of updatedAchievements) { // Notify the user of the unlocked achievement awardAchievement(achievement); @@ -109,4 +111,45 @@ export namespace ProgressionController { logger.error(`Failed to update progression: ${(error as Error).message}`); } } + + /** + * Update multiple progression values and check if any achievements have been unlocked + * + * @memberof achievements + * @function updateProgressions + * + * @param {Array<{name: string, value: string | number | Date | boolean}>} updates - The list of progressions to update + * @returns {Promise} + */ + export async function updateProgressions( + updates: Array<{ + name: string; + value: string | number | Date | boolean; + maximize?: boolean; + }> + ): Promise { + try { + const allUpdatedIds: number[] = []; + for (const update of updates) { + const updatedProgressionsIds = await Progression.updateValue( + { name: update.name }, + update.value.toString(), + update.maximize + ); + allUpdatedIds.push(...updatedProgressionsIds.map((p) => p.id)); + } + + if (allUpdatedIds.length > 0) { + const updatedAchievements = + await Progression.achieveCompletedAchievements(allUpdatedIds); + for (let achievement of updatedAchievements) { + awardAchievement(achievement); + } + } + } catch (error) { + logger.error( + `Failed to update progressions: ${(error as Error).message}` + ); + } + } } diff --git a/src/database/controller/timespent.ts b/src/database/controller/timespent.ts index 47d116c..bf85a75 100644 --- a/src/database/controller/timespent.ts +++ b/src/database/controller/timespent.ts @@ -17,72 +17,53 @@ export namespace TimeSpentController { export async function updateTimeSpentFromSessions(): Promise { const currentDate = new Date(); const currentDateString = currentDate.toISOString().split("T")[0]; - // Update the daily counter - logger.debug("JOB: Updating daily time spent"); - const dailyTimeSpent = await DailySession.calculateDuration( - currentDateString, - currentDateString - ); - await ProgressionController.updateProgression( - constants.criteria.DAILY_TIME_SPENT, - dailyTimeSpent.toString() - ); - // Update the bi-monthly counter - logger.debug("JOB: Updating bi-monthly time spent"); const fourteenDaysAgo = new Date( currentDate.getTime() - 14 * 24 * 60 * 60 * 1000 ); const fourteenDaysAgoString = fourteenDaysAgo.toISOString().split("T")[0]; - const biMonthlyTimeSpent = await DailySession.calculateDuration( - fourteenDaysAgoString, - currentDateString - ); - await ProgressionController.updateProgression( - constants.criteria.TWO_WEEKS_TIME_SPENT, - biMonthlyTimeSpent.toString() - ); - // Update the monthly counter - logger.debug("JOB: Updating monthly time spent"); const firstDayOfMonth = new Date( currentDate.getFullYear(), currentDate.getMonth(), 1 ); const firstDayOfMonthString = firstDayOfMonth.toISOString().split("T")[0]; - const monthlyTimeSpent = await DailySession.calculateDuration( - firstDayOfMonthString, - currentDateString - ); - await ProgressionController.updateProgression( - constants.criteria.MONTHLY_TIME_SPENT, - monthlyTimeSpent.toString() - ); - // Update the yearly counter - logger.debug("JOB: Updating yearly time spent"); const firstDayOfYear = new Date(currentDate.getFullYear(), 0, 1); const firstDayOfYearString = firstDayOfYear.toISOString().split("T")[0]; - const yearlyTimeSpent = await DailySession.calculateDuration( - firstDayOfYearString, - currentDateString - ); - await ProgressionController.updateProgression( - constants.criteria.YEARLY_TIME_SPENT, - yearlyTimeSpent.toString() - ); - // Update the total counter - logger.debug("JOB: Updating total time spent"); - const totalDuration = await DailySession.calculateDuration( - "1970-01-01", - currentDateString - ); - await ProgressionController.updateProgression( - constants.criteria.TOTAL_TIME_SPENT, - totalDuration.toString() - ); + logger.debug("JOB: Updating time spent counters"); + + const stats = await DailySession.getStatsSummary( + currentDateString, + fourteenDaysAgoString, + firstDayOfMonthString, + firstDayOfYearString + ); + + await ProgressionController.updateProgressions([ + { + name: constants.criteria.DAILY_TIME_SPENT, + value: stats.daily.toString(), + }, + { + name: constants.criteria.TWO_WEEKS_TIME_SPENT, + value: stats.twoWeeks.toString(), + }, + { + name: constants.criteria.MONTHLY_TIME_SPENT, + value: stats.monthly.toString(), + }, + { + name: constants.criteria.YEARLY_TIME_SPENT, + value: stats.yearly.toString(), + }, + { + name: constants.criteria.TOTAL_TIME_SPENT, + value: stats.total.toString(), + }, + ]); logger.debug("JOB: Time spent counters updated"); } @@ -140,7 +121,9 @@ export namespace TimeSpentController { yesterdayDate.setDate(yesterdayDate.getDate() - 1); const yesterdayDateString = yesterdayDate.toISOString().split("T")[0]; - const yesterdaySession = await DailySession.getOrCreate(yesterdayDateString); + const yesterdaySession = await DailySession.getOrCreate( + yesterdayDateString + ); let newStreak = 1; diff --git a/src/database/model/migrations.ts b/src/database/model/migrations.ts index ee76391..03cc249 100644 --- a/src/database/model/migrations.ts +++ b/src/database/model/migrations.ts @@ -130,6 +130,32 @@ export async function applyMigration( duration INTEGER NOT NULL )` ); + + const indexes_to_create: { [key: string]: string } = { + // Index for achievement_criterias progression_id (Critical for checkAchievements) + idx_achievement_criterias_progression_id: + "achievement_criterias(progression_id)", + // Index for achievements category filtering (UI) + idx_achievements_category: "achievements(category)", + // Index for achievements group filtering (UI) + idx_achievements_group: 'achievements("group")', + // Index for achievements achieved filtering (UI) + idx_achievements_achieved: "achievements(achieved)", + // Index for achievement_labels filtering (UI) + idx_achievement_labels_label: "achievement_labels(label)", + // Index for achievement_requirements requirement_id (Critical for achievable filter) + idx_achievement_requirements_requirement_id: + "achievement_requirements(requirement_id)", + }; + + for (const [indexName, indexColumns] of Object.entries( + indexes_to_create + )) { + db.run( + `CREATE INDEX IF NOT EXISTS ${indexName} ON ${indexColumns}` + ); + } + db.run("COMMIT"); } catch (error) { db.run("ROLLBACK"); @@ -137,15 +163,31 @@ export async function applyMigration( } }, down: () => { + const tables_to_drop = [ + "schema_version", + "achievements", + "achievement_requirements", + "progressions", + "achievement_criterias", + "achievement_labels", + "daily_sessions", + ]; + const indexes_to_drop = [ + "idx_achievement_criterias_progression_id", + "idx_achievements_category", + "idx_achievements_group", + "idx_achievements_achieved", + "idx_achievement_labels_label", + "idx_achievement_requirements_requirement_id", + ]; db.run("BEGIN TRANSACTION"); try { - db.run("DROP TABLE IF EXISTS schema_version"); - db.run("DROP TABLE IF EXISTS achievements"); - db.run("DROP TABLE IF EXISTS achievement_requirements"); - db.run("DROP TABLE IF EXISTS progressions"); - db.run("DROP TABLE IF EXISTS achievement_criterias"); - db.run("DROP TABLE IF EXISTS achievement_labels"); - db.run("DROP TABLE IF EXISTS daily_sessions"); + for (const index of indexes_to_drop) { + db.run(`DROP INDEX IF EXISTS ${index}`); + } + for (const table of tables_to_drop) { + db.run(`DROP TABLE IF EXISTS ${table}`); + } db.run("COMMIT"); } catch (error) { db.run("ROLLBACK"); diff --git a/src/database/model/tables/Achievement.ts b/src/database/model/tables/Achievement.ts index 7bf2858..9ae4608 100644 --- a/src/database/model/tables/Achievement.ts +++ b/src/database/model/tables/Achievement.ts @@ -592,6 +592,16 @@ class Achievement { conditions.push(`(${labelConditions.join(" AND ")})`); values.push(...filters.labels); } + if (filters.criterias && filters.criterias.length > 0) { + conditions.push(`EXISTS ( + SELECT 1 FROM achievement_criterias ac + JOIN progressions p ON ac.progression_id = p.id + WHERE ac.achievement_id = a.id AND p.name IN (${filters.criterias + .map(() => "?") + .join(", ")}) + )`); + values.push(...filters.criterias); + } if (filters.title) { conditions.push("a.title LIKE ?"); values.push(`%${filters.title}%`); diff --git a/src/database/model/tables/DailySession.ts b/src/database/model/tables/DailySession.ts index 14e6f15..58bb15e 100644 --- a/src/database/model/tables/DailySession.ts +++ b/src/database/model/tables/DailySession.ts @@ -126,4 +126,41 @@ export class DailySession { ); return res.total_duration as number; } + + static async getStatsSummary( + today: string, + twoWeeksAgo: string, + monthStart: string, + yearStart: string + ): Promise<{ + daily: number; + twoWeeks: number; + monthly: number; + yearly: number; + total: number; + }> { + const db = await db_model.getDB(); + const query = ` + SELECT + SUM(CASE WHEN date = ? THEN duration ELSE 0 END) as daily, + SUM(CASE WHEN date >= ? THEN duration ELSE 0 END) as twoWeeks, + SUM(CASE WHEN date >= ? THEN duration ELSE 0 END) as monthly, + SUM(CASE WHEN date >= ? THEN duration ELSE 0 END) as yearly, + SUM(duration) as total + FROM daily_sessions + `; + const result = db_model.get(db, query, [ + today, + twoWeeksAgo, + monthStart, + yearStart, + ]); + return { + daily: result.daily || 0, + twoWeeks: result.twoWeeks || 0, + monthly: result.monthly || 0, + yearly: result.yearly || 0, + total: result.total || 0, + }; + } } diff --git a/src/listeners/files.ts b/src/listeners/files.ts index 0ece1e1..a7c255d 100644 --- a/src/listeners/files.ts +++ b/src/listeners/files.ts @@ -154,47 +154,41 @@ export namespace fileListeners { path.extname(event.document.fileName) ]; if (language) { + let totalLinesAdded = 0; for (const change of event.contentChanges) { - await processContentChange(change, language); + totalLinesAdded += countLinesAdded(change); } - } - } - async function processContentChange( - change: vscode.TextDocumentContentChangeEvent, - language: string - ): Promise { - // Check if the change involves adding new lines - if (change.text.includes("\n")) { - if (change.range.isSingleLine) { - // Increment progression for the added line + if (totalLinesAdded > 0) { + // Increment progression for the added lines await ProgressionController.increaseProgression( - constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language) + constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language), + totalLinesAdded ); // Increment generic lines of code progression await ProgressionController.increaseProgression( - constants.criteria.LINES_OF_CODE + constants.criteria.LINES_OF_CODE, + totalLinesAdded ); - } else { - // Count non-empty lines added in the change - const nonEmptyLinesCount = change.text - .split(/\r?\n/) - .filter((line) => line.trim().length > 0).length; + } + } + } - // Increment progression for the added lines - if (nonEmptyLinesCount > 0) { - await ProgressionController.increaseProgression( - constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language), - nonEmptyLinesCount - ); - // Increment generic lines of code progression - await ProgressionController.increaseProgression( - constants.criteria.LINES_OF_CODE, - nonEmptyLinesCount - ); - } + function countLinesAdded( + change: vscode.TextDocumentContentChangeEvent + ): number { + // Check if the change involves adding new lines + if (change.text.includes("\n")) { + const nonEmptyLinesCount = change.text + .split(/\r?\n/) + .filter((line) => line.trim().length > 0).length; + + if (nonEmptyLinesCount > 0) { + return nonEmptyLinesCount; } + return 1; } + return 0; } let fileErrorCounts = new Map(); diff --git a/src/test/controllers.test.ts b/src/test/controllers.test.ts index 7db47ec..281185c 100644 --- a/src/test/controllers.test.ts +++ b/src/test/controllers.test.ts @@ -3,6 +3,7 @@ import * as vscode from "vscode"; import * as path from "node:path"; import { AchievementController } from "../database/controller/achievements"; import { TimeSpentController } from "../database/controller/timespent"; +import { ProgressionController } from "../database/controller/progressions"; import { getMockContext, cleanupMockContext } from "./utils"; import { DailySession } from "../database/model/tables/DailySession"; import Progression from "../database/model/tables/Progression"; @@ -162,4 +163,29 @@ suite("Controllers Test Suite", () => { assert.strictEqual(result[constants.criteria.DAILY_TIME_SPENT], 100); assert.strictEqual(result[constants.criteria.TWO_WEEKS_TIME_SPENT], 200); }); + + test("ProgressionController.updateProgressions should update multiple progressions", async () => { + // Create progressions first + const p1 = new Progression({ + name: "test_prog_1", + value: 0, + type: "number", + }); + await p1.toRow(); + const p2 = new Progression({ + name: "test_prog_2", + value: 0, + type: "number", + }); + await p2.toRow(); + + await ProgressionController.updateProgressions([ + { name: "test_prog_1", value: 10 }, + { name: "test_prog_2", value: 20 }, + ]); + + const all = await ProgressionController.getProgressions(); + assert.strictEqual(all["test_prog_1"], 10); + assert.strictEqual(all["test_prog_2"], 20); + }); }); diff --git a/src/test/listeners/files.test.ts b/src/test/listeners/files.test.ts index fd8d421..d151e07 100644 --- a/src/test/listeners/files.test.ts +++ b/src/test/listeners/files.test.ts @@ -1,93 +1,155 @@ -import * as assert from 'node:assert'; -import * as vscode from 'vscode'; -import * as path from 'node:path'; -import * as fs from 'node:fs'; -import { fileListeners } from '../../listeners/files'; -import { ProgressionController } from '../../database/controller/progressions'; -import { constants } from '../../constants'; - -suite('File Listeners Test Suite', () => { - const tempDir = path.join(__dirname, 'temp_files_test'); - - suiteSetup(() => { - if (!fs.existsSync(tempDir)) { - fs.mkdirSync(tempDir); - } - }); - - suiteTeardown(() => { - if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } - }); - - test('handleCreateEvent should increase FILES_CREATED progression', async () => { - const testFile = path.join(tempDir, 'test.ts'); // Use .ts for language detection - fs.writeFileSync(testFile, 'content'); - const uri = vscode.Uri.file(testFile); - - let increasedCriteria: string[] = []; - const originalIncrease = ProgressionController.increaseProgression; - ProgressionController.increaseProgression = async (criteria: string) => { - increasedCriteria.push(criteria); - }; - - try { - await fileListeners.handleCreateEvent(uri); - assert.ok(increasedCriteria.includes(constants.criteria.FILES_CREATED)); - // Check for language specific criteria if .ts is mapped - // constants.labels.LANGUAGES_EXTENSIONS['.ts'] should be 'TypeScript' - // So criteria: FILES_CREATED_TypeScript - const langCriteria = constants.criteria.FILES_CREATED_LANGUAGE.replace('%s', 'TypeScript'); - if (constants.labels.LANGUAGES_EXTENSIONS['.ts'] === 'TypeScript') { - assert.ok(increasedCriteria.includes(langCriteria)); - } - } finally { - ProgressionController.increaseProgression = originalIncrease; - } - }); - - test('handleCreateEvent should increase DIRECTORY_CREATED progression', async () => { - const testDir = path.join(tempDir, 'test_dir'); - if (!fs.existsSync(testDir)) { - fs.mkdirSync(testDir); - } - const uri = vscode.Uri.file(testDir); - - let increasedCriteria: string | undefined; - const originalIncrease = ProgressionController.increaseProgression; - ProgressionController.increaseProgression = async (criteria: string) => { - if (criteria === constants.criteria.DIRECTORY_CREATED) { - increasedCriteria = criteria; - } - }; - - try { - await fileListeners.handleCreateEvent(uri); - assert.strictEqual(increasedCriteria, constants.criteria.DIRECTORY_CREATED); - } finally { - ProgressionController.increaseProgression = originalIncrease; - } - }); - - test('handleDeleteEvent should increase RESOURCE_DELETED progression', async () => { - const testFile = path.join(tempDir, 'test_del.txt'); - // File doesn't need to exist - const uri = vscode.Uri.file(testFile); - - let increasedCriteria: string | undefined; - const originalIncrease = ProgressionController.increaseProgression; - ProgressionController.increaseProgression = async (criteria: string) => { - if (criteria === constants.criteria.RESOURCE_DELETED) { - increasedCriteria = criteria; - } - }; - - try { - await fileListeners.handleDeleteEvent(uri); - assert.strictEqual(increasedCriteria, constants.criteria.RESOURCE_DELETED); - } finally { - ProgressionController.increaseProgression = originalIncrease; - } - }); +import * as assert from "node:assert"; +import * as vscode from "vscode"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import { fileListeners } from "../../listeners/files"; +import { ProgressionController } from "../../database/controller/progressions"; +import { constants } from "../../constants"; + +suite("File Listeners Test Suite", () => { + const tempDir = path.join(__dirname, "temp_files_test"); + + suiteSetup(() => { + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); + } + }); + + suiteTeardown(() => { + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("handleCreateEvent should increase FILES_CREATED progression", async () => { + const testFile = path.join(tempDir, "test.ts"); // Use .ts for language detection + fs.writeFileSync(testFile, "content"); + const uri = vscode.Uri.file(testFile); + + let increasedCriteria: string[] = []; + const originalIncrease = ProgressionController.increaseProgression; + ProgressionController.increaseProgression = async (criteria: string) => { + increasedCriteria.push(criteria); + }; + + try { + await fileListeners.handleCreateEvent(uri); + assert.ok(increasedCriteria.includes(constants.criteria.FILES_CREATED)); + // Check for language specific criteria if .ts is mapped + // constants.labels.LANGUAGES_EXTENSIONS['.ts'] should be 'TypeScript' + // So criteria: FILES_CREATED_TypeScript + const langCriteria = constants.criteria.FILES_CREATED_LANGUAGE.replace( + "%s", + "TypeScript" + ); + if (constants.labels.LANGUAGES_EXTENSIONS[".ts"] === "TypeScript") { + assert.ok(increasedCriteria.includes(langCriteria)); + } + } finally { + ProgressionController.increaseProgression = originalIncrease; + } + }); + + test("handleCreateEvent should increase DIRECTORY_CREATED progression", async () => { + const testDir = path.join(tempDir, "test_dir"); + if (!fs.existsSync(testDir)) { + fs.mkdirSync(testDir); + } + const uri = vscode.Uri.file(testDir); + + let increasedCriteria: string | undefined; + const originalIncrease = ProgressionController.increaseProgression; + ProgressionController.increaseProgression = async (criteria: string) => { + if (criteria === constants.criteria.DIRECTORY_CREATED) { + increasedCriteria = criteria; + } + }; + + try { + await fileListeners.handleCreateEvent(uri); + assert.strictEqual( + increasedCriteria, + constants.criteria.DIRECTORY_CREATED + ); + } finally { + ProgressionController.increaseProgression = originalIncrease; + } + }); + + test("handleDeleteEvent should increase RESOURCE_DELETED progression", async () => { + const testFile = path.join(tempDir, "test_del.txt"); + // File doesn't need to exist + const uri = vscode.Uri.file(testFile); + + let increasedCriteria: string | undefined; + const originalIncrease = ProgressionController.increaseProgression; + ProgressionController.increaseProgression = async (criteria: string) => { + if (criteria === constants.criteria.RESOURCE_DELETED) { + increasedCriteria = criteria; + } + }; + + try { + await fileListeners.handleDeleteEvent(uri); + assert.strictEqual( + increasedCriteria, + constants.criteria.RESOURCE_DELETED + ); + } finally { + ProgressionController.increaseProgression = originalIncrease; + } + }); + + test("handleTextChangedEvent should batch line updates", async () => { + const testFile = path.join(tempDir, "test.ts"); + const uri = vscode.Uri.file(testFile); + const document = { + fileName: testFile, + uri: uri, + } as vscode.TextDocument; + + const event = { + document: document, + reason: undefined, + contentChanges: [ + { + range: new vscode.Range(0, 0, 0, 0), + rangeOffset: 0, + rangeLength: 0, + text: "\n", // 1 line + }, + { + range: new vscode.Range(1, 0, 1, 0), + rangeOffset: 1, + rangeLength: 0, + text: "line1\nline2\n", // 2 lines + }, + ], + } as vscode.TextDocumentChangeEvent; + + let callCount = 0; + let totalLines = 0; + const originalIncrease = ProgressionController.increaseProgression; + ProgressionController.increaseProgression = async ( + criteria: string, + amount: number | string = 1 + ) => { + if (criteria === constants.criteria.LINES_OF_CODE) { + callCount++; + totalLines += Number(amount); + } + }; + + try { + await fileListeners.handleTextChangedEvent(event); + assert.strictEqual( + callCount, + 1, + "Should call increaseProgression once for generic lines" + ); + assert.strictEqual(totalLines, 3, "Should sum up all lines (1 + 2)"); + } finally { + ProgressionController.increaseProgression = originalIncrease; + } + }); }); diff --git a/src/test/migrations.test.ts b/src/test/migrations.test.ts new file mode 100644 index 0000000..a566e72 --- /dev/null +++ b/src/test/migrations.test.ts @@ -0,0 +1,58 @@ +import * as assert from "assert"; +import initSqlJs, { Database } from "sql.js"; +import { applyMigration } from "../database/model/migrations"; + +suite("Migrations Test Suite", () => { + let db: Database; + + setup(async () => { + const SQL = await initSqlJs(); + db = new SQL.Database(); + }); + + teardown(() => { + db.close(); + }); + + test("applyMigration should apply all migrations and update schema_version", async () => { + await applyMigration(db); + + const res = db.exec("SELECT * FROM schema_version ORDER BY version ASC"); + + // Check if table exists and has rows + assert.strictEqual( + res.length, + 1, + "schema_version table should exist and have data" + ); + const rows = res[0].values; + + console.log("Schema Version Rows:", JSON.stringify(rows)); + + // We expect the last row to be the latest version + const lastRow = rows[rows.length - 1]; + const version = lastRow[1]; // version column is 2nd (index 1) + + // Currently max version is 1 (since we merged migration 2 into 1) + assert.strictEqual(version, 1, "Database should be at version 1"); + + // Verify indexes exist + const indexes = db.exec( + "SELECT name FROM sqlite_master WHERE type='index'" + ); + const indexNames = indexes[0].values.map((v) => v[0]); + + const expectedIndexes = [ + "idx_achievement_criterias_progression_id", + "idx_achievements_category", + "idx_achievements_group", + "idx_achievements_achieved", + "idx_achievement_labels_label", + "idx_achievement_requirements_requirement_id", + ]; + + for (const index of expectedIndexes) { + assert.ok(indexNames.includes(index), `Index ${index} should exist`); + } + }); +}); diff --git a/src/test/tables.test.ts b/src/test/tables.test.ts index 759b4d9..88833dc 100644 --- a/src/test/tables.test.ts +++ b/src/test/tables.test.ts @@ -122,4 +122,30 @@ suite("Tables Test Suite", () => { // criteria is a dictionary assert.strictEqual(tier1.criteria["test_progression"], 10); }); + + test("DailySession: getStatsSummary should return correct stats", async () => { + const today = new Date().toISOString().split("T")[0]; + const yesterday = new Date(Date.now() - 86400000) + .toISOString() + .split("T")[0]; + const lastMonth = new Date(Date.now() - 30 * 86400000) + .toISOString() + .split("T")[0]; + + await DailySession.getOrCreate(today, 100); + await DailySession.getOrCreate(yesterday, 50); + await DailySession.getOrCreate(lastMonth, 200); + + const stats = await DailySession.getStatsSummary( + today, + yesterday, // twoWeeksAgo + today, // monthStart (so yesterday is excluded from monthly) + today // yearStart + ); + + assert.strictEqual(stats.daily, 100); + assert.strictEqual(stats.twoWeeks, 150); // today + yesterday + assert.strictEqual(stats.monthly, 100); // only today + assert.strictEqual(stats.total, 350); + }); });