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:
@@ -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;
|
||||
|
||||
18
src/main/db/migrations/m004_status.ts
Normal file
18
src/main/db/migrations/m004_status.ts
Normal 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();
|
||||
}
|
||||
80
tests/unit/m004-migration.test.ts
Normal file
80
tests/unit/m004-migration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user