migrator

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Feb 8, 2026 License: MIT Imports: 13 Imported by: 0

README

migrator

Package migrator handles loading authorization schemas into PostgreSQL.

This package provides the primary API for applying OpenFGA schemas to your database, generating specialized SQL functions for permission checking. Migrations are idempotent and safe to run on every application startup.

Package Responsibilities

  • Parse and validate OpenFGA schemas
  • Generate specialized SQL functions per relation
  • Apply migrations atomically within transactions
  • Track migration history for skip-if-unchanged optimization
  • Clean up orphaned functions when schema changes

Public API

High-Level Functions
// Migrate parses schema file and applies to database in one operation.
// Recommended for most applications.
func Migrate(ctx context.Context, db Execer, schemaPath string) error

// MigrateFromString parses schema content and applies to database.
// Useful for embedded schemas or testing.
func MigrateFromString(ctx context.Context, db Execer, content string) error

// MigrateWithOptions provides control over dry-run and skip behavior.
func MigrateWithOptions(ctx context.Context, db Execer, schemaPath string, opts MigrateOptions) (skipped bool, err error)
Migrator Type
// NewMigrator creates a new schema migrator.
func NewMigrator(db Execer, schemaPath string) *Migrator

// MigrateWithTypes performs migration using pre-parsed type definitions.
func (m *Migrator) MigrateWithTypes(ctx context.Context, types []TypeDefinition) error

// MigrateWithTypesAndOptions performs migration with full options control.
func (m *Migrator) MigrateWithTypesAndOptions(ctx context.Context, types []TypeDefinition, opts InternalMigrateOptions) error

// GetStatus returns the current migration status.
func (m *Migrator) GetStatus(ctx context.Context) (*Status, error)

// GetLastMigration returns the most recent migration record.
func (m *Migrator) GetLastMigration(ctx context.Context) (*MigrationRecord, error)

// HasSchema returns true if the schema file exists.
func (m *Migrator) HasSchema() bool
Types
// MigrateOptions controls migration behavior.
type MigrateOptions struct {
    DryRun  io.Writer // Output SQL without applying; nil = apply normally
    Force   bool      // Re-run even if schema unchanged
    Version string    // Melange version for traceability
}

// Status represents the current migration state.
type Status struct {
    SchemaExists bool // Schema file exists on disk
    TuplesExists bool // melange_tuples view exists in database
}

// MigrationRecord represents a row in melange_migrations table.
type MigrationRecord struct {
    MelangeVersion string
    SchemaChecksum string
    CodegenVersion string
    FunctionNames  []string
}

Usage Examples

import "github.com/pthm/melange/pkg/migrator"

// Run on application startup
func main() {
    db, _ := sql.Open("postgres", connString)

    if err := migrator.Migrate(ctx, db, "schemas/schema.fga"); err != nil {
        log.Fatalf("Migration failed: %v", err)
    }

    // Application ready...
}
Embedded Schema
import (
    _ "embed"
    "github.com/pthm/melange/pkg/migrator"
)

//go:embed schema.fga
var embeddedSchema string

func main() {
    db, _ := sql.Open("postgres", connString)

    if err := migrator.MigrateFromString(ctx, db, embeddedSchema); err != nil {
        log.Fatalf("Migration failed: %v", err)
    }
}
Dry-Run (Preview SQL)
var buf bytes.Buffer

_, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", migrator.MigrateOptions{
    DryRun: &buf,
})
if err != nil {
    log.Fatal(err)
}

// Write SQL to file for review
os.WriteFile("migrations/001_authz.sql", buf.Bytes(), 0644)
Force Re-Migration
// Useful after manual schema corruption or template changes
skipped, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", migrator.MigrateOptions{
    Force: true,
})
if err != nil {
    log.Fatal(err)
}
// skipped is always false when Force=true
Check Migration Status
m := migrator.NewMigrator(db, "schemas/schema.fga")

status, err := m.GetStatus(ctx)
if err != nil {
    log.Fatal(err)
}

