Implement Phase 0+1: project setup, config, database, migrations
- Go module, Makefile, .golangci.yaml, .gitignore, example config - TOML config loading with validation - SQLite database with WAL, foreign keys, busy timeout - Schema migrations: users, webauthn_credentials, notebooks, pages, strokes, share_links with indexes and cascading deletes - 4 tests: open+migrate, idempotent, foreign keys, cascade delete Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
114
internal/db/migrations.go
Normal file
114
internal/db/migrations.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var migrations = []struct {
|
||||
name string
|
||||
sql string
|
||||
}{
|
||||
{
|
||||
name: "001_initial_schema",
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webauthn_credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
credential_id BLOB NOT NULL UNIQUE,
|
||||
public_key BLOB NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
sign_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notebooks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
remote_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
page_size TEXT NOT NULL,
|
||||
synced_at INTEGER NOT NULL,
|
||||
UNIQUE(user_id, remote_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
notebook_id INTEGER NOT NULL REFERENCES notebooks(id) ON DELETE CASCADE,
|
||||
remote_id INTEGER NOT NULL,
|
||||
page_number INTEGER NOT NULL,
|
||||
UNIQUE(notebook_id, remote_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS strokes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
pen_size REAL NOT NULL,
|
||||
color INTEGER NOT NULL,
|
||||
style TEXT NOT NULL DEFAULT 'plain',
|
||||
point_data BLOB NOT NULL,
|
||||
stroke_order INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS share_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
notebook_id INTEGER NOT NULL REFERENCES notebooks(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
expires_at INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
name TEXT PRIMARY KEY,
|
||||
applied_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notebooks_user ON notebooks(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_notebook ON pages(notebook_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_strokes_page ON strokes(page_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_share_links_token ON share_links(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_webauthn_user ON webauthn_credentials(user_id);
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
func Migrate(database *sql.DB) error {
|
||||
// Ensure schema_migrations table exists
|
||||
_, err := database.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
name TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create schema_migrations: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
var count int
|
||||
err := database.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE name = ?", m.name).Scan(&count)
|
||||
if err != nil {
|
||||
return fmt.Errorf("check migration %s: %w", m.name, err)
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := database.Exec(m.sql); err != nil {
|
||||
return fmt.Errorf("apply migration %s: %w", m.name, err)
|
||||
}
|
||||
|
||||
if _, err := database.Exec(
|
||||
"INSERT INTO schema_migrations (name, applied_at) VALUES (?, strftime('%s','now'))",
|
||||
m.name,
|
||||
); err != nil {
|
||||
return fmt.Errorf("record migration %s: %w", m.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user