sqlite

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jan 10, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package sqlite provides a SQLite driver for Queen migrations.

This driver supports SQLite 3.8+ and is ideal for embedded databases, development, testing, and single-server applications.

Basic Usage

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
    "github.com/honeynil/queen"
    "github.com/honeynil/queen/drivers/sqlite"
)

db, _ := sql.Open("sqlite3", "myapp.db")
driver := sqlite.New(db)
q := queen.New(driver)

Database File

SQLite stores the database in a single file. Common patterns:

  • Persistent: "myapp.db" or "/path/to/database.db"
  • In-memory: ":memory:" (lost when connection closes)
  • Temporary: "" (empty string, deleted when closed)

For production use, always use a persistent file path.

Locking Mechanism

Unlike PostgreSQL and MySQL, SQLite is a file-based database with different locking characteristics:

  • SQLite uses database-level locks, not connection-level locks
  • This driver uses BEGIN EXCLUSIVE transaction for migration locking
  • The lock is automatically released when the transaction commits/rolls back
  • Only one writer can access the database at a time (by design)

For better concurrent read/write performance, enable WAL (Write-Ahead Logging):

db, _ := sql.Open("sqlite3", "myapp.db?_journal_mode=WAL")

WAL mode allows readers to access the database while a migration is running, though only one migration can run at a time.

Compatibility

  • SQLite 3.8+ (uses WITHOUT ROWID optimization where available)
  • Works on all platforms (Linux, macOS, Windows)
  • Single file, no server required
Example

Example demonstrates basic usage of the SQLite driver.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Connect to SQLite database file
	db, err := sql.Open("sqlite3", "myapp.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Create SQLite driver
	driver := sqlite.New(db)

	// Create Queen instance
	q := queen.New(driver)
	defer q.Close()

	// Register migrations
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users_table",
		UpSQL: `
			CREATE TABLE users (
				id INTEGER PRIMARY KEY AUTOINCREMENT,
				email TEXT NOT NULL UNIQUE,
				name TEXT,
				created_at TEXT DEFAULT (datetime('now'))
			)
		`,
		DownSQL: `DROP TABLE users`,
	})

	q.MustAdd(queen.M{
		Version: "002",
		Name:    "add_users_bio",
		UpSQL:   `ALTER TABLE users ADD COLUMN bio TEXT`,
		DownSQL: `ALTER TABLE users DROP COLUMN bio`,
	})

	// Apply all pending migrations
	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}

	fmt.Println("Migrations applied successfully!")
}
Example (CustomTableName)

Example_customTableName demonstrates using a custom table name for migrations.

package main

import (
	"database/sql"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, _ := sql.Open("sqlite3", "myapp.db")
	defer db.Close()

	// Use custom table name
	driver := sqlite.NewWithTableName(db, "my_custom_migrations")
	q := queen.New(driver)
	defer q.Close()

	// The migrations will be tracked in "my_custom_migrations" table
	// instead of the default "queen_migrations"
}
Example (ForeignKeys)

Example_foreignKeys demonstrates handling foreign keys properly.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Enable foreign keys in connection string
	db, _ := sql.Open("sqlite3", "myapp.db?_foreign_keys=on")
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// First migration: create parent table
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL: `
			CREATE TABLE users (
				id INTEGER PRIMARY KEY AUTOINCREMENT,
				email TEXT NOT NULL UNIQUE
			)
		`,
		DownSQL: `DROP TABLE users`,
	})

	// Second migration: create child table with foreign key
	q.MustAdd(queen.M{
		Version: "002",
		Name:    "create_posts",
		UpSQL: `
			CREATE TABLE posts (
				id INTEGER PRIMARY KEY AUTOINCREMENT,
				user_id INTEGER NOT NULL,
				title TEXT,
				FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
			)
		`,
		// Important: child table must be dropped first
		DownSQL: `DROP TABLE posts`,
	})

	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}

	// When rolling back, Queen will execute down migrations in reverse order:
	// 1. DROP TABLE posts (child)
	// 2. DROP TABLE users (parent)
	// This ensures foreign key constraints are satisfied
}
Example (FullConnectionString)

Example_fullConnectionString demonstrates a production-ready connection string.

package main

import (
	"database/sql"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Recommended connection string for production
	dsn := "myapp.db?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on&_synchronous=NORMAL"
	db, _ := sql.Open("sqlite3", dsn)
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// This configuration provides:
	// - WAL mode: better concurrency
	// - Busy timeout: 5 seconds to wait for locks
	// - Foreign keys: enabled
	// - Synchronous: balanced safety/performance
}
Example (GoFunctionMigration)

