CrackedRuby logo

CrackedRuby

Sequel

Overview

Sequel provides database access and ORM functionality for Ruby applications through a flexible, composable API. The toolkit supports multiple database adapters including PostgreSQL, MySQL, SQLite, and others, offering both raw SQL execution and high-level model abstractions.

The core architecture centers around the Sequel::Database class for connections, Sequel::Dataset for query building, and Sequel::Model for object-relational mapping. Sequel emphasizes explicit control over database operations while providing convenient abstractions for common patterns.

# Database connection
DB = Sequel.connect('postgres://user:pass@localhost/mydb')

# Raw query execution
DB['SELECT * FROM users WHERE active = ?', true].all
# => [{id: 1, name: "Alice", active: true}, ...]

# Dataset query building
DB[:users].where(active: true).select(:id, :name).all
# => [{id: 1, name: "Alice"}, {id: 2, name: "Bob"}]

Sequel datasets use method chaining for query construction, with each method returning a new dataset object rather than modifying the original. This functional approach enables query reuse and composition without side effects.

active_users = DB[:users].where(active: true)
admin_users = active_users.where(role: 'admin')
recent_admins = admin_users.where(created_at: Date.today - 30)

The model layer provides ActiveRecord-style functionality with associations, validations, and hooks while maintaining Sequel's explicit philosophy. Models inherit from Sequel::Model and automatically infer table structure and relationships.

Basic Usage

Database connections require specifying an adapter and connection parameters through a connection string or hash. Sequel supports connection pooling, prepared statements, and transaction management across all supported adapters.

# Connection string format
DB = Sequel.connect('adapter://user:password@host:port/database')

# Hash format with options
DB = Sequel.connect(
  adapter: 'postgres',
  host: 'localhost',
  database: 'myapp',
  user: 'postgres',
  password: 'secret',
  max_connections: 10
)

Dataset operations form the foundation of Sequel's query interface. Datasets represent SQL queries and support method chaining for building complex conditions, joins, and aggregations.

# Basic filtering and selection
users = DB[:users]
active_users = users.where(active: true)
user_names = active_users.select(:id, :name, :email)

# Ordering and limiting
recent_users = users.order(Sequel.desc(:created_at)).limit(10)

# Aggregation
user_count = users.where(active: true).count
# => 42

# Grouping
user_stats = DB[:orders]
  .group(:user_id)
  .select(:user_id, Sequel.function(:count, :*).as(:order_count))

Model definition establishes the relationship between Ruby classes and database tables. Sequel automatically detects primary keys, foreign keys, and column types based on database schema.

class User < Sequel::Model
  # Table automatically inferred as 'users'
  # Primary key automatically detected
  
  one_to_many :orders
  many_to_one :company
  
  def before_save
    self.updated_at = Time.now
    super
  end
end

# Model usage
user = User.new(name: 'Alice', email: 'alice@example.com')
user.save

# Finding records
User[1]  # Find by primary key
User.first(name: 'Alice')
User.where(active: true).all

CRUD operations work through both dataset and model interfaces. The dataset interface provides direct SQL-like operations, while models offer object-oriented persistence methods.

# Dataset CRUD
DB[:users].insert(name: 'Bob', email: 'bob@example.com')
DB[:users].where(id: 1).update(active: false)
DB[:users].where(active: false).delete

# Model CRUD
user = User.create(name: 'Charlie', email: 'charlie@example.com')
user.update(active: false)
user.delete

Advanced Usage

Query composition enables building reusable query components and dynamic filtering logic. Sequel datasets support complex joins, subqueries, and window functions for sophisticated data retrieval patterns.

# Complex joins with multiple tables
detailed_orders = DB[:orders]
  .join(:users, id: :user_id)
  .join(:products, id: Sequel[:orders][:product_id])
  .select(
    Sequel[:orders][:id].as(:order_id),
    Sequel[:users][:name].as(:user_name),
    Sequel[:products][:title].as(:product_name),
    Sequel[:orders][:total]
  )

# Subquery usage
expensive_orders = DB[:orders].where(total: 100..Float::INFINITY)
high_value_users = DB[:users].where(
  id: expensive_orders.select(:user_id).distinct
)

# Window functions for analytics
ranked_sales = DB[:sales]
  .select(
    :*,
    Sequel.function(:row_number)
      .over(partition: :region, order: Sequel.desc(:amount))
      .as(:rank)
  )