if !status.SchemaExists {
    log.Println("No schema file found")
}

if !status.TuplesExists {
    log.Println("melange_tuples view needs to be created")
}
With Pre-Parsed Types
import (
    "github.com/pthm/melange/pkg/parser"
    "github.com/pthm/melange/pkg/migrator"
)

// Parse once, migrate multiple databases
types, err := parser.ParseSchema("schemas/schema.fga")
if err != nil {
    log.Fatal(err)
}

for _, db := range databases {
    m := migrator.NewMigrator(db, "schemas/schema.fga")
    if err := m.MigrateWithTypes(ctx, types); err != nil {
        log.Printf("Migration failed for %s: %v", db, err)
    }
}

Migration Workflow

When you run migration, the following steps occur:

  1. Parse - Read and parse the OpenFGA schema
  2. Validate - Check for cycles and invalid references
  3. Compute - Generate transitive closure for role hierarchies
  4. Analyze - Classify relations and determine SQL patterns
  5. Generate - Create specialized SQL functions per relation
  6. Apply - Execute SQL atomically in a transaction
  7. Track - Record migration in melange_migrations table
  8. Cleanup - Drop orphaned functions from previous schema

Skip-If-Unchanged Optimization

Migrations are skipped when both conditions are met:

  • Schema content hash matches last migration
  • Codegen version matches last migration

This avoids redundant function regeneration on every restart. Use Force: true to bypass.

Transaction Support

When using *sql.DB, migrations are applied atomically:

// All-or-nothing: either all functions are created or none
err := migrator.Migrate(ctx, db, "schema.fga")

For *sql.Tx or *sql.Conn, functions are applied individually (caller manages transaction).

Error Handling

err := migrator.Migrate(ctx, db, "schema.fga")
if err != nil {
    // Check specific error types
    if schema.IsCyclicSchemaErr(err) {
        log.Fatal("Schema has cyclic relations")
    }
    if errors.Is(err, melange.ErrInvalidSchema) {
        log.Fatal("Schema syntax error")
    }
    log.Fatalf("Migration failed: %v", err)
}

Database Requirements

The migrator requires:

  1. PostgreSQL - Generates PL/pgSQL functions
  2. Schema permissions - Ability to CREATE/REPLACE FUNCTION
  3. melange_tuples view - User-defined view over domain tables (checked by GetStatus)

Dependency Information

This package imports:

  • github.com/lib/pq - PostgreSQL driver for array handling
  • github.com/pthm/melange/pkg/parser - Schema parsing
  • github.com/pthm/melange/pkg/schema - Schema types
  • github.com/pthm/melange/lib/sqlgen - SQL generation

Execer Interface

The migrator accepts any type implementing Execer:

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

Compatible types: *sql.DB, *sql.Tx, *sql.Conn

Documentation

Index

Constants

View Source
const CodegenVersion = "1"

CodegenVersion is incremented when SQL generation templates or logic change. This ensures migrations re-run even if schema checksum matches. Bump this when:

  • SQL templates in tooling/schema/templates/ change
  • Codegen logic in schema/codegen.go or schema/codegen_list.go changes
  • New function patterns are added

Variables

View Source
var (
	DetectCycles           = schema.DetectCycles
	ComputeRelationClosure = schema.ComputeRelationClosure
	AnalyzeRelations       = sqlgen.AnalyzeRelations
	ComputeCanGenerate     = sqlgen.ComputeCanGenerate

	GenerateSQL          = sqlgen.GenerateSQL
	GenerateListSQL      = sqlgen.GenerateListSQL
	CollectFunctionNames = sqlgen.CollectFunctionNames
)

Function aliases from schema and sqlgen packages.

Functions

func ComputeSchemaChecksum

func ComputeSchemaChecksum(content string) string

ComputeSchemaChecksum returns a SHA256 hash of the schema content. Used to detect schema changes for skip-if-unchanged optimization.

func Migrate

func Migrate(ctx context.Context, db Execer, schemaPath string) error

