goway

package module
v0.0.0-...-626204f Latest Latest
Warning

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

Go to latest
Published: May 31, 2026 License: MIT Imports: 13 Imported by: 0

README

goway

Go Reference Go Report Card CI Go Version

A version-based database schema migration library for Go, inspired by the behavior and configuration model of Flyway. It discovers versioned and repeatable SQL migration scripts, records what has been applied in a schema history table, and brings a database up to date by running the pending scripts in order, each inside its own transaction.

Alpha. The API may still change and there are no tagged releases yet. Pin a specific commit when depending on this module.

Why

Go has excellent low level database tooling, but migrating a schema reliably and reproducibly across environments is usually delegated to an external binary or a heavyweight framework. goway brings the well understood migration model — immutable, ordered, checksummed migrations recorded in a history table — to Go as a small, embeddable package that uses the connection pool the application already owns.

  • Zero dependency core. The library depends only on the standard library. Database access goes through database/sql, so the application supplies its own driver and *sql.DB.
  • Two databases, no C. PostgreSQL through github.com/jackc/pgx/v5 and SQLite through the pure Go driver modernc.org/sqlite, which needs no cgo.
  • Drop-in friendly. Migration naming, the CRC32 checksum algorithm, version comparison, the schema history table (named flyway_schema_history by default), and SQL statement splitting follow Flyway's documented semantics, so an existing migration set and an existing history table behave as expected.

Install

go get github.com/cgardev/goway

Quick start

Migrations can be read from the file system or embedded into the binary with go:embed.

package main

import (
	"context"
	"database/sql"
	"embed"
	"log"

	"github.com/cgardev/goway"

	_ "github.com/jackc/pgx/v5/stdlib"
)

//go:embed db/migration/*.sql
var migrations embed.FS

func main() {
	database, err := sql.Open("pgx", "postgres://user:password@localhost:5432/app")
	if err != nil {
		log.Fatal(err)
	}
	defer database.Close()

	migrator, err := goway.Configure().
		DataSource(database).
		FS(migrations, "db/migration").
		Schemas("public").
		CreateSchemas(true).
		Load()
	if err != nil {
		log.Fatal(err)
	}

	result, err := migrator.Migrate(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("applied %d migration(s); now at version %s",
		result.MigrationsExecuted, result.TargetSchemaVersion)
}

The dialect is detected automatically from the connection; call Dialect to set it explicitly and skip detection.

Migration scripts

Scripts follow the same naming convention as Flyway:

Kind Pattern Example
Versioned V<version>__<description>.sql V1__create_users.sql
Versioned dotted versions allowed V2.1__add_index.sql
Repeatable R__<description>.sql R__active_users.sql

Versioned migrations run once, in ascending version order. Repeatable migrations run after all versioned ones and are re-applied whenever their checksum changes. A migration may contain several statements; they are split on the semicolon delimiter, taking into account quoted strings, comments, PostgreSQL dollar quoted bodies, and SQLite trigger blocks.

Commands

Every command is a method on the loaded *goway.Flyway value and takes a context.Context.

Command Description
Migrate Applies every pending migration in order.
Info Reports the state of every migration without changing the database.
Validate Checks applied migrations against the resolved scripts.
Baseline Records a baseline so an existing database can be brought under control.
Repair Removes failed entries and realigns recorded checksums.
Clean Drops every object in the managed schemas. Disabled by default.
Callbacks

Register lifecycle callbacks either as SQL scripts placed in the configured locations (beforeMigrate.sql, afterMigrate.sql, beforeEachMigrate.sql, afterEachMigrate__description.sql) or programmatically through Configure().Callbacks(...) with a value implementing Callback, or the CallbackFunc adapter.

Non-transactional migrations

A migration that cannot run inside a transaction, such as one using PostgreSQL's CREATE INDEX CONCURRENTLY or SQLite's VACUUM, opts out of the per-migration transaction with a directive on the first lines of the script:

-- goway:noTransaction
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);

The statements then run directly on a dedicated connection and the history row is still recorded.

Command line tool

A command line front end lives in the cmd/goway module.

go run github.com/cgardev/goway/cmd/goway \
  -url 'postgres://user:password@localhost:5432/app' \
  -locations filesystem:db/migration \
  migrate

