| @@ -189,51 +189,26 @@ func (s *Store) migrateTo(target int) error { |
| 189 | if err != nil { |
189 | if err != nil { |
| 190 | return err |
190 | return err |
| 191 | } |
191 | } |
| 192 | step := func(sqlText string, newVersion int, fkOff bool) (retErr error) { |
192 | for cur < target { |
| 193 | if !fkOff { |
193 | m := ms[cur] |
| 194 | tx, err := s.DB.Begin() |
194 | if err := s.migrateStep(m.up, m.version, m.upFKOff); err != nil { |
| 195 | if err != nil { |
195 | return fmt.Errorf("migration %d up: %w", m.version, err) |
| 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 |
| |
| 224 | } |
196 | } |
| 225 | // The connection goes back to the pool when this returns, so every |
197 | cur = m.version |
| 226 | // path out of here has to put foreign keys back on first. |
198 | } |
| 227 | restoreFK := func() error { |
199 | for cur > target { |
| 228 | _, err := conn.ExecContext(ctx, "PRAGMA foreign_keys = ON") |
200 | m := ms[cur-1] |
| 229 | return err |
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() { |
204 | cur = m.version - 1 |
| 232 | if err := restoreFK(); err != nil && retErr == nil { |
205 | } |
| 233 | retErr = err |
206 | return nil |
| 234 | } |
207 | } |
| 235 | }() |
208 | |
| 236 | tx, err := conn.BeginTx(ctx, nil) |
209 | func (s *Store) migrateStep(sqlText string, newVersion int, fkOff bool) (retErr error) { |
| |
210 | if !fkOff { |
| |
211 | tx, err := s.DB.Begin() |
| 237 | if err != nil { |
212 | if err != nil { |
| 238 | return err |
213 | return err |
| 239 | } |
214 | } |
| @@ -244,44 +219,73 @@ func (s *Store) migrateTo(target int) error { |
| 244 | if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", newVersion)); err != nil { |
219 | if _, err := tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", newVersion)); err != nil { |
| 245 | return err |
220 | return err |
| 246 | } |
221 | } |
| 247 | if err := tx.Commit(); err != nil { |
222 | return tx.Commit() |
| 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() |
| |
| 269 | } |
223 | } |
| 270 | for cur < target { |
224 | |
| 271 | m := ms[cur] |
225 | // A script whose first line is "-- foreign_keys: off" rebuilds a |
| 272 | if err := step(m.up, m.version, m.upFKOff); err != nil { |
226 | // table that other tables reference (labels, milestones): with |
| 273 | return fmt.Errorf("migration %d up: %w", m.version, err) |
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 { |
255 | defer tx.Rollback() |
| 278 | m := ms[cur-1] |
256 | if _, err := tx.Exec(sqlText); err != nil { |
| 279 | if err := step(m.down, m.version-1, m.downFKOff); err != nil { |
257 | return err |
| 280 | return fmt.Errorf("migration %d down: %w", m.version, 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 | // IsInternal reports whether err is the database or the I/O beneath it |
291 | // IsInternal reports whether err is the database or the I/O beneath it |