dmorph

package module
v0.0.0-...-e306806 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: MPL-2.0 Imports: 16 Imported by: 0

README

Logo
Go Version Latest Release Test Pipeline Result CodeQL Pipeline Result Security Pipeline Result Go Report Card Code Coverage CodeRabbit Reviews OpenSSF Scorecard REUSE compliance FOSSA Status FOSSA Status GoDoc Reference

DMorph

DMorph (pronounced [diˈmɔʁf]) is a database migration library. Programs that use a database and have to preserve the data between versions can utilize DMorph to apply the necessary migration steps. If a program can afford to lose all data between version upgrades, this library is not necessary.

Includes direct support for the following relational database management systems:

Additional database management systems can be included providing the necessary queries. While DMorph offers support for these database management systems, it does depend on anything than the Go standard library. Any other dependencies are solely for testing purposes and do not affect users of this library.

Installation

To install DMorph, you can use the following command:

$ go get github.com/AlphaOne1/dmorph

Getting Started

DMorph applies migrations to a database. A migration is a series of steps, defined either in an SQL file or programmatically.

Migration from File

A typical migration file consists of a sequence of SQL statements. Each statement needs to be finalized with a semicolon ;. If a semicolon is found alone at the beginning of a line, all previous statements, that were not yet executed, are executed in one call to Exec in a transaction. A migration is executed completely inside a transaction. If any of the steps of a migration fails, a rollback is issued and the process stops. Take care, that not all database management systems offer a rollback of DDL (CREATE, DROP, ...) statements.

An example for a migration inside a file 01_base_tables is as follows:

CREATE TABLE tab0 (
    id string PRIMARY KEY
)
;

CREATE TABLE tab1 (
    id string PRIMARY KEY
)
;

It can be applied to an already open database with the following snipped:

package testprog

import (
    "database/sql"
    "github.com/AlphaOne1/dmorph"
)

func migrate(db *sql.DB) error {
    return dmorph.Run(db,
        dmorph.WithDialect(dmorph.DialectSQLite()),
        dmorph.WithMigrationFromFile("01_base_tables.sql"))
}

...

In this example just one file is used, the WithMigrationFromFile can be given multiple times. Migrations are executed in alphabetical order of their key. For files the key is the file's name. The WithDialect option is used to select the correct SQL dialect, as DMorph does not have a means to get that information (yet).

Migrations from Folder

As normally multiple migrations are to be executed, they can be assembled in a folder and then executed together. As stated before, the order of multiple files is determined by their alphabetically ordered name.

Taken the example from above, split into two files, like prepared in testData/.

package testprog

import (
    "database/sql"
    _ "embed"
    "io/fs"
    "github.com/AlphaOne1/dmorph"
)

//go:embed testData
var migrationFS embed.FS

func migrate(db *sql.DB) error {
    sub, subErr := fs.Sub(migrationFS, "testData")

    if subErr != nil {
        return subErr
    }

    return dmorph.Run(db,
        dmorph.WithDialect(dmorph.DialectSQLite()),
        dmorph.WithMigrationsFromFS(sub.(fs.ReadDirFS)))
}

...
Programmatic Migration

Sometimes SQL alone is not sufficient to achieve the migration desired. Maybe the data needs to be programmatically changed, checked or otherwise processed. For DMorph a migration is presented as an interface:

type Migration interface {
    Key() string              // identifier, used for ordering
    Migrate(tx *sql.Tx) error // migration functionality
}

The WithMigrationFromF... family of options constructs these migrations for convenience. An example migration fulfilling this interface could look like this:

type CustomMigration struct {}

func (m CustomMigration) Key() string {
    return "0001_custom"
}

func (m CustomMigration) Migrate(tx *sql.Tx) error {
    _, err := tx.Exec(`CREATE TABLE tab0(id INTEGER PRIMARY KEY)`)
    return err
}

Inside the Migrate function the transaction state should not be modified. Commit and Rollback are handled by DMorph as needed. As seen in the example, a potentiel error is returned plain to the caller.

This newly created migration can then be passed to DMorph as follows:

func migrate(db *sql.DB) error {
    return dmorph.Run(db,
        dmorph.WithDialect(dmorph.DialectSQLite()),
        dmorph.WithMigrations(CustomMigration{}))
}
New SQL Dialect