It selects the driver from the URL scheme: postgres:// (or postgresql://) uses pgx, and sqlite: (or file:) uses the pure Go SQLite driver. Run it with no command to see all flags.

Dialects

Capability PostgreSQL SQLite
Addressable schemas yes no
Transactional migrations yes yes
Dollar quoted bodies yes n/a
Trigger block splitting n/a yes
Advisory migration lock yes n/a

Only the two most recent major versions of each database are targeted.

Testing

The core library and its unit tests use only the standard library:

go test ./... -count=1

The integration module exercises real databases. The SQLite tests run anywhere through the pure Go driver; the PostgreSQL tests start a container with testcontainers and skip themselves when Docker is unavailable:

go -C integration test ./... -count=1

Status and roadmap

Implemented: versioned and repeatable SQL migrations, the schema history table, migrate, info, validate, baseline, repair and clean, placeholder replacement, multiple locations, embedded file systems, schema creation, lifecycle callbacks (SQL scripts and programmatic), per-script non-transactional execution, the superseded state for repeatable migrations, and a command line tool.

Not yet implemented: Go code based migrations, undo migrations, and the grouped and mixed transaction modes.

Acknowledgements and License

goway is an independent, clean-room implementation inspired by the behavior and public configuration model of Flyway. It is not affiliated with or endorsed by the Flyway project or Red Gate Software Ltd. No Flyway source code is used in this project; goway is built from scratch in Go and references only the publicly documented behavior and public API surface of Flyway.

Flyway is a trademark of Red Gate Software Ltd. All references to Flyway are for identification and comparison purposes only. For interoperability, goway uses the same default schema history table name (flyway_schema_history) and the same ${flyway:...} placeholder names; these are technical defaults and remain configurable.

goway is distributed under the MIT License (see LICENSE). Flyway Community Edition itself is licensed separately under the Apache License 2.0 by Red Gate Software Ltd; consult the Flyway project for its licensing terms.

goway is licensed under the MIT License. Copyright (c) 2026 Cristian Garcia.

Documentation

Overview

Package goway provides version-based database schema migrations for Go, reimplementing the core behavior of the Java tool Flyway. It discovers versioned and repeatable SQL migration scripts, records what has been applied in a schema history table, and brings a database up to date by executing the pending scripts in order, each inside its own transaction.

The package depends only on the standard library. Database access is performed through the database/sql package, so the calling application supplies its own driver and connection pool. Only PostgreSQL and SQLite are supported, the latter through the pure Go driver modernc.org/sqlite that avoids any binding to C.

The entry point is Configure, which returns a Configuration that is populated with a fluent builder and finalized with Load:

migrator, err := goway.Configure().
	DataSource(database).
	Locations("filesystem:db/migration").
	Schemas("public").
	Load()
if err != nil {
	return err
}
if _, err := migrator.Migrate(ctx); err != nil {
	return err
}

Migration scripts follow the same naming convention as Flyway. Versioned scripts are named V<version>__<description>.sql, for example V1__create_table.sql or V2.1__add_index.sql. Repeatable scripts are named R__<description>.sql and are re-executed whenever their checksum changes.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoDataSource indicates that Load was called without configuring a
	// database connection through DataSource.
	ErrNoDataSource = errors.New("goway: no data source configured")

	// ErrNoDialect indicates that the database dialect could not be detected
	// automatically and was not provided explicitly through Dialect.
	ErrNoDialect = errors.New("goway: could not determine database dialect")

	// ErrUnsupportedDialect indicates that the configured or detected database
	// is not one of the supported dialects (PostgreSQL and SQLite).
	ErrUnsupportedDialect = errors.New("goway: unsupported database dialect")

	// ErrValidationFailed indicates that validation found at least one problem,
	// such as a checksum mismatch or a locally missing applied migration.
	ErrValidationFailed = errors.New("goway: validation failed")

	// ErrCleanDisabled indicates that Clean was invoked while the clean command
	// is disabled, which is the default for safety.
	ErrCleanDisabled = errors.New("goway: clean is disabled")

	// ErrFailedMigration indicates that the schema history contains a migration
	// that previously failed and must be resolved before migrating further.
	ErrFailedMigration = errors.New("goway: detected a previously failed migration")

	// ErrDuplicateVersion indicates that more than one resolved migration shares
	// the same version.
	ErrDuplicateVersion = errors.New("goway: found more than one migration with the same version")

	// ErrDuplicateRepeatable indicates that more than one resolved repeatable
	// migration shares the same description.
	ErrDuplicateRepeatable = errors.New("goway: found more than one repeatable migration with the same description")

	// ErrInvalidMigrationName indicates that a script name does not satisfy the
	// configured naming convention.
	ErrInvalidMigrationName = errors.New("goway: invalid migration name")

	// ErrOutOfOrder indicates that a pending migration has a version lower than
	// an already applied one while out of order execution is disabled.
	ErrOutOfOrder = errors.New("goway: detected an out of order migration")
)

