Skip to content
WeftKitBeta

Ruby & Rails Integration

Ruby & Rails Integration

WeftKit Standalone speaks standard wire protocols, so every existing Ruby database gem and Rails integration works without modification. This guide covers bare Ruby drivers and full Rails integration for each WeftKit engine.

Prerequisites

  • WeftKit Standalone running and reachable (see Deployment)
  • Ruby 3.2 or later
  • Bundler

Gemfile

ruby
# Gemfile

# WeftKitRel — PostgreSQL wire protocol
gem 'pg',    '~> 1.5'       # bare Ruby
gem 'activerecord', '~> 7.1' # Rails ORM (included in Rails)

# WeftKitDoc — MongoDB wire protocol
gem 'mongo', '~> 2.20'

# WeftKitMem — Redis RESP3
gem 'redis',      '~> 5.2'
gem 'hiredis-client', '~> 0.22'  # optional C extension for better performance

# WeftKitGraph — Bolt (Neo4j)
gem 'neo4j-ruby-driver', '~> 4.4'
bash
bundle install

1. WeftKitRel via pg Gem (Bare Ruby)

WeftKitRel speaks the PostgreSQL v3 wire protocol. The pg gem connects to it identically to a PostgreSQL server.

Connecting

ruby
require 'pg'

conn = PG.connect(
  host:     'localhost',
  port:     5432,
  dbname:   'mydb',
  user:     'app_user',
  password: ENV.fetch('WEFTKIT_PASS'),
  sslmode:  'prefer'
)

puts "Connected to WeftKitRel: #{conn.server_version}"

Parameterised Queries

ruby
# Always use parameterised queries — prevents SQL injection
result = conn.exec_params(
  'SELECT id, name, email FROM users WHERE age > $1 ORDER BY name',
  [18]
)

result.each do |row|
  puts "#{row['name']} — #{row['email']}"
end

result.clear

Inserting and Returning

ruby
result = conn.exec_params(
  'INSERT INTO users (name, email, age) VALUES ($1, $2, $3) RETURNING id',
  ['Alice', 'alice@example.com', 30]
)

id = result[0]['id'].to_i
puts "Created user with ID: #{id}"
result.clear

Transaction Block

ruby
conn.transaction do |tx|
  tx.exec_params(
    'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
    [100.00, 1]
  )
  tx.exec_params(
    'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
    [100.00, 2]
  )
  # Automatically commits on block exit; rolls back on exception
end

Iterating Large Result Sets

ruby
# Use exec_params with a block to avoid loading all rows into memory at once
conn.send_query_params('SELECT * FROM large_table WHERE active = $1', [true])
conn.set_single_row_mode

loop do
  result = conn.get_result
  break if result.nil?
  result.check
  result.each { |row| process(row) }
  result.clear
end

2. WeftKitRel via ActiveRecord + Rails

config/database.yml

yaml
default: &default
  adapter:  postgresql
  encoding: unicode
  pool:     <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
  timeout:  5000
  host:     <%= ENV.fetch("WEFTKIT_HOST", "localhost") %>
  port:     <%= ENV.fetch("WEFTKIT_PORT", 5432) %>
  username: <%= ENV.fetch("WEFTKIT_USER", "app_user") %>
  password: <%= ENV.fetch("WEFTKIT_PASS") { Rails.application.credentials.weftkit_pass } %>
  sslmode:  <%= ENV.fetch("WEFTKIT_SSLMODE", "prefer") %>

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

production:
  <<: *default
  database: myapp_production
  sslmode:  require
  pool:     <%= ENV.fetch("RAILS_MAX_THREADS", 10) %>

Rails Credentials (Recommended for Production)

bash
# Edit credentials — stores encrypted in config/credentials.yml.enc
rails credentials:edit
yaml
# config/credentials.yml.enc (decrypted view)
weftkit_pass: "my-production-secret"

ApplicationRecord Model

ruby
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end
ruby
# app/models/user.rb
class User < ApplicationRecord
  has_many :orders, dependent: :destroy

  validates :name,  presence: true
  validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :age,   numericality: { greater_than_or_equal_to: 0 }, allow_nil: true

  scope :adults,   -> { where('age >= ?', 18) }
  scope :by_name,  -> { order(:name) }
end
ruby
# app/models/order.rb
class Order < ApplicationRecord
  belongs_to :user

  validates :total,  numericality: { greater_than: 0 }
  validates :status, inclusion: { in: %w[pending processing shipped delivered cancelled] }

  scope :active, -> { where.not(status: 'cancelled') }
end

