- Project scaffolding: go.mod, Makefile, .golangci.yaml, doc.go - README, ARCHITECTURE, PROJECT_PLAN, PROGRESS documentation - db package: Open (WAL, FK, busy timeout, 0600 permissions), Migrate (sequential, transactional, idempotent), SchemaVersion, Snapshot (VACUUM INTO) - 11 tests covering open, migrate, and snapshot Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
305 lines
6.9 KiB
Go
305 lines
6.9 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpen(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
|
|
database, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer func() { _ = database.Close() }()
|
|
|
|
// Verify WAL mode is enabled.
|
|
var journalMode string
|
|
if err := database.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err != nil {
|
|
t.Fatalf("query journal_mode: %v", err)
|
|
}
|
|
if journalMode != "wal" {
|
|
t.Fatalf("journal_mode = %q, want %q", journalMode, "wal")
|
|
}
|
|
|
|
// Verify foreign keys are enabled.
|
|
var fk int
|
|
if err := database.QueryRow("PRAGMA foreign_keys").Scan(&fk); err != nil {
|
|
t.Fatalf("query foreign_keys: %v", err)
|
|
}
|
|
if fk != 1 {
|
|
t.Fatalf("foreign_keys = %d, want 1", fk)
|
|
}
|
|
|
|
// Verify busy timeout.
|
|
var timeout int
|
|
if err := database.QueryRow("PRAGMA busy_timeout").Scan(&timeout); err != nil {
|
|
t.Fatalf("query busy_timeout: %v", err)
|
|
}
|
|
if timeout != 5000 {
|
|
t.Fatalf("busy_timeout = %d, want 5000", timeout)
|
|
}
|
|
}
|
|
|
|
func TestOpenFilePermissions(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
|
|
database, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
_ = database.Close()
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("Stat: %v", err)
|
|
}
|
|
perm := info.Mode().Perm()
|
|
if perm != 0600 {
|
|
t.Fatalf("permissions = %o, want 0600", perm)
|
|
}
|
|
}
|
|
|
|
func TestOpenCreatesParentDir(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "sub", "dir", "test.db")
|
|
|
|
database, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
_ = database.Close()
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("database file does not exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenExistingDB(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
|
|
// Create and populate.
|
|
db1, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open (first): %v", err)
|
|
}
|
|
if _, err := db1.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY)"); err != nil {
|
|
t.Fatalf("create table: %v", err)
|
|
}
|
|
if _, err := db1.Exec("INSERT INTO t (id) VALUES (42)"); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
_ = db1.Close()
|
|
|
|
// Reopen and verify data persists.
|
|
db2, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open (second): %v", err)
|
|
}
|
|
defer func() { _ = db2.Close() }()
|
|
|
|
var id int
|
|
if err := db2.QueryRow("SELECT id FROM t").Scan(&id); err != nil {
|
|
t.Fatalf("select: %v", err)
|
|
}
|
|
if id != 42 {
|
|
t.Fatalf("id = %d, want 42", id)
|
|
}
|
|
}
|
|
|
|
var testMigrations = []Migration{
|
|
{
|
|
Version: 1,
|
|
Name: "create users",
|
|
SQL: `CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`,
|
|
},
|
|
{
|
|
Version: 2,
|
|
Name: "add email",
|
|
SQL: `ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''`,
|
|
},
|
|
}
|
|
|
|
func TestMigrate(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
if err := Migrate(database, testMigrations); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
// Verify both migrations applied.
|
|
version, err := SchemaVersion(database)
|
|
if err != nil {
|
|
t.Fatalf("SchemaVersion: %v", err)
|
|
}
|
|
if version != 2 {
|
|
t.Fatalf("schema version = %d, want 2", version)
|
|
}
|
|
|
|
// Verify schema is correct.
|
|
if _, err := database.Exec("INSERT INTO users (name, email) VALUES ('a', 'a@b.c')"); err != nil {
|
|
t.Fatalf("insert into migrated schema: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMigrateIdempotent(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
// Run twice.
|
|
if err := Migrate(database, testMigrations); err != nil {
|
|
t.Fatalf("Migrate (first): %v", err)
|
|
}
|
|
if err := Migrate(database, testMigrations); err != nil {
|
|
t.Fatalf("Migrate (second): %v", err)
|
|
}
|
|
|
|
version, err := SchemaVersion(database)
|
|
if err != nil {
|
|
t.Fatalf("SchemaVersion: %v", err)
|
|
}
|
|
if version != 2 {
|
|
t.Fatalf("schema version = %d, want 2", version)
|
|
}
|
|
}
|
|
|
|
func TestMigrateIncremental(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
// Apply only the first migration.
|
|
if err := Migrate(database, testMigrations[:1]); err != nil {
|
|
t.Fatalf("Migrate (first only): %v", err)
|
|
}
|
|
|
|
version, err := SchemaVersion(database)
|
|
if err != nil {
|
|
t.Fatalf("SchemaVersion: %v", err)
|
|
}
|
|
if version != 1 {
|
|
t.Fatalf("schema version = %d, want 1", version)
|
|
}
|
|
|
|
// Now apply all — should pick up only migration 2.
|
|
if err := Migrate(database, testMigrations); err != nil {
|
|
t.Fatalf("Migrate (all): %v", err)
|
|
}
|
|
|
|
version, err = SchemaVersion(database)
|
|
if err != nil {
|
|
t.Fatalf("SchemaVersion: %v", err)
|
|
}
|
|
if version != 2 {
|
|
t.Fatalf("schema version = %d, want 2", version)
|
|
}
|
|
}
|
|
|
|
func TestMigrateRecordsName(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
if err := Migrate(database, testMigrations); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
|
|
var name string
|
|
err := database.QueryRow(
|
|
`SELECT name FROM schema_migrations WHERE version = 1`,
|
|
).Scan(&name)
|
|
if err != nil {
|
|
t.Fatalf("query migration name: %v", err)
|
|
}
|
|
if name != "create users" {
|
|
t.Fatalf("migration name = %q, want %q", name, "create users")
|
|
}
|
|
}
|
|
|
|
func TestSchemaVersionEmpty(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
// Create the table but apply no migrations.
|
|
if err := Migrate(database, nil); err != nil {
|
|
t.Fatalf("Migrate(nil): %v", err)
|
|
}
|
|
|
|
version, err := SchemaVersion(database)
|
|
if err != nil {
|
|
t.Fatalf("SchemaVersion: %v", err)
|
|
}
|
|
if version != 0 {
|
|
t.Fatalf("schema version = %d, want 0", version)
|
|
}
|
|
}
|
|
|
|
func TestSnapshot(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
// Create some data.
|
|
if _, err := database.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)"); err != nil {
|
|
t.Fatalf("create table: %v", err)
|
|
}
|
|
if _, err := database.Exec("INSERT INTO t (val) VALUES ('hello')"); err != nil {
|
|
t.Fatalf("insert: %v", err)
|
|
}
|
|
|
|
// Snapshot.
|
|
dir := t.TempDir()
|
|
snapPath := filepath.Join(dir, "snap.db")
|
|
if err := Snapshot(database, snapPath); err != nil {
|
|
t.Fatalf("Snapshot: %v", err)
|
|
}
|
|
|
|
// Verify snapshot file permissions.
|
|
info, err := os.Stat(snapPath)
|
|
if err != nil {
|
|
t.Fatalf("Stat snapshot: %v", err)
|
|
}
|
|
if perm := info.Mode().Perm(); perm != 0600 {
|
|
t.Fatalf("snapshot permissions = %o, want 0600", perm)
|
|
}
|
|
|
|
// Open snapshot and verify data.
|
|
snapDB, err := sql.Open("sqlite", snapPath)
|
|
if err != nil {
|
|
t.Fatalf("open snapshot: %v", err)
|
|
}
|
|
defer func() { _ = snapDB.Close() }()
|
|
|
|
var val string
|
|
if err := snapDB.QueryRow("SELECT val FROM t").Scan(&val); err != nil {
|
|
t.Fatalf("select from snapshot: %v", err)
|
|
}
|
|
if val != "hello" {
|
|
t.Fatalf("val = %q, want %q", val, "hello")
|
|
}
|
|
}
|
|
|
|
func TestSnapshotCreatesParentDir(t *testing.T) {
|
|
database := openTestDB(t)
|
|
|
|
dir := t.TempDir()
|
|
snapPath := filepath.Join(dir, "sub", "snap.db")
|
|
if err := Snapshot(database, snapPath); err != nil {
|
|
t.Fatalf("Snapshot: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(snapPath); err != nil {
|
|
t.Fatalf("snapshot file does not exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func openTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.db")
|
|
database, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = database.Close() })
|
|
return database
|
|
}
|