ParseMigrationVersion(): extracts numeric version from filenames like 001.sql or 001_description.sql (exported for testing)
Old createSchema() removed entirely
Updated: database_test.go
Verifies schema_migrations table exists alongside other core tables
Verification
docker build . passes — formatting, linting, all tests green.
Closes https://git.eeqj.de/sneak/vaultik/issues/57
Adopts the [pixa migration pattern](https://git.eeqj.de/sneak/pixa/pulls/36) for schema management. Replaces the monolithic `schema.sql` embed with a numbered migration system.
## Changes
### New: `schema/000.sql` — Bootstrap migration
- Creates `schema_migrations` table with `INTEGER PRIMARY KEY` version column
- Self-contained: includes both `CREATE TABLE IF NOT EXISTS` and `INSERT OR IGNORE` for version 0
- Go code does zero INSERTs for bootstrap — just reads and executes 000.sql
### Renamed: `schema.sql` → `schema/001.sql` — Initial schema migration
- Full Vaultik schema (files, chunks, blobs, snapshots, uploads, all indexes)
- Updated header comment to identify it as migration 001
### Removed: `schema/008_uploads.sql`
- Redundant — the uploads table with its current schema was already in the main schema file
- The 008 file had a stale/different schema (TIMESTAMP instead of INTEGER, missing snapshot_id FK)
### Rewritten: `database.go` — Migration engine
- `//go:embed schema/*.sql` replaces `//go:embed schema.sql`
- `bootstrapMigrationsTable()`: checks if `schema_migrations` table exists, applies 000.sql if missing
- `applyMigrations()`: iterates through numbered .sql files, checks `schema_migrations` for each version, applies and records pending ones
- `collectMigrations()`: reads embedded schema dir, returns sorted filenames
- `ParseMigrationVersion()`: extracts numeric version from filenames like `001.sql` or `001_description.sql` (exported for testing)
- Old `createSchema()` removed entirely
### Updated: `database_test.go`
- Verifies `schema_migrations` table exists alongside other core tables
## Verification
`docker build .` passes — formatting, linting, all tests green.
Replace the monolithic schema.sql embed with a numbered migration system
following the pixa pattern:
- Add schema/000.sql: bootstrap migration creating schema_migrations table
with INTEGER PRIMARY KEY version column and applied_at timestamp
- Move full schema to schema/001.sql (renamed from schema.sql)
- Remove redundant schema/008_uploads.sql (uploads table already in main schema)
- Rewrite database.go migration logic:
- Embed schema/*.sql directory instead of single schema.sql
- bootstrapMigrationsTable() checks for table existence, applies 000.sql
- applyMigrations() iterates numbered files, skips already-applied versions
- ParseMigrationVersion() extracts version from filenames (e.g. 001.sql)
- Go code does zero INSERTs for bootstrap — 000.sql is self-contained
- Update database_test.go to verify schema_migrations table exists
Review: FAIL — Missing test coverage for migration engine
What's good
000.sql matches pixa reference exactly — INTEGER PRIMARY KEY, self-contained CREATE TABLE IF NOT EXISTS + INSERT OR IGNORE. Identical to pixa's 000.sql. ✅
Go code does zero INSERTs for bootstrap — bootstrapMigrationsTable() reads and executes 000.sql; no Go-level INSERT for version 0. ✅
008_uploads.sql removal is safe — the uploads table already exists in 001.sql with a better schema (INTEGER timestamps instead of TIMESTAMP, snapshot_id FK with CASCADE). The deleted 008 was stale and redundant. ✅
Migration ordering and idempotency logic is correct — bootstrap runs 000.sql when table missing, loop skips already-applied versions, upgrade path works because 001.sql uses CREATE TABLE IF NOT EXISTS. ✅
docker build . passes — lint, format check, and all tests green. ✅
What must be fixed
Missing TestParseMigrationVersion: The function is exported (the PR description says "exported for testing") and has non-trivial parsing logic with 5+ distinct code paths (empty filename, empty version prefix, underscore splitting, non-numeric chars, valid parse). Yet there are zero tests for it. The reference implementation pixa's database_test.go includes a comprehensive table-driven TestParseMigrationVersion with 9 test cases. Since issue #57 says to follow the pixa pattern, the matching test coverage is required.
Missing migration idempotency test: Pixa includes TestApplyMigrations_Idempotent that verifies running the migration engine twice succeeds without errors or duplicate rows in schema_migrations. This is a critical safety property for a migration engine — proving that re-running on an already-migrated database is a no-op. Vaultik should have an equivalent test.
Missing bootstrap isolation test: Pixa includes TestBootstrapMigrationsTable_FreshDatabase that verifies the bootstrap step in isolation (creates schema_migrations table and records version 0). This test exercises the bootstrap independently from the full migration loop.
Summary
The migration engine code itself is correct and faithfully reproduces the pixa pattern. The issue is test coverage — the PR adopts pixa's code but not pixa's tests. At minimum, add TestParseMigrationVersion (table-driven) and TestApplyMigrations_Idempotent. The bootstrap isolation test would also be nice to have.
Note: applyMigrations is unexported in vaultik (vs exported ApplyMigrations in pixa), but since vaultik's tests are in package database (not package database_test), they can call unexported functions directly, so this is fine.
## Review: FAIL — Missing test coverage for migration engine
### What's good
- **`000.sql` matches pixa reference exactly** — `INTEGER PRIMARY KEY`, self-contained `CREATE TABLE IF NOT EXISTS` + `INSERT OR IGNORE`. Identical to [pixa's 000.sql](https://git.eeqj.de/sneak/pixa/src/branch/main/internal/database/schema/000.sql). ✅
- **Go code does zero INSERTs for bootstrap** — `bootstrapMigrationsTable()` reads and executes 000.sql; no Go-level INSERT for version 0. ✅
- **008_uploads.sql removal is safe** — the uploads table already exists in [001.sql](https://git.eeqj.de/sneak/vaultik/src/branch/feat/migration-bootstrap-000sql/internal/database/schema/001.sql#L120-L132) with a *better* schema (INTEGER timestamps instead of TIMESTAMP, snapshot_id FK with CASCADE). The deleted 008 was stale and redundant. ✅
- **Migration ordering and idempotency logic is correct** — bootstrap runs 000.sql when table missing, loop skips already-applied versions, upgrade path works because 001.sql uses `CREATE TABLE IF NOT EXISTS`. ✅
- **`docker build .` passes** — lint, format check, and all tests green. ✅
### What must be fixed
**Missing `TestParseMigrationVersion`**: The function is exported (the PR description says "exported for testing") and has non-trivial parsing logic with 5+ distinct code paths (empty filename, empty version prefix, underscore splitting, non-numeric chars, valid parse). Yet there are zero tests for it. The reference implementation [pixa's `database_test.go`](https://git.eeqj.de/sneak/pixa/src/branch/main/internal/database/database_test.go) includes a comprehensive table-driven `TestParseMigrationVersion` with 9 test cases. Since [issue #57](https://git.eeqj.de/sneak/vaultik/issues/57) says to follow the pixa pattern, the matching test coverage is required.
**Missing migration idempotency test**: Pixa includes `TestApplyMigrations_Idempotent` that verifies running the migration engine twice succeeds without errors or duplicate rows in `schema_migrations`. This is a critical safety property for a migration engine — proving that re-running on an already-migrated database is a no-op. Vaultik should have an equivalent test.
**Missing bootstrap isolation test**: Pixa includes `TestBootstrapMigrationsTable_FreshDatabase` that verifies the bootstrap step in isolation (creates `schema_migrations` table and records version 0). This test exercises the bootstrap independently from the full migration loop.
### Summary
The migration engine code itself is correct and faithfully reproduces the pixa pattern. The issue is test coverage — the PR adopts pixa's code but not pixa's tests. At minimum, add `TestParseMigrationVersion` (table-driven) and `TestApplyMigrations_Idempotent`. The bootstrap isolation test would also be nice to have.
Note: `applyMigrations` is unexported in vaultik (vs exported `ApplyMigrations` in pixa), but since vaultik's tests are in `package database` (not `package database_test`), they can call unexported functions directly, so this is fine.
Add three tests requested in review:
- TestParseMigrationVersion: table-driven test with 9 cases covering valid
filenames, descriptions, invalid alpha/mixed, and empty string
- TestApplyMigrations_Idempotent: verifies running migrations twice is a
no-op with no duplicate rows in schema_migrations
- TestBootstrapMigrationsTable_FreshDatabase: isolation test verifying
bootstrap creates schema_migrations table with version 0 row
Assertions check both return value and error presence correctly. Each invalid case verifies err != nil; each valid case verifies exact version number.
TestApplyMigrations_Idempotent
Opens fresh in-memory SQLite DB
Runs applyMigrationstwice on the same connection
Counts schema_migrations rows after each run and asserts equality
Correctly exercises the "already applied" skip path in the migration loop
TestBootstrapMigrationsTable_FreshDatabase
Verifies schema_migrations does not exist before bootstrap (queries sqlite_master)
Runs bootstrapMigrationsTable
Verifies table now exists (count=1 in sqlite_master)
Verifies version 0 row is present via SELECT version FROM schema_migrations WHERE version = 0
No cheating
Makefile: untouched ✅
.golangci.yml: untouched ✅
CI config: untouched ✅
Dockerfile: untouched ✅
Migration engine code
000.sql matches pixa reference exactly: INTEGER PRIMARY KEY, CREATE TABLE IF NOT EXISTS, INSERT OR IGNORE INTO schema_migrations (version) VALUES (0)✅
schema.sql → schema/001.sql rename preserves full content with only header comment updated ✅
008_uploads.sql removal is safe — uploads table already exists in 001.sql with a better schema (INTEGER timestamps, snapshot_id FK with CASCADE) ✅
Go code does zero INSERTs for bootstrap — just reads and executes 000.sql ✅
ParseMigrationVersion properly validates numeric-only prefixes with character-level checking before strconv.Atoi✅
docker build .
Passes — lint, format check, all tests green. ✅
## Review: PASS ✅
All three tests requested in the [previous review](https://git.eeqj.de/sneak/vaultik/pulls/58#issuecomment-1369) are present and meaningful.
### TestParseMigrationVersion
9-case table-driven test covering:
- **Valid cases**: `000.sql` (zero), `001.sql`, `099.sql` (boundary), `001_initial_schema.sql` and `123_big_migration.sql` (with description suffix)
- **Invalid cases**: `abc.sql` (pure alpha), `12a.sql` (mixed chars), `schema.sql` (word without numeric prefix), empty string
- Assertions check both return value and error presence correctly. Each invalid case verifies `err != nil`; each valid case verifies exact version number.
### TestApplyMigrations_Idempotent
- Opens fresh in-memory SQLite DB
- Runs `applyMigrations` **twice** on the same connection
- Counts `schema_migrations` rows after each run and asserts equality
- Correctly exercises the "already applied" skip path in the migration loop
### TestBootstrapMigrationsTable_FreshDatabase
- Verifies `schema_migrations` does **not** exist before bootstrap (queries `sqlite_master`)
- Runs `bootstrapMigrationsTable`
- Verifies table **now exists** (count=1 in `sqlite_master`)
- Verifies version 0 row is present via `SELECT version FROM schema_migrations WHERE version = 0`
### No cheating
- Makefile: untouched ✅
- `.golangci.yml`: untouched ✅
- CI config: untouched ✅
- Dockerfile: untouched ✅
### Migration engine code
- `000.sql` matches [pixa reference](https://git.eeqj.de/sneak/pixa/src/branch/main/internal/database/schema/000.sql) exactly: `INTEGER PRIMARY KEY`, `CREATE TABLE IF NOT EXISTS`, `INSERT OR IGNORE INTO schema_migrations (version) VALUES (0)` ✅
- `schema.sql` → `schema/001.sql` rename preserves full content with only header comment updated ✅
- `008_uploads.sql` removal is safe — uploads table already exists in [001.sql](https://git.eeqj.de/sneak/vaultik/src/branch/feat/migration-bootstrap-000sql/internal/database/schema/001.sql) with a better schema (INTEGER timestamps, snapshot_id FK with CASCADE) ✅
- Go code does zero INSERTs for bootstrap — just reads and executes 000.sql ✅
- `ParseMigrationVersion` properly validates numeric-only prefixes with character-level checking before `strconv.Atoi` ✅
### `docker build .`
Passes — lint, format check, all tests green. ✅
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #57
Adopts the pixa migration pattern for schema management. Replaces the monolithic
schema.sqlembed with a numbered migration system.Changes
New:
schema/000.sql— Bootstrap migrationschema_migrationstable withINTEGER PRIMARY KEYversion columnCREATE TABLE IF NOT EXISTSandINSERT OR IGNOREfor version 0Renamed:
schema.sql→schema/001.sql— Initial schema migrationRemoved:
schema/008_uploads.sqlRewritten:
database.go— Migration engine//go:embed schema/*.sqlreplaces//go:embed schema.sqlbootstrapMigrationsTable(): checks ifschema_migrationstable exists, applies 000.sql if missingapplyMigrations(): iterates through numbered .sql files, checksschema_migrationsfor each version, applies and records pending onescollectMigrations(): reads embedded schema dir, returns sorted filenamesParseMigrationVersion(): extracts numeric version from filenames like001.sqlor001_description.sql(exported for testing)createSchema()removed entirelyUpdated:
database_test.goschema_migrationstable exists alongside other core tablesVerification
docker build .passes — formatting, linting, all tests green.Review: FAIL — Missing test coverage for migration engine
What's good
000.sqlmatches pixa reference exactly —INTEGER PRIMARY KEY, self-containedCREATE TABLE IF NOT EXISTS+INSERT OR IGNORE. Identical to pixa's 000.sql. ✅bootstrapMigrationsTable()reads and executes 000.sql; no Go-level INSERT for version 0. ✅CREATE TABLE IF NOT EXISTS. ✅docker build .passes — lint, format check, and all tests green. ✅What must be fixed
Missing
TestParseMigrationVersion: The function is exported (the PR description says "exported for testing") and has non-trivial parsing logic with 5+ distinct code paths (empty filename, empty version prefix, underscore splitting, non-numeric chars, valid parse). Yet there are zero tests for it. The reference implementation pixa'sdatabase_test.goincludes a comprehensive table-drivenTestParseMigrationVersionwith 9 test cases. Since issue #57 says to follow the pixa pattern, the matching test coverage is required.Missing migration idempotency test: Pixa includes
TestApplyMigrations_Idempotentthat verifies running the migration engine twice succeeds without errors or duplicate rows inschema_migrations. This is a critical safety property for a migration engine — proving that re-running on an already-migrated database is a no-op. Vaultik should have an equivalent test.Missing bootstrap isolation test: Pixa includes
TestBootstrapMigrationsTable_FreshDatabasethat verifies the bootstrap step in isolation (createsschema_migrationstable and records version 0). This test exercises the bootstrap independently from the full migration loop.Summary
The migration engine code itself is correct and faithfully reproduces the pixa pattern. The issue is test coverage — the PR adopts pixa's code but not pixa's tests. At minimum, add
TestParseMigrationVersion(table-driven) andTestApplyMigrations_Idempotent. The bootstrap isolation test would also be nice to have.Note:
applyMigrationsis unexported in vaultik (vs exportedApplyMigrationsin pixa), but since vaultik's tests are inpackage database(notpackage database_test), they can call unexported functions directly, so this is fine.Rework complete — added missing migration engine tests
Added all three tests requested in review:
TestParseMigrationVersion
Table-driven test with 9 cases:
000.sql,001.sql,099.sql001_initial_schema.sql,123_big_migration.sqlabc.sql12a.sqlschema.sqlTestApplyMigrations_Idempotent
Opens a fresh in-memory DB, runs
applyMigrationstwice, verifies no errors andschema_migrationsrow count is unchanged between runs.TestBootstrapMigrationsTable_FreshDatabase
Isolation test: fresh DB →
bootstrapMigrationsTable→ verifiesschema_migrationstable exists and version 0 row is present.docker build .passes — formatting, linting, all tests green.Review: PASS ✅
All three tests requested in the previous review are present and meaningful.
TestParseMigrationVersion
9-case table-driven test covering:
000.sql(zero),001.sql,099.sql(boundary),001_initial_schema.sqland123_big_migration.sql(with description suffix)abc.sql(pure alpha),12a.sql(mixed chars),schema.sql(word without numeric prefix), empty stringerr != nil; each valid case verifies exact version number.TestApplyMigrations_Idempotent
applyMigrationstwice on the same connectionschema_migrationsrows after each run and asserts equalityTestBootstrapMigrationsTable_FreshDatabase
schema_migrationsdoes not exist before bootstrap (queriessqlite_master)bootstrapMigrationsTablesqlite_master)SELECT version FROM schema_migrations WHERE version = 0No cheating
.golangci.yml: untouched ✅Migration engine code
000.sqlmatches pixa reference exactly:INTEGER PRIMARY KEY,CREATE TABLE IF NOT EXISTS,INSERT OR IGNORE INTO schema_migrations (version) VALUES (0)✅schema.sql→schema/001.sqlrename preserves full content with only header comment updated ✅008_uploads.sqlremoval is safe — uploads table already exists in 001.sql with a better schema (INTEGER timestamps, snapshot_id FK with CASCADE) ✅ParseMigrationVersionproperly validates numeric-only prefixes with character-level checking beforestrconv.Atoi✅docker build .Passes — lint, format check, all tests green. ✅