Rails Migration

ruby
# db/migrate/20240101000001_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string  :name,  null: false
      t.string  :email, null: false
      t.integer :age

      t.timestamps
    end

    add_index :users, :email, unique: true
    add_index :users, :age
  end
end
ruby
# db/migrate/20240101000002_create_orders.rb
class CreateOrders < ActiveRecord::Migration[7.1]
  def change
    create_table :orders do |t|
      t.references :user,   null: false, foreign_key: true
      t.decimal    :total,  null: false, precision: 10, scale: 2
      t.string     :status, null: false, default: 'pending'

      t.timestamps
    end

    add_index :orders, [:user_id, :status]
  end
end
bash
rails db:migrate

ActiveRecord Queries

ruby
# Create
user = User.create!(name: 'Alice', email: 'alice@example.com', age: 30)

# Find
user = User.find(1)
user = User.find_by!(email: 'alice@example.com')

# Find with conditions
adults = User.where('age > ?', 18).order(:name).limit(50)

# Chained scopes
top_users = User.adults.by_name.includes(:orders).where(orders: { status: 'delivered' })

# Eager loading (avoids N+1 queries)
users = User.includes(:orders).where('age > ?', 18)
users.each do |u|
  puts "#{u.name} has #{u.orders.size} orders"
end

# Update
user.update!(age: 31)
User.where('age < ?', 18).update_all(status: 'minor')

# Destroy
user.destroy
User.where(status: 'cancelled').destroy_all

ActiveRecord Transactions

ruby
ActiveRecord::Base.transaction do
  user  = User.create!(name: 'Bob', email: 'bob@example.com')
  Order.create!(user: user, total: 49.99, status: 'pending')
  # Both records committed together; any exception triggers rollback
end

3. WeftKitDoc via mongo Gem

WeftKitDoc speaks the MongoDB Wire protocol. The mongo gem connects to it directly.

Connecting

ruby
require 'mongo'

Mongo::Logger.logger.level = Logger::WARN

client = Mongo::Client.new(
  'mongodb://app_user:secret@localhost:27017/mydb',
  server_selection_timeout: 5
)

collection = client[:articles]

Inserting Documents

ruby
# Insert one
result = collection.insert_one(
  title:   'Getting Started with WeftKit',
  author:  'Alice',
  tags:    ['database', 'weftkit'],
  views:   0,
  created: Time.now
)
puts "Inserted ID: #{result.inserted_id}"

# Insert many
collection.insert_many([
  { title: 'MVCC Deep Dive', author: 'Bob',   views: 0 },
  { title: 'Vector Search',  author: 'Carol', views: 0 },
])

Finding Documents

ruby
# Find one
article = collection.find(author: 'Alice').first
puts article[:title] if article

# Find many with filter and options
cursor = collection.find({ tags: 'database' })
                   .sort(views: -1)
                   .limit(10)

cursor.each { |doc| puts "#{doc[:title]} — #{doc[:views]} views" }

Updating Documents

ruby
# Update one
collection.update_one(
  { title: 'Getting Started with WeftKit' },
  { '$set' => { views: 1500, featured: true } }
)

# Update many
collection.update_many(
  { author: 'Bob' },
  { '$inc' => { views: 10 } }
)

Aggregation Pipeline

ruby
pipeline = [
  { '$match' => { views: { '$gt' => 100 } } },
  { '$group' => {
    '_id'         => '$author',
    'total_views' => { '$sum' => '$views' },
    'count'       => { '$sum' => 1 }
  }},
  { '$sort' => { 'total_views' => -1 } },
  { '$limit' => 5 }
]

collection.aggregate(pipeline).each do |result|
  puts "#{result['_id']}: #{result['total_views']} total views"
end

4. WeftKitMem via redis Gem

WeftKitMem speaks Redis RESP3. The redis gem connects to it using the standard Redis protocol.

Connecting

ruby
require 'redis'

redis = Redis.new(
  host:     'localhost',
  port:     6379,
  password: ENV.fetch('REDIS_PASSWORD', nil),
  db:       0,
  timeout:  5.0
)

String Commands with Expiry

ruby
# Set with TTL in seconds
redis.setex('session:abc123', 1800, 'user_42')

# Get
value = redis.get('session:abc123')
if value.nil?
  puts 'Key not found or expired'
else
  puts "Session user: #{value}"
end

# Atomic increment
views = redis.incr('article:42:views')

Hash Commands

ruby
# Set multiple hash fields
redis.hmset('user:42', 'name', 'Alice', 'email', 'alice@example.com', 'plan', 'pro')

