Skip to content

limen generate migrations creates duplicate migration versions #7

@rokuosan

Description

@rokuosan

Summary

limen generate migrations generates multiple migration files with the same timestamp/version prefix.

This causes tools like golang-migrate to fail because the migration version must be unique. Also, manually renaming the files is error-prone because some generated tables have foreign key dependencies and must be applied in the correct order.

Steps to reproduce

$ limen generate migrations --driver postgres --dsn "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable"
Generated migration: 20260607034408_accounts
Generated migration: 20260607034408_rate_limits
Generated migration: 20260607034408_sessions
Generated migration: 20260607034408_users
Generated migration: 20260607034408_verifications

To apply these migrations, use any other migration tool that supports SQL files like goose, golang-migrate, etc. Or apply them manually.

$ g st -su
?? migrations/20260607034408_accounts.down.sql
?? migrations/20260607034408_accounts.up.sql
?? migrations/20260607034408_rate_limits.down.sql
?? migrations/20260607034408_rate_limits.up.sql
?? migrations/20260607034408_sessions.down.sql
?? migrations/20260607034408_sessions.up.sql
?? migrations/20260607034408_users.down.sql
?? migrations/20260607034408_users.up.sql
?? migrations/20260607034408_verifications.down.sql
?? migrations/20260607034408_verifications.up.sql

$ migrate -source file://migrations -database "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" up
error: failed to open source, "file://migrations": duplicate migration file: 20260607034408_rate_limits.down.sql
.limen/schemas.json
{
  "schemas": {
    "accounts": {
      "TableName": "accounts",
      "Columns": [
        {
          "Name": "id",
          "LogicalField": "id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": true,
          "DefaultValue": "",
          "Tags": {
            "json": "id"
          }
        },
        {
          "Name": "user_id",
          "LogicalField": "user_id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "user_id"
          }
        },
        {
          "Name": "provider",
          "LogicalField": "provider",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "provider"
          }
        },
        {
          "Name": "provider_account_id",
          "LogicalField": "provider_account_id",
          "Type": "string",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "provider_account_id"
          }
        },
        {
          "Name": "access_token",
          "LogicalField": "access_token",
          "Type": "text",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "access_token"
          }
        },
        {
          "Name": "refresh_token",
          "LogicalField": "refresh_token",
          "Type": "text",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "refresh_token"
          }
        },
        {
          "Name": "access_token_expires_at",
          "LogicalField": "access_token_expires_at",
          "Type": "time.Time",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "access_token_expires_at"
          }
        },
        {
          "Name": "scope",
          "LogicalField": "scope",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "scope"
          }
        },
        {
          "Name": "id_token",
          "LogicalField": "id_token",
          "Type": "text",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "id_token"
          }
        },
        {
          "Name": "created_at",
          "LogicalField": "created_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "@now()",
          "Tags": {
            "json": "created_at"
          }
        },
        {
          "Name": "updated_at",
          "LogicalField": "updated_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "updated_at"
          }
        }
      ],
      "Indexes": [
        {
          "Name": "idx_accounts_user_id_provider",
          "Columns": [
            "user_id"
          ],
          "Unique": false
        },
        {
          "Name": "idx_accounts_provider_provider_account_id",
          "Columns": [
            "provider",
            "provider_account_id"
          ],
          "Unique": true
        }
      ],
      "ForeignKeys": [
        {
          "Name": "fk_accounts_users_user_id",
          "Column": "user_id",
          "ReferencedSchema": "users",
          "ReferencedField": "id",
          "OnDelete": "RESTRICT",
          "OnUpdate": "CASCADE"
        }
      ],
      "SchemaName": "accounts",
      "Extends": "",
      "PluginName": ""
    },
    "rate_limits": {
      "TableName": "rate_limits",
      "Columns": [
        {
          "Name": "id",
          "LogicalField": "id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": true,
          "DefaultValue": "",
          "Tags": {
            "json": "id"
          }
        },
        {
          "Name": "key",
          "LogicalField": "key",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "key"
          }
        },
        {
          "Name": "count",
          "LogicalField": "count",
          "Type": "int",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "count"
          }
        },
        {
          "Name": "last_request_at",
          "LogicalField": "last_request_at",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "last_request_at"
          }
        }
      ],
      "Indexes": [
        {
          "Name": "idx_rate_limits_key",
          "Columns": [
            "key"
          ],
          "Unique": true
        }
      ],
      "ForeignKeys": null,
      "SchemaName": "rate_limits",
      "Extends": "",
      "PluginName": ""
    },
    "sessions": {
      "TableName": "sessions",
      "Columns": [
        {
          "Name": "id",
          "LogicalField": "id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": true,
          "DefaultValue": "",
          "Tags": {
            "json": "id"
          }
        },
        {
          "Name": "token",
          "LogicalField": "token",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "token"
          }
        },
        {
          "Name": "user_id",
          "LogicalField": "user_id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "user_id"
          }
        },
        {
          "Name": "created_at",
          "LogicalField": "created_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "@now()",
          "Tags": {
            "json": "created_at"
          }
        },
        {
          "Name": "expires_at",
          "LogicalField": "expires_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "expires_at"
          }
        },
        {
          "Name": "last_access",
          "LogicalField": "last_access",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "last_access"
          }
        },
        {
          "Name": "metadata",
          "LogicalField": "metadata",
          "Type": "map[string]any",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "metadata"
          }
        }
      ],
      "Indexes": [
        {
          "Name": "idx_sessions_token",
          "Columns": [
            "token"
          ],
          "Unique": true
        },
        {
          "Name": "idx_sessions_user_id",
          "Columns": [
            "user_id"
          ],
          "Unique": false
        }
      ],
      "ForeignKeys": [
        {
          "Name": "fk_sessions_user_id",
          "Column": "user_id",
          "ReferencedSchema": "users",
          "ReferencedField": "id",
          "OnDelete": "RESTRICT",
          "OnUpdate": "CASCADE"
        }
      ],
      "SchemaName": "sessions",
      "Extends": "",
      "PluginName": ""
    },
    "users": {
      "TableName": "users",
      "Columns": [
        {
          "Name": "id",
          "LogicalField": "id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": true,
          "DefaultValue": "",
          "Tags": {
            "json": "id"
          }
        },
        {
          "Name": "email",
          "LogicalField": "email",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "email"
          }
        },
        {
          "Name": "password",
          "LogicalField": "password",
          "Type": "string",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "-"
          }
        },
        {
          "Name": "email_verified_at",
          "LogicalField": "email_verified_at",
          "Type": "time.Time",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "email_verified_at"
          }
        },
        {
          "Name": "first_name",
          "LogicalField": "first_name",
          "Type": "string",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "first_name"
          }
        },
        {
          "Name": "last_name",
          "LogicalField": "last_name",
          "Type": "string",
          "IsNullable": true,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "last_name"
          }
        },
        {
          "Name": "created_at",
          "LogicalField": "created_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "@now()",
          "Tags": {
            "json": "created_at"
          }
        },
        {
          "Name": "updated_at",
          "LogicalField": "updated_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "updated_at"
          }
        }
      ],
      "Indexes": [
        {
          "Name": "idx_users_email",
          "Columns": [
            "email"
          ],
          "Unique": true
        }
      ],
      "ForeignKeys": [],
      "SchemaName": "users",
      "Extends": "",
      "PluginName": ""
    },
    "verifications": {
      "TableName": "verifications",
      "Columns": [
        {
          "Name": "id",
          "LogicalField": "id",
          "Type": "int64",
          "IsNullable": false,
          "IsPrimaryKey": true,
          "DefaultValue": "",
          "Tags": {
            "json": "id"
          }
        },
        {
          "Name": "subject",
          "LogicalField": "subject",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "subject"
          }
        },
        {
          "Name": "value",
          "LogicalField": "value",
          "Type": "string",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "value"
          }
        },
        {
          "Name": "expires_at",
          "LogicalField": "expires_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "expires_at"
          }
        },
        {
          "Name": "created_at",
          "LogicalField": "created_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "@now()",
          "Tags": {
            "json": "created_at"
          }
        },
        {
          "Name": "updated_at",
          "LogicalField": "updated_at",
          "Type": "time.Time",
          "IsNullable": false,
          "IsPrimaryKey": false,
          "DefaultValue": "",
          "Tags": {
            "json": "updated_at"
          }
        }
      ],
      "Indexes": [
        {
          "Name": "idx_verifications_value",
          "Columns": [
            "value"
          ],
          "Unique": true
        },
        {
          "Name": "idx_verifications_subject",
          "Columns": [
            "subject"
          ],
          "Unique": false
        }
      ],
      "ForeignKeys": null,
      "SchemaName": "verifications",
      "Extends": "",
      "PluginName": ""
    }
  },
  "useAutoIncrementID": true
}