DMorph uses the Dialect interface to adapt to different database management systems:

type Dialect interface {
    EnsureMigrationTableExists(db *sql.DB, tableName string) error
    AppliedMigrations(db *sql.DB, tableName string, groupName string) ([]string, error)
    RegisterMigration(tx *sql.Tx, id string, tableName string, groupName string) error
}

It contains a convenience wrapper, NamedParamsDialect, that fits most database systems. It implements the above functions using a set of user supplied SQL statements:

type NamedParamsDialect struct {
    CreateTemplate   string // statement ensuring the existence of the migration table
    AppliedTemplate  string // statement getting applied migrations ordered by application date
    RegisterTemplate string // statement registering a migration
}

All the included SQL dialects, less MySQL/MariaDB, use the NamedParamsDialect to implement their functionality. The tests for DMorph are done using the SQLite dialect. MySQL does not support named parameters, so it uses the NumberedParamsDialect.

As the migration table name can be user supplied, the statements need to have placeholders that will fill the final table name. As there might be special characters, it is always enclosed in the identifier enclosing characters of the database.

DMorph uses the ValidTableNameRex regular expression, to check if a table name is principally valid. The regular expression may be adapted, but it is strongly advised to only do so in pressing circumstances.

Documentation

Overview

Package dmorph provides a simple database migration framework.

Index

Constants

View Source
const (
	// MigrationTableName is the default name for the migration management table in the database.
	MigrationTableName = "migrations"

	// MigrationGroupName is the default name for the migration group.
	MigrationGroupName = "default"
)

Variables

View Source
var (
	// ValidTableNameRex is the regular expression used to check if a given migration table name is valid.
	ValidTableNameRex = regexp.MustCompile("^[a-zA-Z0-9_]+$")

	// ErrMigrationKeyFormat is returned when a migration key does not match the expected format.
	ErrMigrationKeyFormat = errors.New("migration key format invalid")

	// ErrMigrationsUnrelated signals that the set of migrations to apply and the already applied set do not have the
	// same (order of) applied migrations. Applying unrelated migrations could severely harm the database.
	ErrMigrationsUnrelated = errors.New("migrations unrelated")

	// ErrMigrationsUnsorted indicates that the already applied migrations were not registered in the order
	// (using the timestamp) that they should have been registered (using their id).
	ErrMigrationsUnsorted = errors.New("migrations unsorted")

	// ErrNoDialect signals that no dialect for the database operations was chosen.
	ErrNoDialect = errors.New("no dialect")

	// ErrNoMigrations signals that no migrations were chosen to be applied.
	ErrNoMigrations = errors.New("no migrations")

	// ErrNoMigrationTable occurs if there is no migration table present.
	ErrNoMigrationTable = errors.New("no migration table")

	// ErrNoMigrationGroup occurs if there is no migration group present.
	ErrNoMigrationGroup = errors.New("no migration group")

	// ErrMigrationTableNameInvalid occurs if the migration table does not adhere to ValidTableNameRex.
	ErrMigrationTableNameInvalid = errors.New("invalid migration table name")

	// ErrMigrationGroupNameInvalid occurs if the migration group name is empty.
	ErrMigrationGroupNameInvalid = errors.New("invalid migration group name")

	// ErrParamNameInvalid occurs if the param name is invalid.
	ErrParamNameInvalid = errors.New("invalid param name")

	// ErrMigrationsTooOld signals that the migrations to be applied are older than the migrations that are already
	// present in the database. This error can occur when an older version of the application is started using a database
	// used already by a newer version of the application.
	ErrMigrationsTooOld = errors.New("migrations too old")
)

Functions

func Run

func Run(ctx context.Context, db *sql.DB, options ...MorphOption) error

Run is a convenience function to easily get the migration job done. For more control use the Morpher directly.

func WithGroupName

func WithGroupName(groupName string) func(*Morpher) error

WithGroupName sets the migration group name on the provided Morpher instance. If not supplied, the default MigrationGroupName is used instead.

func WithMigrationKeyProperties

func WithMigrationKeyProperties(keyProp MigrationKeyProperties) func(*Morpher) error

WithMigrationKeyProperties sets the migration key comparison properties for a Morpher instance.

