Skip to content
Open
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
11 changes: 10 additions & 1 deletion packages/desktop/src/main/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { initFTS } from './fts.js';

let db: ReturnType<typeof drizzle> | null = null;
let sqlite: Database.Database | null = null;
let initializedWorkspacePath: string | null = null;

const CREATE_TABLES_SQL = `
CREATE TABLE IF NOT EXISTS tasks (
Expand Down Expand Up @@ -107,12 +108,19 @@ function openDatabaseAt(workspacePath: string): void {
}

export function initDatabase(workspacePath: string): void {
if (db) return;
if (db) {
if (initializedWorkspacePath !== workspacePath) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Comparing raw strings for file paths can be fragile. Differences in trailing slashes (e.g., /path/to/ws vs /path/to/ws/) or relative vs. absolute paths will cause this check to fail even if they point to the same directory. It is recommended to normalize the workspacePath (e.g., using path.resolve()) before storing and comparing it to ensure the check is robust.

throw new Error('initDatabase called with different workspace path; use reinitDatabase to switch');
}
return;
}
initializedWorkspacePath = workspacePath;
openDatabaseAt(workspacePath);
}

export function reinitDatabase(workspacePath: string): void {
closeDatabase();
initializedWorkspacePath = workspacePath;
openDatabaseAt(workspacePath);
}

Expand All @@ -133,4 +141,5 @@ export function closeDatabase(): void {
sqlite?.close();
sqlite = null;
db = null;
initializedWorkspacePath = null;
Comment on lines 141 to +144
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If sqlite?.close() throws an error, the subsequent lines to reset sqlite, db, and initializedWorkspacePath will be skipped, leaving the module in an inconsistent state. Using a try...finally block ensures these variables are always reset regardless of whether the close operation succeeds.

  try {
    sqlite?.close();
  } finally {
    sqlite = null;
    db = null;
    initializedWorkspacePath = null;
  }

}
58 changes: 58 additions & 0 deletions packages/desktop/test/db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { initDatabase, reinitDatabase, closeDatabase } from '../src/main/db/index.js';
import Database from 'better-sqlite3';

const mockDbInstance = {
pragma: vi.fn(),
exec: vi.fn(),
close: vi.fn(),
};

vi.mock('better-sqlite3', () => ({
default: vi.fn(() => mockDbInstance),
}));

describe('initDatabase', () => {
beforeEach(() => {
vi.clearAllMocks();
closeDatabase();
});

afterEach(() => {
closeDatabase();
});

it('opens database on first call', () => {
expect(() => initDatabase('/tmp/workspace-a')).not.toThrow();
expect(Database).toHaveBeenCalled();
});

it('returns early when called with same path', () => {
initDatabase('/tmp/workspace-a');
vi.clearAllMocks();
expect(() => initDatabase('/tmp/workspace-a')).not.toThrow();
expect(Database).not.toHaveBeenCalled();
});

it('throws when called with different workspace path', () => {
initDatabase('/tmp/workspace-a');
expect(() => initDatabase('/tmp/workspace-b')).toThrow(
'initDatabase called with different workspace path; use reinitDatabase to switch',
);
});

it('allows init after close', () => {
initDatabase('/tmp/workspace-a');
closeDatabase();
vi.clearAllMocks();
expect(() => initDatabase('/tmp/workspace-b')).not.toThrow();
});

it('throws when init is called with old path after reinit with new path', () => {
initDatabase('/tmp/workspace-a');
reinitDatabase('/tmp/workspace-b');
expect(() => initDatabase('/tmp/workspace-a')).toThrow(
'initDatabase called with different workspace path; use reinitDatabase to switch',
);
});
});