feat(db): m008 — notebooks 테이블 + notes.notebook_id + archived 정리

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
th-kim0823
2026-05-15 09:47:23 +09:00
parent b860187b37
commit c99795c9e4
3 changed files with 74 additions and 1 deletions

View File

@@ -6,8 +6,9 @@ import * as m004 from './m004_status.js';
import * as m005 from './m005_ai_disabled.js';
import * as m006 from './m006_revisions.js';
import * as m007 from './m007_fts.js';
import * as m008 from './m008_notebooks.js';
const migrations = [m001, m002, m003, m004, m005, m006, m007];
const migrations = [m001, m002, m003, m004, m005, m006, m007, m008];
export function latestVersion(): number {
return migrations[migrations.length - 1]!.version;

View File

@@ -0,0 +1,37 @@
// v8: notebooks 테이블 + notes.notebook_id (FK) + archived → completed 정리.
// CHECK 제약 없는 status 컬럼이라 SQL 변경은 데이터 정리만, enum 단속은 TypeScript 측에서.
import type Database from 'better-sqlite3';
import { v7 as uuidv7 } from 'uuid';
export const version = 8;
const DEFAULT_NOTEBOOK_NAME = '기본';
export function up(db: Database.Database): void {
const now = new Date().toISOString();
const defaultId = uuidv7();
db.exec(`
CREATE TABLE notebooks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
color TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_notebooks_name ON notebooks(name);
ALTER TABLE notes ADD COLUMN notebook_id TEXT
REFERENCES notebooks(id) ON DELETE RESTRICT;
CREATE INDEX idx_notes_notebook_id ON notes(notebook_id);
`);
db.prepare(
`INSERT INTO notebooks (id, name, created_at, updated_at) VALUES (?, ?, ?, ?)`
).run(defaultId, DEFAULT_NOTEBOOK_NAME, now, now);
db.prepare(`UPDATE notes SET notebook_id = ? WHERE notebook_id IS NULL`).run(defaultId);
// archived 잔류 (dogfood 0건 확인됐지만 defensive) → completed 로 통합.
db.prepare(`UPDATE notes SET status='completed' WHERE status='archived'`).run();
}

35
tests/unit/m008.test.ts Normal file
View File

@@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { runMigrations } from '../../src/main/db/migrations/index.js';
describe('m008 notebooks migration', () => {
let db: Database.Database;
beforeEach(() => { db = new Database(':memory:'); db.pragma('foreign_keys = ON'); });
afterEach(() => { db.close(); });
it('fresh DB: notebooks 테이블 + default "기본" notebook 생성', () => {
runMigrations(db);
const row = db.prepare(`SELECT name FROM notebooks`).get() as { name: string };
expect(row.name).toBe('기본');
});
it('기존 notes 가 default notebook 으로 마이그레이션 — fresh insert 는 NULL 유지', () => {
runMigrations(db);
const defaultId = (db.prepare(`SELECT id FROM notebooks`).get() as { id: string }).id;
db.prepare(`INSERT INTO notes(id,raw_text,ai_status,created_at,updated_at,status) VALUES('n1','t','pending','2026-05-14','2026-05-14','active')`).run();
const r = db.prepare(`SELECT notebook_id FROM notes WHERE id='n1'`).get() as { notebook_id: string | null };
expect(r.notebook_id).toBeNull(); // m008 의 UPDATE 는 migration 시점의 NULL 만 채움
});
it('archived 잔류 노트가 있다면 completed 로 통합', () => {
runMigrations(db);
expect(() => db.prepare(`SELECT COUNT(*) FROM notes WHERE status='archived'`).get()).not.toThrow();
});
it('UNIQUE index 가 같은 이름 중복 INSERT 거부', () => {
runMigrations(db);
expect(() =>
db.prepare(`INSERT INTO notebooks(id,name,created_at,updated_at) VALUES('x','기본','t','t')`).run()
).toThrow();
});
});