Migrate parses an OpenFGA schema file and applies it to the database in one operation. This is the recommended high-level API for most applications.

The function is idempotent - safe to call on every application startup. It validates the schema, generates specialized SQL functions per relation, and applies everything atomically within a transaction (when db supports BeginTx).

Migration workflow:

  1. Reads the schema file at schemaPath
  2. Parses OpenFGA DSL using the official parser
  3. Validates schema (cycle detection, referential integrity)
  4. Generates specialized check_permission and list_accessible functions
  5. Applies generated SQL atomically via transaction

Example usage on application startup:

if err := migrator.Migrate(ctx, db, "schemas/schema.fga"); err != nil {
    log.Fatalf("migration failed: %v", err)
}

For embedded schemas (no file I/O), use MigrateFromString. For fine-grained control (dry-run, skip optimization), use MigrateWithOptions. For programmatic use with pre-parsed types, use Migrator directly:

types, _ := parser.ParseSchema("schemas/schema.fga")
m := migrator.NewMigrator(db, "schemas/schema.fga")
err := m.MigrateWithTypes(ctx, types)

func MigrateFromString

func MigrateFromString(ctx context.Context, db Execer, content string) error

MigrateFromString parses schema content and applies it to the database. Useful for testing or when schema is embedded in the application binary.

This allows bundling the authorization schema with the application rather than reading from disk, which simplifies deployment and versioning.

Example:

//go:embed schema.fga
var embeddedSchema string

err := migrator.MigrateFromString(ctx, db, embeddedSchema)

The migration is idempotent and transactional (when using *sql.DB).

func MigrateWithOptions

func MigrateWithOptions(ctx context.Context, db Execer, schemaPath string, opts MigrateOptions) (skipped bool, err error)

MigrateWithOptions performs migration with control over dry-run and skip behavior. Use this when you need to preview migrations, force re-application, or detect skips.

The skip-if-unchanged optimization compares the schema content hash and codegen version against the last successful migration. If both match and Force is false, the migration is skipped (skipped=true). This avoids redundant function regeneration on every application restart when schemas are stable.

Returns (skipped, error):

  • skipped=true if migration was skipped due to unchanged schema (only when Force=false and DryRun=nil)
  • error is non-nil if migration failed (parse error, validation error, DB error)

Example: Generate migration script without applying

var buf bytes.Buffer
_, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", migrator.MigrateOptions{
    DryRun: &buf,
})
os.WriteFile("migrations/001_authz.sql", buf.Bytes(), 0644)

Example: Force re-migration (e.g., after manual schema corruption)

skipped, err := migrator.MigrateWithOptions(ctx, db, "schemas/schema.fga", migrator.MigrateOptions{
    Force: true,
})

Types

type Execer

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

Execer is the minimal interface needed for schema migration operations. Implemented by *sql.DB, *sql.Tx, and *sql.Conn.

type GeneratedSQL

type GeneratedSQL = sqlgen.GeneratedSQL

Type aliases for cleaner code.

type InternalMigrateOptions

type InternalMigrateOptions struct {
	DryRun io.Writer
	Force  bool

	// Version is the melange CLI/library version (e.g., "v0.4.3").
	// Recorded in melange_migrations for traceability.
	Version string

	// SchemaContent is the raw schema text used for checksum calculation to detect schema changes.
	// If empty, skip-if-unchanged optimization is disabled.
	SchemaContent string
}

InternalMigrateOptions extends MigrateOptions with internal fields.

type ListGeneratedSQL

type ListGeneratedSQL = sqlgen.ListGeneratedSQL

Type aliases for cleaner code.

type MigrateOptions

type MigrateOptions struct {
	// DryRun outputs SQL to the provided writer without applying changes to the database.
	// If nil, migration proceeds normally. Use for previewing migrations or generating migration scripts.
	DryRun io.Writer

	// Force re-runs migration even if schema/codegen unchanged. Use when manually fixing corrupted state or testing.
	Force bool

	// Version is the melange CLI/library version (e.g., "v0.4.3").
	// Recorded in melange_migrations for traceability.
	Version string
}