The following sentinel errors classify the failures that the package can report. They are returned wrapped with additional context, so callers should compare against them with errors.Is rather than by direct equality.

Functions

This section is empty.

Types

type BaselineResult

type BaselineResult struct {
	// BaselineVersion is the version recorded as the baseline.
	BaselineVersion string
	// BaselineDescription is the description recorded as the baseline.
	BaselineDescription string
	// Created reports whether a baseline entry was created by this command.
	Created bool
}

BaselineResult is the outcome of a baseline command.

type Callback

type Callback interface {
	Handle(ctx context.Context, event CallbackEvent, exec Execer, migration *MigrationInfo) error
}

Callback receives lifecycle events during a migrate run. For the per-migration events the migration argument describes the migration being processed; it is nil for the run level events.

type CallbackEvent

type CallbackEvent string

CallbackEvent identifies a point in the migrate lifecycle at which callbacks are invoked. The values match the corresponding Flyway event names.

const (
	// EventBeforeMigrate fires once before any migration is applied.
	EventBeforeMigrate CallbackEvent = "beforeMigrate"

	// EventAfterMigrate fires once after all migrations have been applied.
	EventAfterMigrate CallbackEvent = "afterMigrate"

	// EventBeforeEachMigrate fires before each individual migration, inside the
	// migration's transaction when one is used.
	EventBeforeEachMigrate CallbackEvent = "beforeEachMigrate"

	// EventAfterEachMigrate fires after each individual migration, inside the
	// migration's transaction when one is used.
	EventAfterEachMigrate CallbackEvent = "afterEachMigrate"
)

type CallbackFunc

type CallbackFunc func(ctx context.Context, event CallbackEvent, exec Execer, migration *MigrationInfo) error

CallbackFunc adapts an ordinary function to the Callback interface.

func (CallbackFunc) Handle

func (f CallbackFunc) Handle(ctx context.Context, event CallbackEvent, exec Execer, migration *MigrationInfo) error

Handle calls the underlying function.

type CleanResult

type CleanResult struct {
	// SchemasCleaned lists the schemas that were cleaned.
	SchemasCleaned []string
}

CleanResult is the outcome of a clean command.

type Configuration

type Configuration struct {
	// contains filtered or unexported fields
}

Configuration collects every setting that controls how migrations are discovered and applied. It is created with Configure, populated through the fluent setters, and finalized with Load. Each setter returns the same Configuration so calls can be chained.

func Configure

func Configure() *Configuration

Configure creates a Configuration populated with the same defaults as Flyway.

func (*Configuration) BaselineDescription

func (c *Configuration) BaselineDescription(description string) *Configuration

BaselineDescription sets the description recorded by the baseline command.

func (*Configuration) BaselineOnMigrate

func (c *Configuration) BaselineOnMigrate(enabled bool) *Configuration

BaselineOnMigrate controls whether a non empty schema without a history table is baselined automatically on the first migrate.

func (*Configuration) BaselineVersion

func (c *Configuration) BaselineVersion(version string) *Configuration

BaselineVersion sets the version recorded by the baseline command and below which migrations are ignored.

func (*Configuration) Callbacks

func (c *Configuration) Callbacks(callbacks ...Callback) *Configuration

Callbacks registers programmatic callbacks invoked during a migrate run, in addition to any SQL callback scripts found in the configured locations.

func (*Configuration) CleanDisabled

func (c *Configuration) CleanDisabled(disabled bool) *Configuration

