feat(v029): m004 마이그레이션 — status/status_changed_at/move_reason 컬럼

- notes 테이블 ADD COLUMN status (DEFAULT 'active'), status_changed_at, move_reason
- deleted_at != NULL 노트 → status='trashed' + status_changed_at=deleted_at 로 backfill
- index.ts registry 에 m004 추가 (runMigrations 자동 적용)
- migrations.test.ts user_version assertion 4 로 갱신
This commit is contained in:
altair823
2026-05-09 15:27:15 +09:00
parent 7d2b8c95ec
commit 06a1caf2bd
4 changed files with 102 additions and 3 deletions

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);
});
});

View File

@@ -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();
});