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:
@@ -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;
|
||||
|
||||
37
src/main/db/migrations/m008_notebooks.ts
Normal file
37
src/main/db/migrations/m008_notebooks.ts
Normal 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
35
tests/unit/m008.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user