Custom dataset methods extend query functionality for domain-specific operations. These methods become available on all datasets and support method chaining like built-in operations.

module DatasetExtensions
  def active
    where(active: true)
  end
  
  def by_date_range(start_date, end_date)
    where(created_at: start_date..end_date)
  end
  
  def with_stats
    select_append(
      Sequel.function(:count, :*).over.as(:total_count),
      Sequel.function(:avg, :amount).over.as(:avg_amount)
    )
  end
end

Sequel::Dataset.register_extension(:custom, DatasetExtensions)
DB.extension(:custom)

# Usage of custom methods
recent_active_orders = DB[:orders]
  .active
  .by_date_range(Date.today - 7, Date.today)
  .with_stats

Model plugins extend functionality through mixins that add methods, validations, and callbacks. Sequel includes numerous plugins for common patterns like timestamps, soft deletes, and serialization.

class User < Sequel::Model
  plugin :timestamps, update_on_create: true
  plugin :validation_helpers
  plugin :json_serializer
  plugin :dirty  # Track attribute changes
  
  def validate
    super
    validates_presence [:name, :email]
    validates_unique :email
    validates_format /\A[^@\s]+@[^@\s]+\z/, :email
  end
  
  def before_update
    if column_changed?(:email)
      self.email_verified = false
    end
    super
  end
end

# Plugin functionality
user = User.new(name: 'Dave')
user.valid?  # => false (missing email)
user.email = 'dave@example.com'
user.save

user.update(email: 'newemail@example.com')
user.email_verified  # => false (automatically set by hook)

Association customization handles complex relationship patterns through options and custom methods. Associations support eager loading, conditions, and custom join logic.

class User < Sequel::Model
  one_to_many :orders, order: Sequel.desc(:created_at)
  one_to_many :recent_orders, class: :Order,
    conditions: { created_at: Date.today - 30..Date.today }
  
  many_to_many :roles, join_table: :user_roles,
    left_key: :user_id, right_key: :role_id
  
  one_to_one :profile, key: :user_id
  
  def admin?
    roles.any? { |role| role.name == 'admin' }
  end
end

class Order < Sequel::Model
  many_to_one :user
  one_to_many :line_items
  many_to_one :shipping_address, class: :Address, key: :ship_address_id
  
  dataset_module do
    def shipped
      where(status: 'shipped')
    end
    
    def high_value(threshold = 100)
      where { total > threshold }
    end
  end
end

# Association usage with eager loading
users_with_orders = User.eager(:orders, :profile).where(active: true)
admin_users = User.eager(:roles).select { |u| u.admin? }

Error Handling & Debugging

Database connection errors require retry logic and graceful degradation strategies. Sequel provides connection pooling that handles temporary failures and connection timeouts automatically.

# Connection error handling
begin
  DB = Sequel.connect(DATABASE_URL, 
    max_connections: 10,
    pool_timeout: 5,
    retry_on_disconnect: true
  )
rescue Sequel::DatabaseConnectionError => e
  logger.error "Database connection failed: #{e.message}"
  raise unless Rails.env.development?
  # Fallback to SQLite in development
  DB = Sequel.sqlite
end

# Query execution error handling
def safe_user_lookup(email)
  User.first(email: email)
rescue Sequel::DatabaseError => e
  logger.warn "Database query failed: #{e.message}"
  nil
rescue Sequel::NoMatchingRow => e
  logger.info "User not found: #{email}"
  nil
end

Validation errors in models provide structured feedback for data integrity issues. Sequel's validation helpers generate detailed error messages for different validation types.

class User < Sequel::Model
  plugin :validation_helpers
  
  def validate
    super
    validates_presence [:name, :email], message: "is required"
    validates_unique :email, message: "already exists"
    validates_format /\A[^@\s]+@[^@\s]+\z/, :email, 
      message: "must be valid email format"
    validates_length_range 8..100, :password, 
      message: "must be between 8 and 100 characters"
  end
end

# Handling validation errors
user = User.new(name: '', email: 'invalid-email')
unless user.valid?
  user.errors.full_messages.each do |error|
    puts error
  end
  # => "name is required"
  # => "email must be valid email format"
end

# Rescue validation errors during save
begin
  user.save
rescue Sequel::ValidationFailed => e
  errors = e.model.errors.full_messages
  render json: { errors: errors }, status: 422
end

