// Package store SQLite 打开、版本化迁移与全部数据访问。 package store import ( "context" "database/sql" "errors" "fmt" "net/url" "path/filepath" "time" _ "modernc.org/sqlite" ) // bgCtx DAO 内部使用的后台 context(个人规模查询均为快速查询)。 var bgCtx = context.Background() // ErrSchemaNewer 数据库 schema 版本超出代码支持范围(§10.4 启动守卫)。 var ErrSchemaNewer = errors.New("数据库 schema 版本高于本程序支持的最高版本,拒绝启动(可用 --allow-newer 显式放行)") // migrations 仅追加式:禁止删列/重命名/改类型(§6.2)。 var migrations = []string{ // v1: 初始 schema(§6.1 DDL) ` CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, title TEXT NOT NULL, summary TEXT NOT NULL DEFAULT '', content TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'private' CHECK (status IN ('public','private')), tags TEXT NOT NULL DEFAULT '[]', pinned INTEGER NOT NULL DEFAULT 0, deleted_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_notes_public ON notes (status, pinned, updated_at DESC); CREATE INDEX IF NOT EXISTS idx_notes_deleted ON notes (deleted_at); CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, sha256 TEXT NOT NULL UNIQUE, mime TEXT NOT NULL, size INTEGER NOT NULL, data BLOB NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS image_refs ( image_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE, note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE, PRIMARY KEY (image_id, note_id) ); CREATE INDEX IF NOT EXISTS idx_image_refs_note ON image_refs (note_id); CREATE TABLE IF NOT EXISTS sessions ( token_hash TEXT PRIMARY KEY, csrf_token TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); `, } // MaxSchemaVersion 代码支持的最高 schema 版本。 var MaxSchemaVersion = len(migrations) // Store 封装 *sql.DB;单连接串行写(§6.2 并发策略)。 type Store struct { db *sql.DB } // Open 打开数据库并执行迁移(start/init 用)。dbPath 为文件绝对/相对路径。 // allowNewer 跳过 user_version 上界守卫(§10.4)。 func Open(dbPath string, allowNewer bool) (*Store, error) { db, err := openDB(dbPath) if err != nil { return nil, err } if err := migrateDB(db, allowNewer); err != nil { db.Close() return nil, err } return &Store{db: db}, nil } // OpenData 打开数据库仅做数据操作:**不迁移、不做版本守卫** // (passwd/backup/gc 维护子命令,D6:在更新 schema 的库上备份/清理 // 不应被守卫阻断,也不应产生迁移副作用)。 func OpenData(dbPath string) (*Store, error) { db, err := openDB(dbPath) if err != nil { return nil, err } return &Store{db: db}, nil } func openDB(dbPath string) (*sql.DB, error) { // SQLite URI 不接受相对路径 abs, err := filepath.Abs(dbPath) if err != nil { return nil, err } dsn := url.URL{ Scheme: "file", Path: abs, RawQuery: "_pragma=journal_mode(WAL)" + "&_pragma=busy_timeout(5000)" + "&_pragma=foreign_keys(1)" + "&_pragma=synchronous(NORMAL)", } db, err := sql.Open("sqlite", dsn.String()) if err != nil { return nil, err } // 单写者串行化(§6.2) db.SetMaxOpenConns(1) return db, nil } func migrateDB(db *sql.DB, allowNewer bool) error { var v int if err := db.QueryRow("PRAGMA user_version").Scan(&v); err != nil { return err } if v > MaxSchemaVersion && !allowNewer { return fmt.Errorf("%w: 库版本 %d > 支持上限 %d", ErrSchemaNewer, v, MaxSchemaVersion) } for i := v; i < MaxSchemaVersion; i++ { tx, err := db.Begin() if err != nil { return err } if _, err := tx.Exec(migrations[i]); err != nil { tx.Rollback() return fmt.Errorf("执行迁移 v%d 失败: %w", i+1, err) } // PRAGMA 不能参数化,i 为内部 int 常量,无注入面。 if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", i+1)); err != nil { tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } } return nil } // SchemaVersion 返回当前库版本。 func (s *Store) SchemaVersion() (int, error) { var v int err := s.db.QueryRow("PRAGMA user_version").Scan(&v) return v, err } // Close 关闭连接。 func (s *Store) Close() error { return s.db.Close() } // DB 暴露底层连接(仅备份等维护路径使用)。 func (s *Store) DB() *sql.DB { return s.db } func nowUnix() int64 { return time.Now().Unix() }