schema

package module
v1.1.14-0...-4236df0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 17, 2021 License: MIT Imports: 11 Imported by: 0

README

Schema - Embedded Database Migration Library for Go

An opinionated, embeddable library for tracking and application modifications to your Go application's database schema.

Build Status Go Report Card codecov GoDoc

Package Opinions

There are many other schema migration tools. This one exists because of a particular set of opinions:

  1. Database credentials are runtime configuration details, but database schema is a build-time applicaton dependency, which means it should be "compiled in" to the build, and should not rely on external tools.
  2. Using an external command-line tool for schema migrations needlessly complicates testing and deployment.
  3. Sequentially-numbered integer migration IDs will create too many unnecessary schema collisions on a distributed, asynchronously-communicating team.
  4. SQL is the best language to use to specify changes to SQL schemas.
  5. "Down" migrations add needless complication, aren't often used, and are tedious to properly test when they are used. In the unlikely event you need to migrate backwards, it's possible to write the "rollback" migration as a separate "up" migration.
  6. Deep dependency chains should be avoided, especially in a compiled binary. We don't want to import an ORM into our binaries just to get SQL the features of this package. The schema package imports only standard library packages (NOTE *We do import ory/dockertest in our tests).
  7. Storing raw SQL as strings inside .go files is an acceptable trade-off for the above. (For users who depend on .sql files, bolt-on integrations of go-bindata, go-rice or similar binary embedders are possible).

Supported Databases

This package was extracted from a PostgreSQL project, so that's all that's tested at the moment, but all the databases below should be easy to add with a contribution:

  • PostgreSQL
  • SQLite
  • MySQL (open a Pull Request)
  • SQL Server (open a Pull Request)

Roadmap

  • Basic feature set for PostgreSQL
  • Continuous integration tests / Code coverage
  • Basic Documentation: basic overview, usage documentation
  • Add a validation pass inside Apply() to throw an error when checksums or IDs of previously-run migrations appear to have been changed or when problematic migration IDs are being used.
  • Enhancements to facilitate asset embedding tools like go-rice or packr to get syntax highlighting for external .sql files which are embedded only at build time (or clear documentation to explain how they can be used without changing schema).
  • Support for additional databases.

Usage Instructions

Create a schema.Migrator in your bootstrap/config/database connection code, then call its Apply() method with your database connection and a slice of *schema.Migration structs. Like so:

db, err := sql.Open(...) // Or however you get a *sql.DB

migrator := schema.NewMigrator()
migrator.Apply(db, []*schema.Migration{
  &schema.Migration{
    ID: "2019-09-24 Create Albums",
    Script: `
    CREATE TABLE albums (
      id SERIAL PRIMARY KEY,
      title CHARACTER VARYING (255) NOT NULL
    )
    `
  }
})

The .Apply() function figures out which of the supplied Migrations have not yet been executed in the database (based on the ID), and executes the Script for each in alphabetical order by ID. This procedure means its OK to call .Apply() on the same Migrator with a different set of Migrations each time (which you might do if you want to avoid the ugliness of one giant migrations.go file with hundreds of lines of embedded SQL in it).

The NewMigrator() function accepts option arguments to customize the dialect and the name of the migration tracking table. By default, the tracking table will be set to schema.DefaultTableName (schema_migrations). To change it to my_migrations:

migrator := schema.NewMigrator(schema.WithTableName("my_migrations"))

It is theoretically possible to create multiple Migrators and to use mutliple migration tracking tables within the same application and database.

It is also OK for multiple processes to run Apply on identically configured migrators simultaneously. The Migrator only creates the tracking table if it does not exist, and then locks it to modifications while building and running the migration plan. This means that the first-arriving process will win and will perform its migrations on the database.

Rules of Applying Migrations

  1. Never, ever change the ID or Script of a Migration which has already been executed on your database. If you've made a mistake, you'll need to correct it in a subsequent migration.

  2. Use a consistent, but descriptive format for migration IDs. Your format Consider prefixing them with today's timestamp. Examples:

     ID: "2019-01-01T13:45:00 Creates Users"
     ID: "2001-12-18 001 Changes the Default Value of User Affiliate ID"
    

    Do not use simple sequentialnumbers like ID: "1".

Migration Ordering

Migrations are not executed in the order they are specified in the slice. They will be re-sorted alphabetically by their IDs before executing them.

Inspecting the State of Applied Migrations

Call migrator.GetAppliedMigrations(db) to get info about migrations which have been successfully applied.

Contributions

... are welcome. Please include tests with your contribution. We've integrated dockertest to automate the process of creating clean test databases.

Before contributing, please read the package opinions section. If your contribution is in disagreement with those opinions, then there's a good chance a different schema migration tool is more appropriate.

Documentation

Index

Constants

View Source
const DefaultTableName = "schema_migrations"

DefaultTableName defines the name of the database table which will hold the status of applied migrations

Variables

View Source
var ErrNilDB = errors.New("DB pointer is nil")

ErrNilDB is thrown when the database pointer is nil

View Source
var ErrSQLiteLockTimeout = errors.New("sqlite: timeout requesting lock")
View Source
var Postgres = postgresDialect{}