SQL query debugging utilizes Sequel's logging capabilities to inspect generated queries, parameter binding, and execution timing. The logger captures all database operations including transactions.

# Enable query logging
DB.loggers << Logger.new($stdout)

# Log specific operations
DB.transaction do
  user = User.create(name: 'Test User', email: 'test@example.com')
  # Logs: INSERT INTO users (name, email) VALUES ('Test User', 'test@example.com')
  
  orders = user.orders_dataset.where(status: 'pending')
  # Logs: SELECT * FROM orders WHERE (user_id = 1) AND (status = 'pending')
end

# Custom query inspection
dataset = DB[:users].where(active: true).order(:name)
puts dataset.sql
# => SELECT * FROM users WHERE (active IS TRUE) ORDER BY name

# Parameter binding inspection
filtered_users = DB[:users].where(created_at: Date.today..Date.today + 1)
puts filtered_users.sql
# => SELECT * FROM users WHERE (created_at >= '2023-01-01' AND created_at < '2023-01-02')

Transaction debugging tracks nested transactions, rollback scenarios, and deadlock detection. Sequel provides transaction hooks for monitoring and error recovery.

# Transaction monitoring
class TransactionMonitor
  def self.around_transaction(db)
    start_time = Time.now
    result = yield
    duration = Time.now - start_time
    Rails.logger.info "Transaction completed in #{duration}s"
    result
  rescue => e
    Rails.logger.error "Transaction failed: #{e.message}"
    raise
  end
end

DB.transaction(rollback: :reraise) do |conn|
  TransactionMonitor.around_transaction(DB) do
    # Complex multi-table operations
    user = User.create(name: 'New User', email: 'new@example.com')
    profile = Profile.create(user_id: user.id, bio: 'User bio')
    
    # Simulate potential failure point
    raise "Simulated error" if rand < 0.5
    
    Order.create(user_id: user.id, total: 99.99)
  end
end

Performance & Memory

Query optimization focuses on efficient SQL generation, proper indexing usage, and minimizing database round trips. Sequel's dataset methods generate optimized queries that leverage database-specific features.

# Efficient bulk operations
users_data = [
  { name: 'User 1', email: 'user1@example.com' },
  { name: 'User 2', email: 'user2@example.com' },
  # ... 1000 more records
]

# Bulk insert - single query instead of 1000
DB[:users].multi_insert(users_data)

# Bulk update with conditions
DB[:users].where(active: false).update(status: 'inactive')

# Efficient counting with exists
has_orders = DB[:orders].where(user_id: user.id).exists
# Better than: orders_count = DB[:orders].where(user_id: user.id).count > 0

Memory management becomes critical when processing large result sets. Sequel provides streaming interfaces and lazy evaluation to handle datasets that exceed available memory.

# Memory-efficient iteration over large datasets
DB[:large_table].paged_each(rows_per_fetch: 1000) do |row|
  # Process one row at a time
  # Only 1000 rows held in memory at once
  process_row(row)
end

# Lazy dataset evaluation
large_dataset = DB[:orders].where(created_at: Date.today - 365..Date.today)
# No query executed yet

processed_count = 0
large_dataset.each do |order|
  # Query executed when iteration begins
  # Processes rows as they're fetched
  process_order(order)
  processed_count += 1
  
  # Periodic progress logging
  puts "Processed #{processed_count} orders" if processed_count % 1000 == 0
end

Connection pooling configuration affects both performance and resource utilization. Pool size, timeout settings, and connection validation prevent resource exhaustion and improve response times.

# Production connection pool settings
DB = Sequel.connect(DATABASE_URL,
  max_connections: 20,          # Pool size based on server capacity
  pool_timeout: 5,              # Wait time for available connection
  pool_sleep_time: 0.001,       # Sleep between connection attempts
  pool_connection_validation: true,  # Validate connections before use
  
  # Connection-specific optimizations
  prepared_statements: true,     # Cache prepared statements
  prepared_statements_cache_size: 100,
  
  # Timeout settings
  connect_timeout: 10,          # Initial connection timeout
  read_timeout: 30,             # Query execution timeout
  write_timeout: 30             # Write operation timeout
)

# Monitor pool usage
pool_stats = DB.pool.instance_eval do
  {
    size: @allocated.size,
    available: @available.size,
    created: @created_count
  }
end
puts "Pool stats: #{pool_stats}"

Query performance analysis identifies bottlenecks through execution plan examination and timing measurement. Sequel integrates with database-specific profiling tools for detailed performance insights.

