| @@ -189,51 +189,26 @@ func (s *Store) migrateTo(target int) error { |
| 189 | 189 | if err != nil { |
| 190 | 190 | return err |
| 191 | 191 | } |
| 192 | | step := func(sqlText string, newVersion int, fkOff bool) (retErr error) { |
| 193 | | if !fkOff { |
| 194 | | tx, err := s.DB.Begin() |
| 195 | | if err != nil { |
| 196 | | return err |
| 197 | | } |
| 198 | | defer tx.Rollback() |
| 199 | | if _, err := tx.Exec(sqlText); err != nil { |
| 200 | | return err |
| 201 | | } |
| 202 | | if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", newVersion)); err != nil { |
| 203 | | return err |
| 204 | | } |
| 205 | | return tx.Commit() |
| 206 | | } |
| 207 | | |
| 208 | | // A script whose first line is "-- foreign_keys: off" rebuilds a |
| 209 | | // table that other tables reference (labels, milestones): with |
| 210 | | // foreign keys on, the rebuild-by-rename loses the children's |
| 211 | | // rows. PRAGMA foreign_keys is a no-op inside a transaction, and |
| 212 | | // the pool gives no guarantee that a pragma set on one connection |
| 213 | | // is seen by the connection Begin() draws next, so the whole step |
| 214 | | // — pragma off, transaction, pragma on, foreign_key_check — runs |
| 215 | | // on a single pinned connection. |
| 216 | | ctx := context.Background() |
| 217 | | conn, err := s.DB.Conn(ctx) |
| 218 | | if err != nil { |
| 219 | | return err |
| 220 | | } |
| 221 | | defer conn.Close() |
| 222 | | if _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = OFF"); err != nil { |
| 223 | | return err |
| 192 | for cur < target { |
| 193 | m := ms[cur] |
| 194 | if err := s.migrateStep(m.up, m.version, m.upFKOff); err != nil { |
| 195 | return fmt.Errorf("migration %d up: %w", m.version, err) |
| 224 | 196 | } |
| 225 | | // The connection goes back to the pool when this returns, so every |
| 226 | | // path out of here has to put foreign keys back on first. |
| 227 | | restoreFK := func() error { |
| 228 | | _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = ON") |
| 229 | | return err |
| 197 | cur = m.version |
| 198 | } |
| 199 | for cur > target { |
| 200 | m := ms[cur-1] |
| 201 | if err := s.migrateStep(m.down, m.version-1, m.downFKOff); err != nil { |
| 202 | return fmt.Errorf("migration %d down: %w", m.version, err) |
| 230 | 203 | } |
| 231 | | defer func() { |
| 232 | | if err := restoreFK(); err != nil && retErr == nil { |
| 233 | | retErr = err |
| 234 | | } |
| 235 | | }() |
| 236 | | tx, err := conn.BeginTx(ctx, nil) |
| 204 | cur = m.version - 1 |
| 205 | } |
| 206 | return nil |
| 207 | } |
| 208 | |
| 209 | func (s *Store) migrateStep(sqlText string, newVersion int, fkOff bool) (retErr error) { |
| 210 | if !fkOff { |
| 211 | tx, err := s.DB.Begin() |
| 237 | 212 | if err != nil { |
| 238 | 213 | return err |
| 239 | 214 | } |
| @@ -244,44 +219,73 @@ func (s *Store) migrateTo(target int) error { |
| 244 | 219 | if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", newVersion)); err != nil { |
| 245 | 220 | return err |
| 246 | 221 | } |
| 247 | | if err := tx.Commit(); err != nil { |
| 248 | | return err |
| 249 | | } |
| 250 | | if err := restoreFK(); err != nil { |
| 251 | | return err |
| 252 | | } |
| 253 | | rows, err := conn.QueryContext(ctx, "PRAGMA foreign_key_check") |
| 254 | | if err != nil { |
| 255 | | return err |
| 256 | | } |
| 257 | | defer rows.Close() |
| 258 | | if rows.Next() { |
| 259 | | var table string |
| 260 | | var rowid sql.NullInt64 |
| 261 | | var referredTable string |
| 262 | | var fkid int |
| 263 | | if err := rows.Scan(&table, &rowid, &referredTable, &fkid); err != nil { |
| 264 | | return err |
| 265 | | } |
| 266 | | return fmt.Errorf("foreign_key_check failed after migration: %s", table) |
| 267 | | } |
| 268 | | return rows.Err() |
| 222 | return tx.Commit() |
| 269 | 223 | } |
| 270 | | for cur < target { |
| 271 | | m := ms[cur] |
| 272 | | if err := step(m.up, m.version, m.upFKOff); err != nil { |
| 273 | | return fmt.Errorf("migration %d up: %w", m.version, err) |
| 224 | |
| 225 | // A script whose first line is "-- foreign_keys: off" rebuilds a |
| 226 | // table that other tables reference (labels, milestones): with |
| 227 | // foreign keys on, the rebuild-by-rename loses the children's |
| 228 | // rows. PRAGMA foreign_keys is a no-op inside a transaction, and |
| 229 | // the pool gives no guarantee that a pragma set on one connection |
| 230 | // is seen by the connection Begin() draws next, so the whole step |
| 231 | // — pragma off, transaction, foreign_key_check, commit, pragma on — |
| 232 | // runs on a single pinned connection. The check runs before commit: |
| 233 | // checking after would report a violation once the bad schema and |
| 234 | // user_version were already persisted. |
| 235 | ctx := context.Background() |
| 236 | conn, err := s.DB.Conn(ctx) |
| 237 | if err != nil { |
| 238 | return err |
| 239 | } |
| 240 | defer conn.Close() |
| 241 | if _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = OFF"); err != nil { |
| 242 | return err |
| 243 | } |
| 244 | // The connection goes back to the pool when this returns, so every |
| 245 | // path out of here has to put foreign keys back on first. |
| 246 | defer func() { |
| 247 | if _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil && retErr == nil { |
| 248 | retErr = err |
| 274 | 249 | } |
| 275 | | cur = m.version |
| 250 | }() |
| 251 | tx, err := conn.BeginTx(ctx, nil) |
| 252 | if err != nil { |
| 253 | return err |
| 276 | 254 | } |
| 277 | | for cur > target { |
| 278 | | m := ms[cur-1] |
| 279 | | if err := step(m.down, m.version-1, m.downFKOff); err != nil { |
| 280 | | return fmt.Errorf("migration %d down: %w", m.version, err) |
| 255 | defer tx.Rollback() |
| 256 | if _, err := tx.Exec(sqlText); err != nil { |
| 257 | return err |
| 258 | } |
| 259 | if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", newVersion)); err != nil { |
| 260 | return err |
| 261 | } |
| 262 | // foreign_key_check works with enforcement off: it inspects the data |
| 263 | // directly rather than consulting the pragma. Running it here, inside |
| 264 | // the transaction, means a violation rolls back the whole rebuild |
| 265 | // (the deferred tx.Rollback fires) instead of leaving the bad schema |
| 266 | // and version committed. |
| 267 | rows, err := tx.QueryContext(ctx, "PRAGMA foreign_key_check") |
| 268 | if err != nil { |
| 269 | return err |
| 270 | } |
| 271 | if rows.Next() { |
| 272 | var table string |
| 273 | var rowid sql.NullInt64 |
| 274 | var referredTable string |
| 275 | var fkid int |
| 276 | if err := rows.Scan(&table, &rowid, &referredTable, &fkid); err != nil { |
| 277 | rows.Close() |
| 278 | return err |
| 281 | 279 | } |
| 282 | | cur = m.version - 1 |
| 280 | rows.Close() |
| 281 | return fmt.Errorf("foreign_key_check failed after migration: %s row %v", table, rowid) |
| 283 | 282 | } |
| 284 | | return nil |
| 283 | if err := rows.Err(); err != nil { |
| 284 | rows.Close() |
| 285 | return err |
| 286 | } |
| 287 | rows.Close() |
| 288 | return tx.Commit() |
| 285 | 289 | } |
| 286 | 290 | |
| 287 | 291 | // IsInternal reports whether err is the database or the I/O beneath it |