ydb

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Feb 4, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package ydb provides a YandexDB (YDB) driver for Queen migrations.

YDB is a distributed SQL database that combines high availability and scalability with strong consistency and ACID transactions.

Basic Usage

import (
    "database/sql"
    _ "github.com/ydb-platform/ydb-go-sdk/v3"
    "github.com/honeynil/queen"
    "github.com/honeynil/queen/drivers/ydb"
)

db, _ := sql.Open("ydb", "grpc://localhost:2136/local")
driver, _ := ydb.New(db)
q := queen.New(driver)

Connection String

The connection string format:

grpc://localhost:2136/local
grpcs://user:password@localhost:2135/local

Locking Mechanism

YDB uses optimistic concurrency control and doesn't have advisory locks like PostgreSQL. Instead, this driver uses a lock table with expiration times to implement distributed locking across multiple processes/containers.

The lock table is automatically created during initialization and uses TTL (Time To Live) for automatic cleanup of expired locks.

Compatibility

This driver requires YDB with YQL support and the ydb-go-sdk/v3 driver. Tested with YDB 23.3+.

Example

Example demonstrates basic usage of the YDB driver.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	// Connect to YDB
	// Connection string format: grpc://host:port/database
	// For secure connection use grpcs://
	db, err := sql.Open("ydb", "grpc://localhost:2136/local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Create YDB driver
	driver, err := ydb.New(db)
	if err != nil {
		log.Fatal(err)
	}

	// 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         Utf8,
				email      Utf8 NOT NULL,
				name       Utf8,
				created_at Timestamp,
				PRIMARY KEY (id)
			)
		`,
		DownSQL: `DROP TABLE users`,
	})

	q.MustAdd(queen.M{
		Version: "002",
		Name:    "add_users_bio",
		UpSQL:   `ALTER TABLE users ADD COLUMN bio Utf8`,
		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"
	"log"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	db, err := sql.Open("ydb", "grpc://localhost:2136/local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Use custom table name
	driver, err := ydb.NewWithTableName(db, "my_custom_migrations")
	if err != nil {
		log.Fatal(err)
	}
	q := queen.New(driver)
	defer q.Close()

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

Example_goFunctionMigration demonstrates using Go functions for complex migrations.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	db, err := sql.Open("ydb", "grpc://localhost:2136/local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	driver, err := ydb.New(db)
	if err != nil {
		log.Fatal(err)
	}
	q := queen.New(driver)
	defer q.Close()

	// SQL migration to insert initial data
	q.MustAdd(queen.M{
		Version: "003",
		Name:    "add_users",
		UpSQL: `
			UPSERT INTO users (id, email, name)
			VALUES
				('1', 'alice@example.com', 'Alice Smith'),
				('2', 'bob@example.com', 'Bob Johnson'),
				('3', 'carol@example.com', 'Carol Williams'),
				('4', 'david@example.com', 'David Brown'),
				('5', 'eve@example.com', 'Eve Davis')
		`,
		DownSQL: `DELETE FROM users WHERE id IN ('1', '2', '3', '4', '5')`,
	})

	// Migration using Go function for complex logic
	q.MustAdd(queen.M{
		Version:        "004",
		Name:           "normalize_names",
		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, name FROM users")
			if err != nil {
				return err
			}
			defer rows.Close()

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

				// Convert to uppercase
				normalized := normalizeNames(name)

				// Update using UPSERT (YDB-specific)
				_, err = tx.ExecContext(ctx,
					"UPSERT INTO users (id, name) VALUES ($1, $2)",
					id, normalized)
				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 name normalization
func normalizeNames(name string) string {

	return strings.ToUpper(name)
}
Example (Status)

Example_status demonstrates checking migration status.

Note: This example requires a running YDB server. It will be skipped in CI if YDB is not available.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	db, err := sql.Open("ydb", "grpc://localhost:2136/local")
	if err != nil {
		fmt.Println("YDB not available")
		return
	}
	defer db.Close()

	// Check if YDB is actually available
	if err := db.Ping(); err != nil {
		fmt.Println("YDB not available")
		return
	}

	driver, err := ydb.New(db)
	if err != nil {
		log.Fatal(err)
	}
	q := queen.New(driver)
	defer q.Close()

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

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

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

	// Example output (when YDB is available):
	// 001: create_users (applied)
	// 002: add_users_bio (pending)
}
Example (TransactionIsolation)

Example_transactionIsolation demonstrates using custom transaction isolation levels.

package main

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

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	db, err := sql.Open("ydb", "grpc://localhost:2136/local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	driver, err := ydb.New(db)
	if err != nil {
		log.Fatal(err)
	}

	// Create Queen with custom isolation level
	config := &queen.Config{
		TableName:      "migrations",
		IsolationLevel: sql.LevelSerializable, // YDB default is Serializable
	}
	q := queen.NewWithConfig(driver, config)
	defer q.Close()

	// You can also set isolation level per migration
	q.MustAdd(queen.M{
		Version:        "001",
		Name:           "create_users",
		IsolationLevel: sql.LevelSerializable,
		UpSQL: `
			CREATE TABLE users (
				id   Utf8,
				name Utf8,
				PRIMARY KEY (id)
			)
		`,
		DownSQL: `DROP TABLE users`,
	})

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

Example_withAuthentication demonstrates connecting to YDB with authentication.

package main

import (
	"database/sql"
	"log"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	// For YDB with authentication, use connection string with credentials:
	// grpcs://user:password@host:port/database
	db, err := sql.Open("ydb", "grpcs://root:password@localhost:2135/local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	driver, err := ydb.New(db)
	if err != nil {
		log.Fatal(err)
	}

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

	// Your migrations here
}
Example (WithConfig)

Example_withConfig demonstrates using custom configuration.

package main

import (
	"database/sql"
	"log"
	"time"

	"github.com/honeynil/queen"
	"github.com/honeynil/queen/drivers/ydb"
	_ "github.com/ydb-platform/ydb-go-sdk/v3"
)

func main() {
	db, err := sql.Open("ydb", "grpc://localhost:2136/local")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	driver, err := ydb.New(db)
	if err != nil {
		log.Fatal(err)
	}

	// Create Queen with custom config
	config := &queen.Config{
		TableName:   "custom_migrations",
		LockTimeout: 10 * time.Minute, // 10 minutes
	}
	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 {
	base.Driver
	// contains filtered or unexported fields
}

Driver implements the queen.Driver interface for YDB.

YDB uses table-based locking since it doesn't support advisory locks. The driver is thread-safe and can be used concurrently by multiple goroutines.

func New

func New(db *sql.DB) (*Driver, error)

New creates a new YDB driver.

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

Example:

db, err := sql.Open("ydb", "grpc://localhost:2136/local")
if err != nil {
    log.Fatal(err)
}
driver, err := ydb.New(db)
if err != nil {
    log.Fatal(err)
}

func NewWithTableName

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

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

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

Example:

driver, err := ydb.NewWithTableName(db, "my_custom_migrations")
if err != nil {
    log.Fatal(err)
}

func (*Driver) Init

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

Init creates the migrations tracking table and lock table if they don't exist.

The migrations table schema:

  • version: Utf8 PRIMARY KEY - unique migration version
  • name: Utf8 NOT NULL - human-readable migration name
  • applied_at: Timestamp NOT NULL - when the migration was applied
  • checksum: Utf8 NOT NULL - hash of migration content for validation

The lock table schema:

  • lock_key: Utf8 PRIMARY KEY - lock identifier
  • acquired_at: Timestamp - when the lock was acquired
  • expires_at: Timestamp NOT NULL - when the lock expires
  • owner_id: Utf8 NOT NULL - unique owner identifier
  • TTL: automatic cleanup of expired locks

YDB note: YDB requires explicit PRIMARY KEY specification for all tables. Timestamps are stored as Timestamp type which provides microsecond precision.

This method is idempotent and safe to call multiple times.

func (*Driver) Lock

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

Lock acquires a distributed lock to prevent concurrent migrations.

YDB doesn't have advisory locks like PostgreSQL. Instead, this driver uses a lock table with expiration times to implement distributed locking across multiple processes/containers.

The lock mechanism: 1. Cleans up expired locks using DELETE 2. Checks if an active lock exists using SELECT 3. If no lock exists, attempts INSERT 4. Retries with exponential backoff until timeout or lock is acquired

YDB uses optimistic concurrency control, so INSERT conflicts are handled gracefully by retrying with backoff.

Exponential backoff starts at 50ms and doubles up to 1s maximum to reduce database load during lock contention.

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. YDB-specific implementation that includes applied_at timestamp.

Unlike other drivers, YDB does not support DEFAULT values with function calls (e.g., DEFAULT CurrentUtcTimestamp()). It only supports literal DEFAULT values. Therefore, this method explicitly inserts the timestamp using CurrentUtcTimestamp() in the SQL query instead of relying on a column DEFAULT.

func (*Driver) Unlock

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

Unlock releases the migration lock.

This removes the lock record from the lock table, allowing other processes to acquire the lock.

The unlock operation checks the owner_id to ensure only the process that acquired the lock can release it. This prevents race conditions where an expired lock is released by the wrong process.

This method is graceful: it returns nil if the lock doesn't exist, was already released, or belongs to another process. This prevents errors during cleanup when locks expire via TTL or in error recovery scenarios.

Jump to

Keyboard shortcuts

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