func WithTableName

func WithTableName(tableName string) func(*Morpher) error

WithTableName sets the migration table name to the given one. If not supplied, the default MigrationTableName is used instead.

Types

type Dialect

type Dialect interface {
	EnsureMigrationTableExists(ctx context.Context, db *sql.DB, tableName string) error
	AppliedMigrations(ctx context.Context, db *sql.DB, tableName string, groupName string) ([]string, error)
	RegisterMigration(ctx context.Context, tx *sql.Tx, id string, tableName string, groupName string) error
}

Dialect is an interface describing the functionalities needed to manage migrations inside a database.

type FileMigration

type FileMigration struct {
	Name string
	FS   fs.FS
	// contains filtered or unexported fields
}

FileMigration implements the Migration interface. It helps to apply migrations from a file or fs.FS.

func (FileMigration) Key

func (f FileMigration) Key() string

Key returns the key of the migration to register in the migration table.

func (FileMigration) Migrate

func (f FileMigration) Migrate(ctx context.Context, tx *sql.Tx) error

Migrate executes the migration on the given transaction.

type Migration

type Migration interface {
	Key() string                                   // identifier, used for ordering
	Migrate(ctx context.Context, tx *sql.Tx) error // migration functionality
}

Migration is an interface to provide abstract information about the migration at hand.

type MigrationKeyProperties

type MigrationKeyProperties struct {
	MigrationOrder    func(m, n Migration) int
	MigrationKeyOrder func(m, n string) int
	MigrationKeyValid func(m string) bool
}

MigrationKeyProperties defines the properties and functions for managing migration key behaviors.

func MigrationKeyAlphabetical

func MigrationKeyAlphabetical() MigrationKeyProperties

MigrationKeyAlphabetical returns MigrationKeyProperties configured for alphabetical sorting of migration keys.

func MigrationKeySemVerPrefix

func MigrationKeySemVerPrefix() MigrationKeyProperties

MigrationKeySemVerPrefix returns a MigrationKeyProperties object configured for semantic version prefix-based operations.

type MorphOption

type MorphOption func(*Morpher) error

MorphOption is the type used for functional options.

func WithDialect

func WithDialect(dialect Dialect) MorphOption

WithDialect sets the vendor-specific database dialect to be used.

func WithLog

func WithLog(log *slog.Logger) MorphOption

WithLog sets the logger that is to be used. If none is supplied, the default logger is used instead.

func WithMigrationFromFile

func WithMigrationFromFile(name string) MorphOption

WithMigrationFromFile generates a FileMigration that will run the content of the given file.

func WithMigrationFromFileFS

func WithMigrationFromFileFS(name string, dir fs.FS) MorphOption

WithMigrationFromFileFS generates a FileMigration that will run the content of the given file from the given filesystem.

func WithMigrations

func WithMigrations(migrations ...Migration) MorphOption

WithMigrations adds the given migrations to be executed.

func WithMigrationsFromFS

func WithMigrationsFromFS(d fs.FS) MorphOption

WithMigrationsFromFS generates a FileMigration that will run all migration scripts of the files in the given filesystem.

type Morpher

type Morpher struct {
	Dialect    Dialect                // database vendor specific dialect
	Migrations []Migration            // migrations to be applied
	TableName  string                 // table name for migration management
	GroupName  string                 // name of the migration group
	KeyProp    MigrationKeyProperties // migration comparison mode
	Log        *slog.Logger           // logger to be used
}

Morpher contains all the required information to run a given set of database migrations on a database.

func NewMorpher

func NewMorpher(options ...MorphOption) (*Morpher, error)

NewMorpher creates a new Morpher configuring it with the given options. It ensures that the newly created Morpher has migrations and a database dialect configured. If no migration table name is given, the default MigrationTableName is used instead.

func (*Morpher) IsValid

func (m *Morpher) IsValid() error

IsValid checks if the Morpher contains all the required information to run.

func (*Morpher) Run

func (m *Morpher) Run(ctx context.Context, db *sql.DB) error

Run runs the configured Morpher on the given database. If the migrations already applied to the database are a superset of the migrations the Morpher would apply, ErrMigrationsTooOld is returned. Run will run each migration in a separate transaction, with the last step to register the migration in the migration table.