# Query timing measurement
def benchmark_query(description, &block)
  start_time = Time.now
  result = yield
  duration = Time.now - start_time
  puts "#{description}: #{(duration * 1000).round(2)}ms"
  result
end

# Compare query strategies
users = benchmark_query("N+1 query pattern") do
  User.all.map { |user| [user.name, user.orders.count] }
end

users = benchmark_query("Optimized with eager loading") do
  User.eager(:orders).map { |user| [user.name, user.orders.length] }
end

# Explain query execution
complex_query = DB[:users]
  .join(:orders, user_id: :id)
  .where(active: true)
  .group(:user_id)
  .having { count(:*) > 5 }

puts complex_query.explain
# Database-specific execution plan output

Production Patterns

Application integration with web frameworks requires careful database connection management, transaction handling, and error recovery strategies. Sequel integrates seamlessly with Rack-based applications and provides middleware for connection management.

# Rails integration
class ApplicationController < ActionController::Base
  around_action :wrap_in_transaction, only: [:create, :update, :destroy]
  
  private
  
  def wrap_in_transaction
    DB.transaction(rollback: :reraise) do
      yield
    rescue ActiveRecord::RecordInvalid => e
      # Handle validation errors gracefully
      render json: { errors: e.record.errors.full_messages }, 
        status: 422
      raise Sequel::Rollback
    end
  end
end

# Sinatra integration with connection management
class APIApp < Sinatra::Base
  before do
    # Ensure fresh connection for each request
    DB.test_connection
  rescue Sequel::DatabaseDisconnectError
    DB.disconnect
    DB.test_connection
  end
  
  after do
    # Clean up any open transactions
    DB.rollback_on_exit = false
  end
end

Background job processing requires robust error handling and transaction management to ensure data consistency across job failures and retries.

class OrderProcessor
  include Sidekiq::Worker
  sidekiq_options retry: 3, backtrace: true
  
  def perform(order_id)
    DB.transaction do
      order = Order[order_id]
      raise "Order not found: #{order_id}" unless order
      
      # Process payment
      payment_result = PaymentService.charge(order.total, order.payment_method)
      
      # Update order status
      order.update(
        status: 'paid',
        payment_id: payment_result.id,
        processed_at: Time.now
      )
      
      # Send confirmation email
      OrderMailer.confirmation(order.id).deliver_now
      
      # Log successful processing
      Rails.logger.info "Order #{order_id} processed successfully"
    end
  rescue => e
    # Log error details for debugging
    Rails.logger.error "Order processing failed: #{e.message}"
    Rails.logger.error e.backtrace.join("\n")
    raise  # Re-raise for Sidekiq retry logic
  end
end

Monitoring and observability require instrumentation of database operations, connection pool metrics, and query performance tracking. Integration with APM tools provides production insights.

# Custom instrumentation
module DatabaseInstrumentation
  def self.instrument_queries
    DB.extension :connection_validator
    DB.pool.connection_validation_timeout = 3600
    
    # Hook into query execution
    DB.log_info_level = :debug
    DB.sql_log_level = :debug
    
    # Track slow queries
    DB.define_singleton_method(:log_duration) do |duration, sql|
      if duration > 1.0  # Queries slower than 1 second
        Rails.logger.warn "Slow query (#{duration}s): #{sql}"
        # Send to monitoring service
        StatsD.histogram('database.slow_query', duration, tags: ['table:users'])
      end
      super(duration, sql)
    end
  end
end

# Health check endpoint
class HealthController < ApplicationController
  def database
    start_time = Time.now
    DB.test_connection
    duration = Time.now - start_time
    
    pool_info = {
      size: DB.pool.size,
      max_size: DB.pool.max_size,
      available: DB.pool.available_connections.size
    }
    
    render json: {
      status: 'healthy',
      response_time: "#{(duration * 1000).round(2)}ms",
      pool: pool_info
    }
  rescue => e
    render json: { 
      status: 'unhealthy', 
      error: e.message 
    }, status: 503
  end
end

Deployment considerations include database migrations, schema management, and zero-downtime deployment strategies. Sequel provides migration tools that support incremental schema changes.