Postgres is the dialect for Postgres-compatible databases

Functions

func MigrationIDFromFilename

func MigrationIDFromFilename(filename string) string

MigrationIDFromFilename removes directory paths and extensions from the filename to make a friendlier Migration ID

func NewSQLite

func NewSQLite(opts ...func(s *sqliteDialect)) *sqliteDialect

NewSQLite creates a new sqlite dialect. Customization of the lock table name and lock duration are made with WithSQLiteLockTable and WithSQLiteLockDuration options.

func SortMigrations

func SortMigrations(migrations []*Migration)

SortMigrations sorts a slice of migrations by their IDs

func WithSQLiteLockDuration

func WithSQLiteLockDuration(d time.Duration) func(s *sqliteDialect)

WithSQLiteLockDuration sets the lock timeout and expiration. The default is 30 seconds. If the migration will take longer (e.g. copying of entire large tables), increase the timeout accordingly.

func WithSQLiteLockTable

func WithSQLiteLockTable(name string) func(s *sqliteDialect)

WithSQLiteLockTable configures the lock table name. The default name without this option is 'schema_lock'.

Types

type AppliedMigration

type AppliedMigration struct {
	Migration
	Checksum              string
	ExecutionTimeInMillis int
	AppliedAt             time.Time
}

AppliedMigration is a schema change which was successfully completed

type Dialect

type Dialect interface {
	QuotedTableName(schemaName, tableName string) string
	CreateSQL(tableName string) string
	SelectSQL(tableName string) string
	InsertSQL(tableName string) string
}

Dialect defines the interface for a database dialect. All interface functions take the customized table name as input and return a SQL statement with placeholders appropriate to the database.

type File

type File interface {
	Name() string
	Read(b []byte) (n int, err error)
}

File wraps the standard library io.Read and os.File.Name methods

type Locker

type Locker interface {
	Lock(db *sql.DB) error
	Unlock(db *sql.DB) error
}

Locker defines an interface that implements locking.

type Logger

type Logger interface {
	Print(...interface{})
}

Logger is the interface for logging operations of the logger. By default the migrator operates silently. Providing a Logger enables output of the migrator's operations.

type Migration

type Migration struct {
	ID     string
	Script string
}

Migration is a yet-to-be-run change to the schema

func MigrationFromFile

func MigrationFromFile(file File) (migration *Migration, err error)

MigrationFromFile builds a migration by reading from an open File-like object. The migration's ID will be based on the file's name. The file will *not* be closed after being read.

func MigrationFromFilePath

func MigrationFromFilePath(filename string) (migration *Migration, err error)

MigrationFromFilePath creates a Migration from a path on disk

func MigrationsFromDirectoryPath

func MigrationsFromDirectoryPath(dirPath string) (migrations []*Migration, err error)

MigrationsFromDirectoryPath retrieves a slice of Migrations from the contents of the directory. Only .sql files are read

type Migrator

type Migrator struct {
	SchemaName string
	TableName  string
	Dialect    Dialect
	Logger     Logger
}

Migrator is an instance customized to perform migrations on a particular against a particular tracking table and with a particular dialect defined.

func NewMigrator

func NewMigrator(options ...Option) Migrator

NewMigrator creates a new Migrator with the supplied options

func (Migrator) Apply

func (m Migrator) Apply(db *sql.DB, migrations []*Migration) (err error)

Apply takes a slice of Migrations and applies any which have not yet been applied

func (Migrator) GetAppliedMigrations

func (m Migrator) GetAppliedMigrations(db Queryer) (applied map[string]*AppliedMigration, err error)

GetAppliedMigrations retrieves all already-applied migrations in a map keyed by the migration IDs

func (Migrator) QuotedTableName

func (m Migrator) QuotedTableName() string

QuotedTableName returns the dialect-quoted fully-qualified name for the migrations tracking table

type Option

type Option func(m Migrator) Migrator

Option supports option chaining when creating a Migrator. An Option is a function which takes a Migrator and returns a Migrator with an Option modified.

func WithDialect

func WithDialect(dialect Dialect) Option

WithDialect builds an Option which will set the supplied dialect on a Migrator. Usage: NewMigrator(WithDialect(MySQL))

func WithLogger

func WithLogger(logger Logger) Option

WithLogger builds an Option which will set the supplied Logger on a Migrator. Usage: NewMigrator(WithLogger(logrus.New()))

func WithTableName

func WithTableName(names ...string) Option

WithTableName is an option which customizes the name of the schema_migrations tracking table. It can be called with either 1 or 2 string arguments. If called with 2 arguments, the first argument is assumed to be a schema qualifier (for example, WithTableName("public", "schema_migrations") would assign the table named "schema_migrations" in the the default "public" schema for Postgres)

type Queryer

type Queryer interface {
	Query(sql string, args ...interface{}) (*sql.Rows, error)
}

Queryer is something which can execute a Query (either a sql.DB or a sql.Tx))

type SQLLocker

type SQLLocker interface {
	LockSQL(tableName string) string
	UnlockSQL(tableName string) string
}

SQLLocker defines an interface that implements locking using a single SQL statement.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL