Hone logo
Hone
Problems

Go Database Migrations: Building a Schema Evolution System

This challenge asks you to implement a foundational system for managing database schema changes in a Go application. Database migrations are crucial for evolving your application's data structure over time in a controlled and repeatable manner, ensuring consistency across different environments.

Problem Description

Your task is to build a Go package that can manage and apply database migrations. This system should allow developers to define schema changes as individual migration files, track which migrations have been applied, and apply new migrations in the correct order.

Key Requirements:

  • Migration Definition: Each migration should be defined as a Go struct with at least two methods: Up() to apply the schema change and Down() to revert it.
  • Migration Ordering: Migrations must be applied in a specific, ordered sequence. This order can be determined by timestamps or sequential version numbers.
  • State Tracking: The system needs to track which migrations have already been applied to the database. This is typically done by creating a dedicated "migrations" table in the database itself.
  • Applying Migrations: A function should exist to iterate through pending migrations, apply their Up() method, and record their successful application in the tracking table.
  • Reverting Migrations: A function should exist to revert the latest applied migration by calling its Down() method and removing its record from the tracking table.
  • Database Agnosticism (Optional but Recommended): While the initial implementation can target a specific database (e.g., PostgreSQL), consider designing it with potential for other SQL databases in mind.

Expected Behavior:

  1. When the migration system is initialized, it should check the "migrations" table.
  2. If the table doesn't exist, it should be created.
  3. The system should scan for available migration files (or registered migration structs).
  4. It should compare the list of available migrations with those already recorded in the "migrations" table.
  5. Any new migrations should be applied sequentially using their Up() method. After each successful Up() operation, the migration's identifier should be inserted into the "migrations" table.
  6. A mechanism should be provided to apply Down() methods to reverse the latest migration(s).

Edge Cases:

  • What happens if a migration fails during the Up() or Down() process? The system should handle this gracefully, perhaps by rolling back any partial changes or returning an error.
  • Concurrent access to the "migrations" table: Consider how to prevent race conditions if multiple instances of your application might try to run migrations simultaneously. A simple locking mechanism or transaction might be necessary.
  • Handling existing schemas: If the database already has tables, the Up() methods should be idempotent or designed to handle pre-existing structures without error.

Examples

Example 1: Basic Migration Application

Let's assume you have two migration files:

  • 001_create_users_table.go: Creates a users table.
  • 002_add_email_to_users.go: Adds an email column to the users table.

Initial State:

  • Database is empty.
  • migrations table does not exist.

Action: Run the migration tool.

Expected Output:

  • migrations table is created.
  • users table is created.
  • email column is added to the users table.
  • The migrations table contains entries for 001_create_users_table and 002_add_email_to_users.

Example 2: Reverting a Migration

Initial State:

  • Database has users table and email column.
  • migrations table contains entries for both migrations.

Action: Run a command to revert the latest migration.

Expected Output:

  • email column is dropped from the users table.
  • The entry for 002_add_email_to_users is removed from the migrations table.

Constraints

  • The migration system must be implemented entirely in Go.
  • You should use a standard SQL database driver (e.g., database/sql package) for interacting with the database.
  • For simplicity in this challenge, you can assume a single database connection is used for migration operations.
  • Migration identifiers should be sortable (e.g., YYYYMMDDHHMMSS_description or a simple integer sequence).

Notes

  • Consider how you will register your migrations with the migration system. A common approach is to have a central registry or to scan a specific directory for migration files.
  • Think about the transactionality of your Up() and Down() operations. Applying a migration should ideally be an atomic operation.
  • For more robust systems, you might consider features like unique migration names, checksums for migration files, and robust error handling with rollback capabilities.
  • The problem statement allows for a specific database to be targeted initially. PostgreSQL or SQLite are good choices for ease of setup.
Loading editor...
go