#1116·goose

goose create can produce duplicate migration versions

Author: faiyazrahmannCreated Sep 2, 2026Updated Sep 6, 2026

Follow up to #707 and #1104. That PR fixed the %!w(<nil>) error when the exact same filename already exists. But the underlying problem is that O_EXCL only protects the filename, not the version. Two migrations with different names can still land on the same version.

Timestamp mode

No concurrency needed, just two commands in the same second:

$ goose -dir ./m create alpha sql
2026/09/03 03:19:32 Created new file: m/20260902211932_alpha.sql
$ goose -dir ./m create beta sql
2026/09/03 03:19:32 Created new file: m/20260902211932_beta.sql

Both succeed since the filenames differ. It blows up later:

$ goose -dir ./m sqlite3 test.db up
panic: goose: duplicate version 20260902211932 detected:
      m/20260902211932_beta.sql
      m/20260902211932_alpha.sql

That panic is Migrations.Less in migrate.go:38. provider_collect.go:73 returns a proper error for the same case, so it is only the legacy path that panics.

timestampFormat in goose.go:17 is 20060102150405, so anything created within the same second collides.

Sequential mode

Worse here. create.go:33-47 scans the dir to compute last.Version+1, then creates the file at line 61. Classic TOCTOU.

Ran 8 concurrent goose create -s into a fresh dir, 15 times. 14 of 15 runs produced duplicate versions. A typical result:

00001_a.sql
00001_b.sql
00001_h.sql

8 creates, 3 files. The other 5 panicked inside create itself, because the dir scan hit the duplicates the other processes had just written:

panic: goose: duplicate version 1 detected:
      00001_b.sql
      00001_a.sql

So goose create can panic and silently drop migrations.

Possible directions

  1. Sub second precision in timestampFormat. Fixes timestamp mode cleanly but changes generated version numbers for everyone.
  2. Detect the collision and bump the version, retrying the create.
  3. A lock file around the scan and create for sequential mode.

Whichever way it goes, the create should stay atomic rather than going back to a stat then create check, since that is the race #1104 just removed.

I’d be happy to take this on and put together a PR for whichever approach you think makes the most sense.