CleanDisabled controls whether the clean command is permitted.

func (*Configuration) CreateSchemas

func (c *Configuration) CreateSchemas(create bool) *Configuration

CreateSchemas controls whether missing schemas are created automatically.

func (*Configuration) DataSource

func (c *Configuration) DataSource(db *sql.DB) *Configuration

DataSource sets the database connection pool used for every operation.

func (*Configuration) DefaultSchema

func (c *Configuration) DefaultSchema(schema string) *Configuration

DefaultSchema overrides the schema that holds the schema history table.

func (*Configuration) Dialect

func (c *Configuration) Dialect(dialect Dialect) *Configuration

Dialect sets the database dialect explicitly, bypassing automatic detection.

func (*Configuration) FS

func (c *Configuration) FS(fileSystem fs.FS, paths ...string) *Configuration

FS registers an additional file system, such as one produced by go:embed, together with the directory paths inside it that contain migrations.

func (*Configuration) InstalledBy

func (c *Configuration) InstalledBy(user string) *Configuration

InstalledBy overrides the user recorded for applied migrations.

func (*Configuration) Load

func (c *Configuration) Load() (*Migrator, error)

Load validates the configuration, determines the dialect when one was not set explicitly, resolves the migrations from disk, and returns a ready Migrator instance.

func (*Configuration) LoadContext

func (c *Configuration) LoadContext(ctx context.Context) (*Migrator, error)

LoadContext behaves like Load while honoring the supplied context during dialect detection.

func (*Configuration) Locations

func (c *Configuration) Locations(locations ...string) *Configuration

Locations replaces the list of file system locations scanned for migrations. A location may carry a "filesystem:" or "classpath:" prefix; a bare path is also accepted.

func (*Configuration) OutOfOrder

func (c *Configuration) OutOfOrder(enabled bool) *Configuration

OutOfOrder controls whether migrations with a version lower than the current one may still be applied.

func (*Configuration) PlaceholderPrefix

func (c *Configuration) PlaceholderPrefix(prefix string) *Configuration

PlaceholderPrefix sets the opening delimiter of a placeholder.

func (*Configuration) PlaceholderSuffix

func (c *Configuration) PlaceholderSuffix(suffix string) *Configuration

PlaceholderSuffix sets the closing delimiter of a placeholder.

func (*Configuration) Placeholders

func (c *Configuration) Placeholders(placeholders map[string]string) *Configuration

Placeholders sets the placeholder values substituted into scripts.

func (*Configuration) RepeatableSQLMigrationPrefix

func (c *Configuration) RepeatableSQLMigrationPrefix(prefix string) *Configuration

RepeatableSQLMigrationPrefix sets the prefix that marks repeatable scripts.

func (*Configuration) SQLMigrationPrefix

func (c *Configuration) SQLMigrationPrefix(prefix string) *Configuration

SQLMigrationPrefix sets the prefix that marks versioned migration scripts.

func (*Configuration) SQLMigrationSeparator

func (c *Configuration) SQLMigrationSeparator(separator string) *Configuration

SQLMigrationSeparator sets the token between the version and the description.

func (*Configuration) SQLMigrationSuffixes

func (c *Configuration) SQLMigrationSuffixes(suffixes ...string) *Configuration

SQLMigrationSuffixes sets the recognized script file suffixes.

func (*Configuration) Schemas

func (c *Configuration) Schemas(schemas ...string) *Configuration

Schemas sets the schemas managed by the migrator. The first schema is the default schema in which the schema history table is created.

func (*Configuration) Table

func (c *Configuration) Table(table string) *Configuration

Table sets the name of the schema history table.

func (*Configuration) Target

func (c *Configuration) Target(version string) *Configuration

Target sets the highest version that migrate will apply.

func (*Configuration) ValidateOnMigrate

func (c *Configuration) ValidateOnMigrate(enabled bool) *Configuration

ValidateOnMigrate controls whether migrate validates before applying.

type Dialect

type Dialect interface {
	// Name returns the canonical dialect name.
	Name() string
	// contains filtered or unexported methods
}

Dialect abstracts the differences between the supported databases. The interface is sealed: its methods are unexported, so callers obtain a dialect only through the Postgres and SQLite constructors. This mirrors the way the reference query builder isolates database specific rendering behind a single abstraction.

