ory/hydra · error

neither normal (%s) nor legacy migration (%s) exist

Error message

neither normal (%s) nor legacy migration (%s) exist

What it means

During Down, if neither the migration's full-length version nor its legacy 14-char truncated version exists in schema_migration, the migrator cannot map the rollback to an applied migration and aborts. This prevents rolling back a migration that the tracking table says was never applied.

Source

Thrown at oryx/popx/migrator.go:383

			if i >= attemptSteps {
				break
			}
			l := mb.l.WithField("version", mi.Version).WithField("migration_name", mi.Name).WithField("migration_file", mi.Path)
			l.Debugf("handling migration %s", mi.Name)
			exists, err := c.Where("version = ?", mi.Version).Exists(mtn)
			if err != nil {
				return errors.Wrapf(err, "problem checking for migration version %s", mi.Version)
			}

			if !exists && len(mi.Version) > 14 {
				legacyVersion := mi.Version[:14]
				legacyVersionExists, err := c.Where("version = ?", legacyVersion).Exists(mtn)
				if err != nil {
					return errors.Wrapf(err, "problem checking for legacy migration version %s", legacyVersion)
				}

				if !legacyVersionExists {
					return errors.Errorf("neither normal (%s) nor legacy migration (%s) exist", mi.Version, legacyVersion)
				}
			} else if !exists {
				return errors.Errorf("migration version %s does not exist", mi.Version)
			}

			if err := mi.Valid(); err != nil {
				return errors.WithStack(err)
			}

			if mb.shouldNotUseTransaction(mi) {
				err := mi.Runner(mi, c)
				if err != nil {
					return errors.WithStack(err)
				}

				// #nosec G201 - mtn is a system-wide const
				if err := c.RawQuery(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), mi.Version).Exec(); err != nil {
					return errors.Wrapf(err, "problem deleting migration version %s. YOUR DATABASE MAY BE IN AN INCONSISTENT STATE! MANUAL INTERVENTION REQUIRED!", mi.Version)

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Inspect schema_migration rows and compare with your migration files' versions
  2. If the migration was truly applied, manually INSERT the missing version row (full-length format) and re-run Down
  3. If it was never applied, verify you are on the right database/DSN and skip this migration
  4. Restore consistency from a backup if rows were deleted accidentally

Example fix

// before: row missing, Down aborts
// after: restore the tracked version then re-run Down
// INSERT INTO schema_migration (version) VALUES ('20221012101010');
Defensive patterns

Strategy: validation

Validate before calling

// verify every down-candidate version is tracked before calling Down
for _, v := range expectedVersions {
    var ok bool
    err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM schema_migration WHERE version=? OR version=?)", v, v[:min(14,len(v))]).Scan(&ok)
    if err != nil || !ok { return fmt.Errorf("version %s not tracked", v) }
}

Try / catch

if err := box.Down(ctx, 1); err != nil {
    if strings.Contains(err.Error(), "neither normal nor legacy migration exist") {
        // compare schema_migration rows with migration files, restore missing rows or correct DSN
    }
    return err
}

Prevention

When it happens

Trigger: Calling Down when the migration count says rows exist but the specific candidate migration's version (and its legacy form) is missing from schema_migration — e.g. rows were manually deleted, or migrations were applied by a different tool/version that recorded different versions.

Common situations: Manual cleanup of schema_migration rows; running Down against a database migrated by another tool (goose, golang-migrate) with a different versioning scheme; truncated vs full timestamp mismatch after DB restore from partial backup.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/d56f91f85708464c. Report an issue: GitHub.