Expected behaviour

The CLI should generate migration filenames with unique, monotonically increasing versions in an order that respects foreign key dependencies.

For example:

20260607034408_users.up.sql
20260607034409_rate_limits.up.sql
20260607034410_verifications.up.sql
20260607034411_accounts.up.sql
20260607034412_sessions.up.sql

The exact order of independent tables does not matter, but dependent tables should come after the tables they reference.

Alternatively, generating a single migration file containing all table creation statements in dependency-safe order would also avoid the duplicate version issue.

Actual behaviour

limen generate migrations generates multiple migration files with the same timestamp/version prefix.

$ limen generate migrations --driver postgres --dsn "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable"
Generated migration: 20260607034408_accounts
Generated migration: 20260607034408_rate_limits
Generated migration: 20260607034408_sessions
Generated migration: 20260607034408_users
Generated migration: 20260607034408_verifications

To apply these migrations, use any other migration tool that supports SQL files like goose, golang-migrate, etc. Or apply them manually.

$ g st -su
?? migrations/20260607034408_accounts.down.sql
?? migrations/20260607034408_accounts.up.sql
?? migrations/20260607034408_rate_limits.down.sql
?? migrations/20260607034408_rate_limits.up.sql
?? migrations/20260607034408_sessions.down.sql
?? migrations/20260607034408_sessions.up.sql
?? migrations/20260607034408_users.down.sql
?? migrations/20260607034408_users.up.sql
?? migrations/20260607034408_verifications.down.sql
?? migrations/20260607034408_verifications.up.sql

Affected module

cmd/limen (CLI)

Limen version(s)

v0.1.1

Go version

go version go1.26.4 darwin/arm64

OS / environment

macOS 26.3

Additional context

No response

Checklist

  • I have searched existing issues and this is not a duplicate.
  • I have confirmed this is not a security vulnerability (which should be reported privately).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions