diff --git a/src/main/db/migrations/index.ts b/src/main/db/migrations/index.ts index 61129bd..a3630de 100644 --- a/src/main/db/migrations/index.ts +++ b/src/main/db/migrations/index.ts @@ -2,8 +2,9 @@ import type Database from 'better-sqlite3'; import * as m001 from './m001_initial.js'; import * as m002 from './m002_due_date.js'; import * as m003 from './m003_soft_delete.js'; +import * as m004 from './m004_status.js'; -const migrations = [m001, m002, m003]; +const migrations = [m001, m002, m003, m004]; export function latestVersion(): number { return migrations[migrations.length - 1]!.version; diff --git a/src/main/db/migrations/m004_status.ts b/src/main/db/migrations/m004_status.ts new file mode 100644 index 0000000..2225506 --- /dev/null +++ b/src/main/db/migrations/m004_status.ts @@ -0,0 +1,18 @@ +// v4: status 4분기 (active/completed/archived/trashed) + 사유 + status_changed_at. +// 기존 deleted_at != NULL 노트는 status='trashed' 로 migrate. deleted_at 컬럼은 +// backward compat 위해 잔류 (status='trashed' 와 동기화). 향후 cut 에서 제거 가능. +import type Database from 'better-sqlite3'; + +export const version = 4; + +export function up(db: Database.Database): void { + db.exec(` + ALTER TABLE notes ADD COLUMN status TEXT NOT NULL DEFAULT 'active'; + ALTER TABLE notes ADD COLUMN status_changed_at TEXT; + ALTER TABLE notes ADD COLUMN move_reason TEXT; + `); + db.prepare( + `UPDATE notes SET status='trashed', status_changed_at=deleted_at + WHERE deleted_at IS NOT NULL` + ).run(); +} diff --git a/tests/unit/m004-migration.test.ts b/tests/unit/m004-migration.test.ts new file mode 100644 index 0000000..c804c99 --- /dev/null +++ b/tests/unit/m004-migration.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import Database from 'better-sqlite3'; +import { up } from '../../src/main/db/migrations/m004_status.js'; + +describe('m004 migration — status column', () => { + let db: Database.Database; + + beforeEach(() => { + db = new Database(':memory:'); + // m003 baseline (notes 테이블 with deleted_at, real schema 따름) + db.exec(` + CREATE TABLE notes ( + id TEXT PRIMARY KEY, + raw_text TEXT NOT NULL, + ai_title TEXT, + ai_summary TEXT, + ai_status TEXT NOT NULL + CHECK (ai_status IN ('pending','done','failed')), + ai_error TEXT, + ai_provider TEXT, + ai_generated_at TEXT, + title_edited_by_user INTEGER NOT NULL DEFAULT 0, + summary_edited_by_user INTEGER NOT NULL DEFAULT 0, + user_intent TEXT, + intent_prompted_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + due_date TEXT, + due_date_edited_by_user INTEGER NOT NULL DEFAULT 0, + deleted_at TEXT, + last_recalled_at TEXT, + recall_dismissed_at TEXT + ); + INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at, deleted_at) + VALUES ('a', 't1', 'done', '2026-05-01T00:00:00Z', '2026-05-01T00:00:00Z', NULL), + ('b', 't2', 'done', '2026-05-01T00:00:00Z', '2026-05-01T00:00:00Z', '2026-05-08T00:00:00Z'); + `); + }); + + afterEach(() => { + db.close(); + }); + + it('adds status / status_changed_at / move_reason columns', () => { + up(db); + const cols = db.prepare(`PRAGMA table_info(notes)`).all() as Array<{ name: string }>; + const names = cols.map((c) => c.name); + expect(names).toContain('status'); + expect(names).toContain('status_changed_at'); + expect(names).toContain('move_reason'); + }); + + it('default status="active" for non-deleted notes', () => { + up(db); + const a = db.prepare(`SELECT status FROM notes WHERE id=?`).get('a') as { status: string }; + expect(a.status).toBe('active'); + }); + + it('migrates deleted_at != NULL to status="trashed" + status_changed_at', () => { + up(db); + const b = db + .prepare(`SELECT status, status_changed_at FROM notes WHERE id=?`) + .get('b') as { status: string; status_changed_at: string }; + expect(b.status).toBe('trashed'); + expect(b.status_changed_at).toBe('2026-05-08T00:00:00Z'); + }); + + it('move_reason NULL by default', () => { + up(db); + const a = db.prepare(`SELECT move_reason FROM notes WHERE id=?`).get('a') as { + move_reason: string | null; + }; + expect(a.move_reason).toBeNull(); + }); + + it('version exported as 4', async () => { + const mod = await import('../../src/main/db/migrations/m004_status.js'); + expect(mod.version).toBe(4); + }); +}); diff --git a/tests/unit/migrations.test.ts b/tests/unit/migrations.test.ts index b4b66a8..fc8337c 100644 --- a/tests/unit/migrations.test.ts +++ b/tests/unit/migrations.test.ts @@ -51,11 +51,11 @@ describe('migration v3 — soft delete columns', () => { db.close(); }); - it('user_version reaches 3', () => { + it('user_version reaches 4', () => { const db = new Database(':memory:'); runMigrations(db); const row = db.prepare('PRAGMA user_version').get() as { user_version: number }; - expect(row.user_version).toBe(3); + expect(row.user_version).toBe(4); db.close(); });