# Migration management
Sequel.migration do
  up do
    create_table(:user_preferences) do
      primary_key :id
      foreign_key :user_id, :users, null: false
      String :key, null: false
      Text :value
      DateTime :created_at, null: false
      DateTime :updated_at, null: false
      
      index [:user_id, :key], unique: true
    end
    
    # Populate default preferences for existing users
    from(:users).select(:id).each do |user|
      from(:user_preferences).insert(
        user_id: user[:id],
        key: 'email_notifications',
        value: 'true',
        created_at: Time.now,
        updated_at: Time.now
      )
    end
  end
  
  down do
    drop_table(:user_preferences)
  end
end

# Deployment script
class DatabaseMigrator
  def self.migrate_safely
    # Check for pending migrations
    Sequel::Migrator.check_current(DB, 'db/migrations')
    
    # Run migrations with timeout
    Timeout::timeout(300) do  # 5 minute timeout
      Sequel::Migrator.run(DB, 'db/migrations', 
        allow_missing_migration_files: true
      )
    end
    
    puts "Database migration completed successfully"
  rescue => e
    puts "Migration failed: #{e.message}"
    exit 1
  end
end

Reference

Database Connection Methods

Method Parameters Returns Description
Sequel.connect(url, **opts) Connection URL/hash, options Database Creates database connection
DB.test_connection None true Tests connection validity
DB.disconnect None nil Closes all connections
DB.transaction(**opts, &block) Transaction options, block Block result Executes block in transaction

Dataset Query Methods

Method Parameters Returns Description
dataset.where(conditions) Hash/conditions Dataset Filters records by conditions
dataset.select(*columns) Column names/expressions Dataset Selects specific columns
dataset.order(*expressions) Order expressions Dataset Orders results
dataset.limit(count, offset=nil) Count, optional offset Dataset Limits result count
dataset.join(table, conditions=nil) Table, join conditions Dataset Joins with another table
dataset.group(*columns) Column expressions Dataset Groups results
dataset.having(conditions) Having conditions Dataset Filters grouped results

Dataset Execution Methods

Method Parameters Returns Description
dataset.all None Array<Hash> Returns all matching rows
dataset.first None Hash Returns first matching row
dataset.count None Integer Returns count of matches
dataset.exists None Boolean Tests if any records exist
dataset.each(&block) Block Dataset Iterates over results
dataset.insert(values) Hash/Array Primary key Inserts new record
dataset.update(values) Hash Integer Updates matching records
dataset.delete None Integer Deletes matching records

Model Class Methods

Method Parameters Returns Description
Model.create(attributes) Attribute hash Model Creates and saves instance
Model.find(id) Primary key value Model Finds by primary key
Model.first(conditions) Filter conditions Model Returns first match
Model.where(conditions) Filter conditions Dataset Returns filtered dataset
Model.all None Array<Model> Returns all instances
Model.each(&block) Block nil Iterates over all instances

Model Instance Methods

Method Parameters Returns Description
model.save None Model Saves instance to database
model.update(attributes) Attribute hash Model Updates and saves attributes
model.delete None Model Deletes instance from database
model.valid? None Boolean Validates instance
model.errors None Errors Returns validation errors
model.changed_columns None Array<Symbol> Lists modified attributes

Association Methods

Method Parameters Returns Description
one_to_many(name, **opts) Association name, options nil Defines one-to-many association
many_to_one(name, **opts) Association name, options nil Defines many-to-one association
many_to_many(name, **opts) Association name, options nil Defines many-to-many association
one_to_one(name, **opts) Association name, options nil Defines one-to-one association

Common Options

Connection Options:

  • :max_connections - Pool size (Integer)
  • :pool_timeout - Connection wait timeout (Numeric)
  • :retry_on_disconnect - Retry failed connections (Boolean)
  • :prepared_statements - Enable prepared statements (Boolean)

Query Options:

  • :server - Database server/shard to use (Symbol)
  • :graph - Include associated objects (Hash)
  • :eager - Eager load associations (Symbol/Array)
  • :lock - Row locking mode (String/Symbol)

Validation Plugin Methods:

  • validates_presence(attributes, **opts)
  • validates_unique(attributes, **opts)
  • validates_format(regexp, attributes, **opts)
  • validates_length_range(range, attributes, **opts)
  • validates_type(type, attributes, **opts)

Error Classes

Error Description
Sequel::Error Base error class
Sequel::DatabaseError Database operation errors
Sequel::DatabaseConnectionError Connection failures
Sequel::ValidationFailed Model validation errors
Sequel::NoMatchingRow No records found
Sequel::ConstraintViolation Database constraint errors