applyOne reads a migration file and applies it. For migrations without the no-transaction directive: SQL + tracker insert run in a single transaction so a partial-failure migration is safe to retry. Tx isolation defaults to READ COMMITTED, which is correct here (we don't want SERIALIZABLE forcing r
(ctx context.Context, conn *pgxpool.Conn, fsys fs.FS, name string)
| 348 | // itself; the directive is opt-in for the specific Postgres features |
| 349 | // that require it. |
| 350 | func applyOne(ctx context.Context, conn *pgxpool.Conn, fsys fs.FS, name string) error { |
| 351 | body, err := fs.ReadFile(fsys, name) |
| 352 | if err != nil { |
| 353 | return fmt.Errorf("read %s: %w", name, err) |
| 354 | } |
| 355 | |
| 356 | if hasNoTransactionDirective(string(body)) { |
| 357 | // pgx's simple protocol sends multi-statement strings to the |
| 358 | // server as a single Query message, which Postgres wraps in an |
| 359 | // implicit transaction — defeating the directive's purpose and |
| 360 | // surfacing as a confusing "cannot run inside a transaction |
| 361 | // block" error from the actual offending statement. Catch it |
| 362 | // here with a clear message. |
| 363 | if looksMultiStatement(string(body)) { |
| 364 | return fmt.Errorf("migration %s uses -- e2a:no-transaction but contains multiple statements; split into separate files (one per statement)", name) |
| 365 | } |
| 366 | if _, err := conn.Exec(ctx, string(body)); err != nil { |
| 367 | return fmt.Errorf("exec migration (no-transaction): %w", err) |
| 368 | } |
| 369 | if _, err := conn.Exec(ctx, |
| 370 | "INSERT INTO schema_migrations (filename) VALUES ($1) ON CONFLICT DO NOTHING", |
| 371 | name, |
| 372 | ); err != nil { |
| 373 | return fmt.Errorf("record migration: %w", err) |
| 374 | } |
| 375 | return nil |
| 376 | } |
| 377 | |
| 378 | tx, err := conn.BeginTx(ctx, pgx.TxOptions{}) |
| 379 | if err != nil { |
| 380 | return fmt.Errorf("begin tx: %w", err) |
| 381 | } |
| 382 | defer func() { _ = tx.Rollback(ctx) }() |
| 383 | |
| 384 | if _, err := tx.Exec(ctx, string(body)); err != nil { |
| 385 | return fmt.Errorf("exec migration: %w", err) |
| 386 | } |
| 387 | if _, err := tx.Exec(ctx, |
| 388 | "INSERT INTO schema_migrations (filename) VALUES ($1) ON CONFLICT DO NOTHING", |
| 389 | name, |
| 390 | ); err != nil { |
| 391 | return fmt.Errorf("record migration: %w", err) |
| 392 | } |
| 393 | if err := tx.Commit(ctx); err != nil { |
| 394 | return fmt.Errorf("commit: %w", err) |
| 395 | } |
| 396 | return nil |
| 397 | } |
no test coverage detected