func Postgres

func Postgres() Dialect

Postgres returns the dialect for PostgreSQL.

func SQLite

func SQLite() Dialect

SQLite returns the dialect for SQLite.

type Execer

type Execer interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

Execer is the minimal interface needed to run a statement. It is satisfied by *sql.DB, *sql.Tx and *sql.Conn, so a callback can execute SQL on whichever handle is active for the current event.

type InfoResult

type InfoResult struct {
	// Migrations lists every migration with its derived state.
	Migrations []MigrationInfo
	// Current is the current schema version.
	Current string
}

InfoResult is the outcome of an info command.

type MigrateResult

type MigrateResult struct {
	// InitialSchemaVersion is the current version before migrating.
	InitialSchemaVersion string
	// TargetSchemaVersion is the current version after migrating.
	TargetSchemaVersion string
	// MigrationsExecuted is the number of migrations applied.
	MigrationsExecuted int
	// Migrations lists the migrations applied during this run.
	Migrations []MigrationInfo
}

MigrateResult summarizes the outcome of a migrate command.

type MigrationInfo

type MigrationInfo struct {
	// Version is the migration version, or an empty string for repeatable
	// migrations and the schema marker.
	Version string

	// Description is the human readable description.
	Description string

	// Type classifies the migration, for example "SQL" or "BASELINE".
	Type string

	// Script is the script file name or synthetic marker name.
	Script string

	// Checksum is the resolved checksum when available.
	Checksum *int32

	// State describes the relationship between the script and the history.
	State MigrationState

	// InstalledRank is the order in which the migration was applied, or zero when
	// it has not been applied.
	InstalledRank int

	// InstalledOn is the application timestamp, or nil when not applied.
	InstalledOn *time.Time

	// InstalledBy is the database user that applied the migration.
	InstalledBy string

	// ExecutionTime is the duration of the application in milliseconds.
	ExecutionTime int
}

MigrationInfo is the public, read only view of a single migration that combines what was resolved from disk with what was recorded in history.

type MigrationState

type MigrationState string

MigrationState describes the relationship between a resolved migration script and the recorded history for a single migration.

const (
	// StatePending indicates a resolved migration that has not been applied.
	StatePending MigrationState = "Pending"

	// StateSuccess indicates a migration that was applied successfully.
	StateSuccess MigrationState = "Success"

	// StateFailed indicates a migration whose application failed.
	StateFailed MigrationState = "Failed"

	// StateOutOfOrder indicates a migration that was applied out of order.
	StateOutOfOrder MigrationState = "Out of Order"

	// StateOutdated indicates a repeatable migration whose script changed since
	// it was last applied and will be re-applied.
	StateOutdated MigrationState = "Outdated"

	// StateSuperseded indicates an older applied run of a repeatable migration
	// that has since been re-applied; only the newest run is current.
	StateSuperseded MigrationState = "Superseded"

	// StateMissing indicates a successfully applied migration that can no longer
	// be resolved from the configured locations.
	StateMissing MigrationState = "Missing"

	// StateMissingFailed indicates a failed migration that can no longer be
	// resolved from the configured locations.
	StateMissingFailed MigrationState = "Failed (Missing)"

	// StateFuture indicates an applied migration with a version higher than any
	// resolved migration.
	StateFuture MigrationState = "Future"

	// StateFutureFailed indicates a failed applied migration with a version
	// higher than any resolved migration.
	StateFutureFailed MigrationState = "Failed (Future)"

	// StateAboveTarget indicates a pending migration above the configured target.
	StateAboveTarget MigrationState = "Above Target"

	// StateIgnored indicates a pending migration that will be skipped, for
	// example because it is below the baseline.
	StateIgnored MigrationState = "Ignored"

	// StateBaseline indicates the synthetic baseline entry.
	StateBaseline MigrationState = "Baseline"
)

type MigrationType

type MigrationType string

MigrationType classifies the kind of entry recorded in the schema history table. The string values match those written by Flyway so that a history table can be shared with the reference implementation.