MigrateOptions controls migration behavior (public API).

type MigrationRecord

type MigrationRecord struct {
	MelangeVersion string
	SchemaChecksum string
	CodegenVersion string
	FunctionNames  []string
}

MigrationRecord represents a row in the melange_migrations table.

type Migrator

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

Migrator handles loading authorization schemas into PostgreSQL. The migrator is idempotent - safe to run on every application startup.

The migration process:

  1. Creates/replaces check_permission and list_accessible_* functions
  2. Loads generated SQL entrypoints into the database

Usage

Use the convenience functions in pkg/migrator for most use cases:

import "github.com/pthm/melange/pkg/migrator"
err := migrator.Migrate(ctx, db, "schemas/schema.fga")

For embedded schemas (no file I/O):

err := migrator.MigrateFromString(ctx, db, schemaContent)

Use the Migrator directly when you have pre-parsed TypeDefinitions or need fine-grained control (DDL-only, status checks, etc.):

types, _ := parser.ParseSchema("schemas/schema.fga")
m := migrator.NewMigrator(db, "schemas/schema.fga")
err := m.MigrateWithTypes(ctx, types)

func NewMigrator

func NewMigrator(db Execer, schemaPath string) *Migrator

NewMigrator creates a new schema migrator. The schemaPath should point to an OpenFGA DSL schema file (e.g., "schemas/schema.fga"). The Execer is typically *sql.DB but can be *sql.Tx for testing.

func (*Migrator) ApplyDDL

func (m *Migrator) ApplyDDL(ctx context.Context) error

ApplyDDL applies any base schema required by Melange. With fully generated SQL entrypoints, no base DDL is required.

func (*Migrator) GetLastMigration

func (m *Migrator) GetLastMigration(ctx context.Context) (*MigrationRecord, error)

GetLastMigration returns the most recent migration record, or nil if none exists. This can be used to check if migration is needed before calling MigrateWithTypesAndOptions.

func (*Migrator) GetStatus

func (m *Migrator) GetStatus(ctx context.Context) (*Status, error)

GetStatus returns the current migration status. Useful for health checks or migration diagnostics.

func (*Migrator) HasSchema

func (m *Migrator) HasSchema() bool

HasSchema returns true if the schema file exists. Use this to conditionally run migration or skip if not configured.

func (*Migrator) MigrateWithTypes

func (m *Migrator) MigrateWithTypes(ctx context.Context, types []TypeDefinition) error

MigrateWithTypes performs database migration using pre-parsed type definitions. This is the core migration method used by the tooling package's Migrate function.

The method:

  1. Validates the schema (checks for cycles)
  2. Computes derived data (closure)
  3. Analyzes relations and generates specialized SQL functions
  4. Applies everything atomically in a transaction: - Generated specialized functions and dispatcher

This is idempotent - safe to run multiple times with the same types.

Uses a transaction if the db supports it (*sql.DB). This ensures the schema is updated atomically or not at all.

func (*Migrator) MigrateWithTypesAndOptions

func (m *Migrator) MigrateWithTypesAndOptions(ctx context.Context, types []TypeDefinition, opts InternalMigrateOptions) error

MigrateWithTypesAndOptions performs database migration with options. This is the full-featured migration method that supports dry-run, skip-if-unchanged, and orphan cleanup.

See MigrateWithTypes for basic usage without options.

func (*Migrator) SchemaPath

func (m *Migrator) SchemaPath() string

SchemaPath returns the path to the schema file.

type Status

type Status struct {
	// SchemaExists indicates if the schema.fga file exists on disk.
	SchemaExists bool

	// TuplesExists indicates if the melange_tuples relation exists (view, table, or materialized view).
	// This must be created by the user to map their domain tables.
	TuplesExists bool
}

Status represents the current migration state. Use GetStatus to check if the authorization system is properly configured.

type TypeDefinition

type TypeDefinition = schema.TypeDefinition

Type aliases for cleaner code.

Jump to

Keyboard shortcuts

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