# Get all fields
fields = redis.hgetall('user:42')
puts "#{fields['name']} — #{fields['plan']}"

# Get one field
email = redis.hget('user:42', 'email')

Pub/Sub

ruby
# Publisher (in a separate thread or process)
Thread.new do
  publisher = Redis.new(host: 'localhost', port: 6379, password: ENV.fetch('REDIS_PASSWORD', nil))
  loop do
    publisher.publish('events:orders', JSON.generate({ order_id: 42, status: 'shipped' }))
    sleep 1
  end
end

# Subscriber (blocks current thread)
redis.subscribe('events:orders') do |on|
  on.message do |channel, message|
    data = JSON.parse(message)
    puts "Order #{data['order_id']} is now #{data['status']}"
  end
end

5. WeftKitGraph via Neo4j Ruby Driver

WeftKitGraph speaks the Bolt protocol. Use the neo4j-ruby-driver gem.

Connecting

ruby
require 'neo4j/driver'

driver = Neo4j::Driver::GraphDatabase.driver(
  'bolt://localhost:7687',
  Neo4j::Driver::AuthTokens.basic('app_user', 'secret')
)

Read Session

ruby
friends = driver.session(default_access_mode: Neo4j::Driver::AccessMode::READ) do |session|
  session.read_transaction do |tx|
    result = tx.run(
      'MATCH (p:Person {name: $name})-[:FRIEND]->(f:Person) RETURN f.name AS name, f.age AS age',
      name: 'Alice'
    )
    result.map { |record| { name: record[:name], age: record[:age] } }
  end
end

friends.each { |f| puts "#{f[:name]} (age #{f[:age]})" }

Write Session

ruby
driver.session do |session|
  session.write_transaction do |tx|
    tx.run(
      'MERGE (a:Person {name: $name1}) MERGE (b:Person {name: $name2}) MERGE (a)-[:FRIEND]->(b)',
      name1: 'Alice', name2: 'Carol'
    )
  end
end

Cleanup

ruby
at_exit { driver.close }

6. Rails Integration — Redis Caching and Sessions

Cache Store in config/environments/production.rb

ruby
# config/environments/production.rb
Rails.application.configure do
  config.cache_store = :redis_cache_store, {
    url:            ENV.fetch('REDIS_URL', 'redis://:secret@localhost:6379/1'),
    connect_timeout: 30,
    read_timeout:    0.2,
    write_timeout:   0.2,
    reconnect_attempts: 1,
    error_handler:  ->(method:, returning:, exception:) {
      Rails.logger.error "Redis error in #{method}: #{exception.message}"
    }
  }

  config.session_store :cache_store, key: '_myapp_session', expire_after: 2.hours
end

Action Cable Redis Adapter

yaml
# config/cable.yml
development:
  adapter: redis
  url:     <%= ENV.fetch("REDIS_URL", "redis://localhost:6379/0") %>
  channel_prefix: myapp_development

production:
  adapter: redis
  url:     <%= ENV.fetch("REDIS_URL") %>
  channel_prefix: myapp_production

Using the Cache in Controllers and Models

ruby
# In a controller
def show
  @product = Rails.cache.fetch("product:#{params[:id]}", expires_in: 1.hour) do
    Product.find(params[:id])
  end
end

# Fragment caching in a view (works automatically with redis cache_store)
# <%= cache @product do %>
#   <%= render @product %>
# <% end %>

Low-Level Redis Access in Rails

ruby
# config/initializers/redis.rb
REDIS = Redis.new(
  url:      ENV.fetch('REDIS_URL', 'redis://localhost:6379/0'),
  password: ENV.fetch('REDIS_PASSWORD', nil)
)
ruby
# Anywhere in your Rails app
REDIS.set('rate_limit:user:42', 0, ex: 3600)
count = REDIS.incr('rate_limit:user:42')
raise TooManyRequestsError if count > 100

Connection String Quick Reference

| Engine | Gem | Connection | |---|---|---| | WeftKitRel | pg | PG.connect(host: 'localhost', port: 5432, dbname: 'mydb', user: 'app_user', password: '...') | | WeftKitRel | ActiveRecord | adapter: postgresql in database.yml | | WeftKitDoc | mongo | Mongo::Client.new('mongodb://app_user:secret@localhost:27017/mydb') | | WeftKitMem | redis | Redis.new(host: 'localhost', port: 6379, password: '...') | | WeftKitGraph | neo4j-ruby-driver | GraphDatabase.driver('bolt://localhost:7687', AuthTokens.basic(...)) |

Next Steps