dmorph

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

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

Go to latest
Published: Dec 29, 2025 License: MPL-2.0 Imports: 15 Imported by: 0

README

Logo
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) ([]string, error)
    RegisterMigration(tx *sql.Tx, id string, tableName string) error
}

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

type BaseDialect 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 use the BaseDialect to implement their functionality. The tests for DMorph are done using the SQLite dialect.

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 = "migrations"

MigrationTableName is the default name for the migration management table in the database.

Variables

View Source
var ErrMigrationTableNameInvalid = errors.New("invalid migration table name")

ErrMigrationTableNameInvalid occurs if the migration table does not adhere to ValidTableNameRex.

View Source
var ErrMigrationsTooOld = errors.New("migrations too old")

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.

View Source
var ErrMigrationsUnrelated = errors.New("migrations unrelated")

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.

View Source
var ErrMigrationsUnsorted = errors.New("migrations unsorted")

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).

View Source
var ErrNoDialect = errors.New("no dialect")

ErrNoDialect signals that no dialect for the database operations was chosen.

View Source
var ErrNoMigrationTable = errors.New("no migration table")

ErrNoMigrationTable occurs if there is no migration table present.

View Source
var ErrNoMigrations = errors.New("no migrations")

ErrNoMigrations signals that no migrations were chosen to be applied.

View Source
var ValidTableNameRex = regexp.MustCompile("^[a-zA-Z0-9_]+$")

ValidTableNameRex is the regular expression used to check if a given migration table name is valid.

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 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 BaseDialect

type BaseDialect 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
}

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

func DialectCSVQ

func DialectCSVQ() BaseDialect

DialectCSVQ returns a Dialect configured for CSVQ databases.

func DialectDB2

func DialectDB2() BaseDialect

DialectDB2 returns a Dialect configured for DB2 databases.

func DialectMSSQL

func DialectMSSQL() BaseDialect

DialectMSSQL returns a Dialect configured for Microsoft SQL Server databases.

func DialectMySQL

func DialectMySQL() BaseDialect

DialectMySQL returns a Dialect configured for MySQL databases.

func DialectOracle

func DialectOracle() BaseDialect

DialectOracle returns a Dialect configured for Oracle Database.

func DialectPostgres

func DialectPostgres() BaseDialect

DialectPostgres returns a Dialect configured for Postgres databases.

func DialectSQLite

func DialectSQLite() BaseDialect

DialectSQLite returns a Dialect configured for SQLite databases.

func (BaseDialect) AppliedMigrations

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

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

func (BaseDialect) EnsureMigrationTableExists

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

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

func (BaseDialect) RegisterMigration

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

RegisterMigration registers a migration in the migration table.

type Dialect

type Dialect interface {
	EnsureMigrationTableExists(ctx context.Context, db *sql.DB, tableName string) error
	AppliedMigrations(ctx context.Context, db *sql.DB, tableName string) ([]string, error)
	RegisterMigration(ctx context.Context, tx *sql.Tx, id string, tableName 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 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
	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.

Jump to

Keyboard shortcuts

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