const (
	// MigrationTypeSQL marks a migration backed by a SQL script.
	MigrationTypeSQL MigrationType = "SQL"

	// MigrationTypeBaseline marks the synthetic entry created by the baseline
	// command to establish a starting point.
	MigrationTypeBaseline MigrationType = "BASELINE"

	// MigrationTypeSchema marks the synthetic entry created when the migrator
	// creates one or more schemas on the caller's behalf.
	MigrationTypeSchema MigrationType = "SCHEMA"

	// MigrationTypeDeleted marks an entry that was removed from consideration by
	// the repair command.
	MigrationTypeDeleted MigrationType = "DELETED"
)

type Migrator

type Migrator struct {
	// contains filtered or unexported fields
}

Migrator is the entry point for running migration commands against a database. It is created by Load and is safe to reuse for multiple commands. Each command re-reads the schema history so that concurrent changes are observed.

func (*Migrator) Baseline

func (f *Migrator) Baseline(ctx context.Context) (*BaselineResult, error)

Baseline records a baseline entry in the schema history so that existing databases can be brought under management without re-running the migrations that produced their current state. Migrations at or below the baseline version are subsequently ignored.

func (*Migrator) Clean

func (f *Migrator) Clean(ctx context.Context) (*CleanResult, error)

Clean drops every object in the managed schemas, returning the database to an empty state. It is disabled by default and must be enabled explicitly through CleanDisabled, since it is destructive and irreversible.

func (*Migrator) Dialect

func (f *Migrator) Dialect() Dialect

Dialect returns the dialect that was configured or detected.

func (*Migrator) Info

func (f *Migrator) Info(ctx context.Context) (*InfoResult, error)

Info computes the state of every migration without changing the database.

func (*Migrator) Migrate

func (f *Migrator) Migrate(ctx context.Context) (*MigrateResult, error)

Migrate brings the database up to date by applying every pending migration in order. Missing schemas and the schema history table are created when needed. Each migration runs inside its own transaction, so a failed migration leaves no partial state and can be retried after the script is corrected.

func (*Migrator) Repair

func (f *Migrator) Repair(ctx context.Context) (*RepairResult, error)

Repair removes failed entries from the schema history and realigns the recorded checksums of applied migrations with the resolved scripts. It is the recommended remedy after a failed migration on a database without transactional data definition, or after a deliberate change to an already applied script.

func (*Migrator) Validate

func (f *Migrator) Validate(ctx context.Context) (*ValidateResult, error)

Validate compares the applied migrations against the resolved scripts and reports any discrepancy, such as a checksum mismatch, a locally missing migration, a failed migration, or a resolved migration that has not been applied. When validation fails, the returned error wraps ErrValidationFailed and the result still carries the full list of problems.

type RepairResult

type RepairResult struct {
	// RemovedFailed is the number of failed rows removed.
	RemovedFailed int
	// AlignedChecksums is the number of rows whose checksum was realigned.
	AlignedChecksums int
}

RepairResult is the outcome of a repair command.

type ValidateResult

type ValidateResult struct {
	// Valid reports whether validation found no problems.
	Valid bool
	// Errors lists the problems found.
	Errors []ValidationError
	// ValidationCount is the number of migrations validated.
	ValidationCount int
}

ValidateResult is the outcome of a validate command.

type ValidationError

type ValidationError struct {
	// Version is the migration version, empty for repeatable migrations.
	Version string
	// Description is the migration description.
	Description string
	// Script is the script file name or synthetic marker.
	Script string
	// Message explains the problem.
	Message string
}

ValidationError describes a single problem found during validation.

type Version

type Version struct {
	// contains filtered or unexported fields
}

Version represents a parsed, comparable migration version such as "1", "1.1" or "2.0.3". A version is a sequence of non negative integer parts that are compared element by element. A missing trailing element is treated as zero, so "1.0" and "1" are considered equal.

func (*Version) Compare

func (v *Version) Compare(other *Version) int

Compare returns a negative number, zero, or a positive number when the receiver is respectively lower than, equal to, or greater than the other version. Components beyond the length of either version are treated as zero.

func (*Version) Equal

func (v *Version) Equal(other *Version) bool

Equal reports whether two versions are numerically equal. Two nil receivers are considered equal, and a nil receiver never equals a non nil one.

func (*Version) String

func (v *Version) String() string

String returns the textual representation used for display and storage.

Jump to

Keyboard shortcuts

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