Example_goFunctionMigration demonstrates using Go functions for complex migrations.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, _ := sql.Open("sqlite3", "myapp.db")
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// Migration using Go function for complex logic
	q.MustAdd(queen.M{
		Version:        "003",
		Name:           "normalize_emails",
		ManualChecksum: "v1", // Important: track function changes!
		UpFunc: func(ctx context.Context, tx *sql.Tx) error {
			// Fetch all users
			rows, err := tx.QueryContext(ctx, "SELECT id, email FROM users")
			if err != nil {
				return err
			}
			defer rows.Close()

			// Normalize each email
			for rows.Next() {
				var id int
				var email string
				if err := rows.Scan(&id, &email); err != nil {
					return err
				}

				// Convert to lowercase
				normalized := normalizeEmail(email)

				// Update the email
				_, err = tx.ExecContext(ctx,
					"UPDATE users SET email = ? WHERE id = ?",
					normalized, id)
				if err != nil {
					return err
				}
			}

			return rows.Err()
		},
		DownFunc: func(ctx context.Context, tx *sql.Tx) error {
			// Rollback is not possible for this migration
			return nil
		},
	})

	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}
}

// Helper function for email normalization
func normalizeEmail(email string) string {

	return email
}
Example (InMemory)

Example_inMemory demonstrates using an in-memory database for testing.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Use in-memory database (perfect for testing)
	db, _ := sql.Open("sqlite3", ":memory:")
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// Migrations work exactly the same with in-memory databases
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`,
		DownSQL: `DROP TABLE users`,
	})

	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}

	fmt.Println("In-memory migrations applied!")
}
Example (Indexes)

Example_indexes demonstrates creating indexes for better query performance.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, _ := sql.Open("sqlite3", "myapp.db")
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// Create table
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL: `
			CREATE TABLE users (
				id INTEGER PRIMARY KEY AUTOINCREMENT,
				email TEXT NOT NULL,
				name TEXT,
				created_at TEXT DEFAULT (datetime('now'))
			)
		`,
		DownSQL: `DROP TABLE users`,
	})

	// Add indexes in a separate migration
	q.MustAdd(queen.M{
		Version: "002",
		Name:    "add_user_indexes",
		UpSQL: `
			CREATE UNIQUE INDEX idx_users_email ON users(email);
			CREATE INDEX idx_users_created_at ON users(created_at);
		`,
		DownSQL: `
			DROP INDEX IF EXISTS idx_users_email;
			DROP INDEX IF EXISTS idx_users_created_at;
		`,
	})

	ctx := context.Background()
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}
}
Example (Status)

Example_status demonstrates checking migration status.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, _ := sql.Open("sqlite3", ":memory:")
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// Register migrations
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id INTEGER PRIMARY KEY)`,
		DownSQL: `DROP TABLE users`,
	})

	q.MustAdd(queen.M{
		Version: "002",
		Name:    "create_posts",
		UpSQL:   `CREATE TABLE posts (id INTEGER PRIMARY KEY)`,
		DownSQL: `DROP TABLE posts`,
	})

	ctx := context.Background()

	// Apply first migration only
	if err := q.UpSteps(ctx, 1); err != nil {
		log.Fatal(err)
	}

	// Check status
	statuses, err := q.Status(ctx)
	if err != nil {
		log.Fatal(err)
	}

	for _, s := range statuses {
		fmt.Printf("%s: %s (%s)\n", s.Version, s.Name, s.Status)
	}

}
Output:

001: create_users (applied)
002: create_posts (pending)
Example (Testing)

Example_testing demonstrates best practices for testing migrations.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Use in-memory database for fast, isolated tests
	db, err := sql.Open("sqlite3", ":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	driver := sqlite.New(db)
	// Note: In actual tests, use queen.NewTest(t, driver)
	q := queen.New(driver)
	defer q.Close()

	// Register migrations
	q.MustAdd(queen.M{
		Version: "001",
		Name:    "create_users",
		UpSQL:   `CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)`,
		DownSQL: `DROP TABLE users`,
	})

	ctx := context.Background()

	// Test up migration
	if err := q.Up(ctx); err != nil {
		log.Fatal(err)
	}

	// Verify table exists
	var tableName string
	err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").Scan(&tableName)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Table created:", tableName)

	// Test down migration
	if err := q.Reset(ctx); err != nil {
		log.Fatal(err)
	}

	// Verify table is gone
	err = db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").Scan(&tableName)
	if err != sql.ErrNoRows {
		log.Fatal("table should be gone")
	}
	fmt.Println("Table dropped successfully")

}
Output:

Table created: users
Table dropped successfully
Example (WalMode)

Example_walMode demonstrates using WAL mode for better concurrency.

package main

import (
	"database/sql"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	// Enable WAL mode for better concurrent read/write performance
	db, _ := sql.Open("sqlite3", "myapp.db?_journal_mode=WAL")
	defer db.Close()

	driver := sqlite.New(db)
	q := queen.New(driver)
	defer q.Close()

	// Your migrations here
}
Example (WithConfig)