type NamedParamsDialect

type NamedParamsDialect struct {
	CreateTemplate   string // statement ensuring the existence of the migration table
	AppliedTemplate  string // statement getting applied migrations ordered by application date
	RegisterTemplate string // statement registering a migration
}

NamedParamsDialect is a convenience type for databases that manage the necessary operations solely using queries. Defining the CreateTemplate, AppliedTemplate and RegisterTemplate enables the NamedParamsDialect to perform all the necessary operations to fulfill the Dialect interface.

func DialectCSVQ

func DialectCSVQ() NamedParamsDialect

DialectCSVQ returns a Dialect configured for CSVQ databases.

func DialectDB2

func DialectDB2() NamedParamsDialect

DialectDB2 returns a Dialect configured for DB2 databases.

func DialectMSSQL

func DialectMSSQL() NamedParamsDialect

DialectMSSQL returns a Dialect configured for Microsoft SQL Server databases.

func DialectOracle

func DialectOracle() NamedParamsDialect

DialectOracle returns a Dialect configured for Oracle Database.

func DialectPostgres

func DialectPostgres() NamedParamsDialect

DialectPostgres returns a Dialect configured for Postgres databases.

func DialectSQLite

func DialectSQLite() NamedParamsDialect

DialectSQLite returns a Dialect configured for SQLite databases.

func (NamedParamsDialect) AppliedMigrations

func (b NamedParamsDialect) AppliedMigrations(
	ctx context.Context,
	db *sql.DB,
	tableName string,
	groupName string) ([]string, error)

AppliedMigrations gets the already applied migrations from the database, ordered by application date.

func (NamedParamsDialect) EnsureMigrationTableExists

func (b NamedParamsDialect) EnsureMigrationTableExists(ctx context.Context, db *sql.DB, tableName string) error

EnsureMigrationTableExists ensures that the migration table, saving the applied migrations ids, exists.

func (NamedParamsDialect) RegisterMigration

func (b NamedParamsDialect) RegisterMigration(
	ctx context.Context,
	tx *sql.Tx,
	id string,
	tableName string,
	groupName string) error

RegisterMigration registers a migration in the migration table.

type NumberedParamsDialect

type NumberedParamsDialect struct {
	NamedParamsDialect

	AppliedMigrationsParamsOrder []ParamName // defines the order of parameters for retrieving applied migrations.
	RegisterMigrationParamsOrder []ParamName // defines the order of parameters for registering a migration.
}

NumberedParamsDialect extends NamedParamsDialect to support positional parameterized SQL queries.

func DialectMySQL

func DialectMySQL() NumberedParamsDialect

DialectMySQL returns a Dialect configured for MySQL databases.

func DialectSQLiteNumbered

func DialectSQLiteNumbered() NumberedParamsDialect

DialectSQLiteNumbered returns a Dialect configured for SQLite databases with numbered parameters. This is mainly used for tests, but nothing speaks against it being used otherwise.

func (NumberedParamsDialect) AppliedMigrations

func (b NumberedParamsDialect) AppliedMigrations(
	ctx context.Context,
	db *sql.DB,
	tableName string,
	groupName string) ([]string, error)

AppliedMigrations gets the already applied migrations from the database, ordered by application date.

func (NumberedParamsDialect) EnsureMigrationTableExists

func (b NumberedParamsDialect) EnsureMigrationTableExists(ctx context.Context, db *sql.DB, tableName string) error

EnsureMigrationTableExists ensures that the migration table, saving the applied migrations ids, exists.

func (NumberedParamsDialect) RegisterMigration

func (b NumberedParamsDialect) RegisterMigration(
	ctx context.Context,
	tx *sql.Tx,
	id string,
	tableName string,
	groupName string) error

RegisterMigration registers a migration in the migration table.

type ParamName

type ParamName string

ParamName represents a named parameter for use in SQL queries or migrations.

const (

	// ParamNameID represents the "id" parameter used in SQL queries or migration operations.
	ParamNameID ParamName = "id"

	// ParamNameMGroup represents the "mgroup" parameter used in SQL queries or migration operations.
	ParamNameMGroup ParamName = "mgroup"
)

Jump to

Keyboard shortcuts

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