A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix db migration logic

+80 -53
+2 -2
docker-compose.yml
··· 55 55 container_name: atcr-hold 56 56 ports: 57 57 - "8080:8080" 58 - volumes: 59 - - atcr-hold:/var/lib/atcr/hold 58 + # volumes: 59 + # - atcr-hold:/var/lib/atcr/hold 60 60 restart: unless-stopped 61 61 dns: 62 62 - 8.8.8.8
+3
pkg/appview/db/migrations/0001_example.yaml
··· 1 + description: Example migrarion query 2 + query: | 3 + SELECT COUNT(*) FROM schema_migrations;
-8
pkg/appview/db/migrations/0001_remove_star_count_from_repository_stats.yaml
··· 1 - version: 1 2 - name: remove_star_count_from_repository_stats 3 - up: | 4 - -- Drop star_count column if it exists (SQLite 3.35.0+) 5 - ALTER TABLE repository_stats DROP COLUMN IF EXISTS star_count; 6 - 7 - -- Drop the old star_count index if it exists 8 - DROP INDEX IF EXISTS idx_repository_stats_star_count;
+24 -16
pkg/appview/db/migrations/README.md
··· 7 7 Each migration is a YAML file with the following structure: 8 8 9 9 ```yaml 10 - version: 1 11 - name: descriptive_migration_name 12 - up: | 10 + description: Optional human-readable description of what this migration does 11 + query: | 13 12 SQL commands to apply the migration 14 13 ``` 15 14 15 + **Version and name are parsed from the filename**, so you don't need to specify them in the YAML. 16 + 16 17 ## Naming Convention 17 18 18 - Migration files should be named: `{version:04d}_{name}.yaml` 19 + Migration files **must** be named: `{version:04d}_{migration_name}.yaml` 20 + 21 + The filename determines: 22 + - **Version**: Numeric prefix (e.g., `0001` → version 1) 23 + - **Name**: Everything after first underscore (e.g., `add_repository_labels` → "add repository labels") 19 24 20 25 Examples: 21 - - `0001_remove_star_count_from_repository_stats.yaml` 22 - - `0002_add_repository_labels.yaml` 23 - - `0003_create_webhooks_table.yaml` 26 + - `0001_remove_star_count_from_repository_stats.yaml` → version 1, name "remove star count from repository stats" 27 + - `0002_add_repository_labels.yaml` → version 2, name "add repository labels" 28 + - `0003_create_webhooks_table.yaml` → version 3, name "create webhooks table" 24 29 25 30 ## Creating a New Migration 26 31 27 32 1. **Choose the next version number** - Look at existing migrations and increment by 1 28 - 2. **Create a new YAML file** with the naming convention above 29 - 3. **Write your SQL** - Use the `|` block scalar for clean multi-line SQL 30 - 4. **Use `IF EXISTS` / `IF NOT EXISTS`** where possible for idempotency 33 + 2. **Create a new YAML file** with format `000N_descriptive_name.yaml` 34 + 3. **Add description** (optional) - Explain what the migration does 35 + 4. **Write your SQL in `query`** - Use the `|` block scalar for clean multi-line SQL 36 + 5. **Use `IF EXISTS` / `IF NOT EXISTS`** where possible for idempotency (note: not supported for `DROP COLUMN`) 31 37 32 38 ## Examples 33 39 34 40 ### Simple single-statement migration: 35 41 42 + Filename: `0002_add_repository_description_index.yaml` 43 + 36 44 ```yaml 37 - version: 2 38 - name: add_repository_description_index 39 - up: | 45 + description: Add index on manifests description field for faster searches 46 + query: | 40 47 CREATE INDEX IF NOT EXISTS idx_manifests_description ON manifests(description); 41 48 ``` 42 49 43 50 ### Complex multi-statement migration: 51 + 52 + Filename: `0003_create_webhooks_table.yaml` 44 53 45 54 ```yaml 46 - version: 3 47 - name: create_webhooks_table 48 - up: | 55 + description: Create webhooks table for repository event notifications 56 + query: | 49 57 -- Create webhooks table 50 58 CREATE TABLE IF NOT EXISTS webhooks ( 51 59 id INTEGER PRIMARY KEY AUTOINCREMENT,
+51 -27
pkg/appview/db/schema.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "embed" 5 6 "fmt" 6 - "os" 7 + "io/fs" 7 8 "path/filepath" 8 9 "sort" 10 + "strconv" 11 + "strings" 9 12 10 13 _ "github.com/mattn/go-sqlite3" 11 14 "go.yaml.in/yaml/v4" 12 15 ) 16 + 17 + //go:embed migrations/*.yaml 18 + var migrationsFS embed.FS 13 19 14 20 const schema = ` 15 21 CREATE TABLE IF NOT EXISTS schema_migrations ( ··· 204 210 205 211 // Migration represents a database migration 206 212 type Migration struct { 207 - Version int `yaml:"version"` 208 - Name string `yaml:"name"` 209 - Up string `yaml:"up"` 213 + Version int 214 + Name string 215 + Description string `yaml:"description"` 216 + Query string `yaml:"query"` 210 217 } 211 218 212 219 // runMigrations applies any pending database migrations ··· 236 243 } 237 244 238 245 // Apply migration 239 - fmt.Printf("Applying migration %d: %s\n", m.Version, m.Name) 240 - if _, err := db.Exec(m.Up); err != nil { 246 + fmt.Printf("Applying migration %d: %s\n%s\n", m.Version, m.Name, m.Description) 247 + if _, err := db.Exec(m.Query); err != nil { 241 248 return fmt.Errorf("failed to apply migration %d (%s): %w", m.Version, m.Name, err) 242 249 } 243 250 ··· 252 259 return nil 253 260 } 254 261 255 - // loadMigrations loads all migration files from the migrations directory 262 + // loadMigrations loads all migration files from embedded filesystem 256 263 func loadMigrations() ([]Migration, error) { 257 - // Get the path to the migrations directory 258 - // Try relative to working directory first, then relative to this file 259 - migrationsDir := "pkg/appview/db/migrations" 260 - if _, err := os.Stat(migrationsDir); os.IsNotExist(err) { 261 - // Try embedded path (when running from different directory) 262 - migrationsDir = filepath.Join(".", "migrations") 263 - } 264 - 265 - // Read all .yaml files in the migrations directory 266 - files, err := filepath.Glob(filepath.Join(migrationsDir, "*.yaml")) 264 + // Read all migration files from embedded FS 265 + entries, err := fs.Glob(migrationsFS, "migrations/[0-9][0-9][0-9][0-9]_*.yaml") 267 266 if err != nil { 268 267 return nil, fmt.Errorf("failed to list migration files: %w", err) 269 268 } 270 269 271 270 var migrations []Migration 272 - for _, file := range files { 273 - data, err := os.ReadFile(file) 271 + for _, file := range entries { 272 + // Parse version and name from filename 273 + basename := filepath.Base(file) 274 + version, name, err := parseMigrationFilename(basename) 275 + if err != nil { 276 + return nil, fmt.Errorf("invalid migration filename %s: %w", basename, err) 277 + } 278 + 279 + // Read YAML content from embedded FS 280 + data, err := migrationsFS.ReadFile(file) 274 281 if err != nil { 275 282 return nil, fmt.Errorf("failed to read migration file %s: %w", file, err) 276 283 } ··· 280 287 return nil, fmt.Errorf("failed to parse migration file %s: %w", file, err) 281 288 } 282 289 290 + // Set version and name from filename 291 + m.Version = version 292 + m.Name = name 293 + 283 294 // Validate migration 284 - if m.Version <= 0 { 285 - return nil, fmt.Errorf("invalid migration version in %s: %d", file, m.Version) 286 - } 287 - if m.Name == "" { 288 - return nil, fmt.Errorf("missing migration name in %s", file) 289 - } 290 - if m.Up == "" { 291 - return nil, fmt.Errorf("missing migration 'up' SQL in %s", file) 295 + if m.Query == "" { 296 + return nil, fmt.Errorf("missing migration 'query' in %s", file) 292 297 } 293 298 294 299 migrations = append(migrations, m) ··· 296 301 297 302 return migrations, nil 298 303 } 304 + 305 + // parseMigrationFilename extracts version and name from migration filename 306 + // Expected format: 0001_migration_name.yaml 307 + // Returns: version (int), name (string), error 308 + // Note: Glob pattern ensures format is valid, so minimal validation needed 309 + func parseMigrationFilename(filename string) (int, string, error) { 310 + // Remove extension (.yaml or .yml) 311 + ext := filepath.Ext(filename) 312 + fileNameWithoutExt := filename[:len(filename)-len(ext)] 313 + 314 + // First 4 characters are the version (glob guarantees they're digits) 315 + version, _ := strconv.Atoi(fileNameWithoutExt[:4]) 316 + 317 + // Remainder after position 5 is the name (glob guarantees it exists) 318 + name := strings.ReplaceAll(fileNameWithoutExt[5:], "_", " ") 319 + name = strings.TrimSpace(name) 320 + 321 + return version, name, nil 322 + }