Example_withConfig demonstrates using custom configuration.

package main

import (
	"database/sql"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/sqlite"
	_ "github.com/mattn/go-sqlite3"
)

func main() {
	db, _ := sql.Open("sqlite3", "myapp.db")
	defer db.Close()

	driver := sqlite.New(db)

	// Create Queen with custom config
	config := &queen.Config{
		TableName:   "custom_migrations",
		LockTimeout: 5 * 60, // 5 minutes in seconds
	}
	q := queen.NewWithConfig(driver, config)
	defer q.Close()

	// Your migrations here
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Driver

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

Driver implements the queen.Driver interface for SQLite.

The driver is thread-safe for concurrent reads, but SQLite's database-level locking means only one write operation (migration) can occur at a time. This is handled automatically by the BEGIN EXCLUSIVE transaction.

func New

func New(db *sql.DB) *Driver

New creates a new SQLite driver.

The database connection should already be open and configured. The default migrations table name is "queen_migrations".

Example:

db, err := sql.Open("sqlite3", "myapp.db")
if err != nil {
    log.Fatal(err)
}
driver := sqlite.New(db)

For better performance with concurrent reads, use WAL mode:

db, err := sql.Open("sqlite3", "myapp.db?_journal_mode=WAL")

func NewWithTableName

func NewWithTableName(db *sql.DB, tableName string) *Driver

NewWithTableName creates a new SQLite driver with a custom table name.

Use this when you need to manage multiple independent sets of migrations in the same database file, or when you want to customize the table name for organizational purposes.

Example:

driver := sqlite.NewWithTableName(db, "my_migrations")

func (*Driver) Close

func (d *Driver) Close() error

Close closes the database connection.

If you're using a file-based database (not :memory:), the database file persists after closing. For in-memory databases, all data is lost.

func (*Driver) Exec

func (d *Driver) Exec(ctx context.Context, fn func(*sql.Tx) error) error

Exec executes a function within a transaction.

If the function returns an error, the transaction is rolled back. Otherwise, the transaction is committed.

This provides ACID guarantees for migration execution.

Note: SQLite supports nested transactions using SAVEPOINT, but this driver uses simple transactions for compatibility and simplicity.

func (*Driver) GetApplied

func (d *Driver) GetApplied(ctx context.Context) ([]queen.Applied, error)

GetApplied returns all applied migrations sorted by applied_at in ascending order.

This is used by Queen to determine which migrations have already been applied and which are pending.

Note: SQLite stores timestamps as TEXT in ISO8601 format. We parse them back to time.Time for consistency with other drivers.

func (*Driver) Init

func (d *Driver) Init(ctx context.Context) error

Init creates the migrations tracking table if it doesn't exist.

The table schema:

  • version: TEXT PRIMARY KEY - unique migration version
  • name: TEXT NOT NULL - human-readable migration name
  • applied_at: TEXT - ISO8601 timestamp when migration was applied
  • checksum: TEXT - hash of migration content for validation

This method is idempotent and safe to call multiple times.

Note: SQLite doesn't have a native TIMESTAMP type. We use TEXT with ISO8601 format (YYYY-MM-DD HH:MM:SS) which sorts correctly and is human-readable.

func (*Driver) Lock

func (d *Driver) Lock(ctx context.Context, timeout time.Duration) error

Lock acquires an exclusive database lock to prevent concurrent migrations.

SQLite uses database-level locking. This driver uses BEGIN EXCLUSIVE to acquire an exclusive lock on the entire database. This prevents any other connections from writing to the database until the lock is released.

The lock is held in a separate transaction (d.lockTx) and is released when Unlock() is called.

Important: Unlike PostgreSQL and MySQL where locks are separate from migration transactions, SQLite's locking transaction must remain open for the entire migration process.

If the lock cannot be acquired within the timeout, returns queen.ErrLockTimeout.

func (*Driver) Record

func (d *Driver) Record(ctx context.Context, m *queen.Migration) error

Record marks a migration as applied in the database.

This should be called after successfully executing a migration's up function. The checksum is automatically computed from the migration content.

The timestamp is automatically set by SQLite to the current time.

func (*Driver) Remove

func (d *Driver) Remove(ctx context.Context, version string) error

Remove removes a migration record from the database.

This should be called after successfully rolling back a migration's down function.

func (*Driver) Unlock

func (d *Driver) Unlock(ctx context.Context) error

Unlock releases the migration lock.

This commits the exclusive transaction, allowing other connections to write to the database.

This should be called in a defer statement after acquiring the lock. It's safe to call even if the lock wasn't acquired.

Jump to

Keyboard shortcuts

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