SQLite Crypto

Version License types supported Tests Coverage Status

Drop-in UUID and ULID primary keys for Rails + SQLite3

# Just use :uuid or :ulid instead of default integer IDs
create_table :users, id: :uuid do |t|
  t.string :email
end

What You Get

UUID primary keys (v4 random or v7 time-sortable)
ULID primary keys (time-sortable, 26 characters)
Automatic foreign key detection - t.references just works
Model generators - generates_uuid :token
Clean schema.rb - No verbose type definitions
Zero dependencies - Uses Ruby's built-in SecureRandom

Quick Start

bundle add sqlite_crypto
rails generate sqlite_crypto:install

ID Type Comparison

Type Format Performance Use Case
Integer 1, 2, 3... Baseline Simple apps, no distribution
UUIDv7 018d3f91-... (36 chars) ~1-3% slower inserts Recommended - Time-sortable + fast
UUIDv4 550e8400-e29b-... (36 chars) ~2-5% slower inserts Random IDs, legacy compatibility
ULID 01ARZ3NDEK... (26 chars) ~3-7% slower inserts Time-sortable, compact format

UUIDv7 embeds a timestamp in the first 48 bits, making IDs naturally sortable by creation time:

UUIDv7: 018d3f91-8f4a-7000-9e7b-4a5c8d2e1f3a  

Performance Impact:

  • New records insert at the end of B-tree index (not random positions)
  • Reduces page splits and fragmentation
  • ~40% faster index writes vs UUIDv4 at scale (10k+ records)

Requirements: Ruby 3.3+ (falls back to v4 on older versions)

Configuration

Choose UUID Version (v4 or v7)

After running rails generate sqlite_crypto:install:

# config/initializers/sqlite_crypto.rb
SqliteCrypto.configure do |config|
  config.uuid_version = :v7  # Recommended (requires Ruby 3.3+)
  # config.uuid_version = :v4  # Use this for Ruby 3.1/3.2
end

Ruby Version Support:

  • Ruby 3.3+ → v4 and v7
  • Ruby 3.1/3.2 → v4 only

Check programmatically:

SqliteCrypto::Generators::Uuid.v7_available?  # => true/false

Usage Examples

UUID Primary Keys

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, id: :uuid do |t|
      t.string :email
      t.timestamps
    end
  end
end

Result:

user = User.create!(email: "[email protected]")
user.id  # => "018d3f91-8f4a-7000-9e7b-4a5c8d2e1f3a" (UUIDv7)

ULID Primary Keys

class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts, id: :ulid do |t|
      t.string :title
      t.timestamps
    end
  end
end

Result:

post = Post.create!(title: "Hello World")
post.id  # => "01ARZ3NDEKTSV4RRFFQ69G5FAV" (26 chars, time-sortable)

Automatic Foreign Keys

The gem automatically detects parent table ID types:

# Users have UUID primary keys
create_table :users, id: :uuid do |t|
  t.string :name
end

# Posts automatically get varchar(36) foreign keys
create_table :posts do |t|
  t.references :user      # Auto-detected as varchar(36)!
  t.string :title
end

Works with ULID too:

create_table :categories, id: :ulid do |t|
  t.string :name
end

create_table :articles do |t|
  t.references :category  # Auto-detected as varchar(26)!
end

Generate UUIDs/ULIDs for Any Column

class User < ApplicationRecord
  generates_uuid :token                    # Auto-generate on create
  generates_ulid :reference, unique: true  # With uniqueness validation
end

user = User.create!(email: "[email protected]")
user.token      # => "018d3f91-..." (auto-generated)
user.reference  # => "01ARZ3NDEK..." (auto-generated + validated)

Requirements

  • Ruby: 3.1+ (3.3+ for UUIDv7)
  • Rails: 7.1, 7.2, 8.0, 8.1
  • Database: SQLite3

Performance Benchmarks

Run your own benchmarks: bundle exec rspec --tag performance

Typical results (M1 Mac, SQLite3, 10k records):

Operation Integer (baseline) UUIDv4 UUIDv7 ULID
Insert (10k) 1.00x 1.02x 1.01x 1.05x
Query by ID 1.00x 1.03x 1.03x 1.04x
Index size 100% 145% 145% 130%

Key takeaway: UUIDv7 has nearly identical performance to v4, with better write scaling.

Advanced Usage

Custom Table Names

Use :to_table for non-standard associations:

create_table :posts do |t|
  t.references :author, to_table: :users  # Uses users table's ID type
end

Mixing ID Types

Different tables can use different ID types:

create_table :users, id: :uuid do |t|
  t.string :email
end

create_table :sessions, id: :ulid do |t|
  t.references :user  # Auto-detected as varchar(36)
end

create_table :logs do |t|  # Integer ID (default)
  t.string :message
end

ID Prefixes (Stripe-style)

class Invoice < ApplicationRecord
  before_create :add_prefix

  private
  def add_prefix
    self.id = "inv_#{SecureRandom.uuid}" if id.nil?
  end
end

Migrating Existing Apps

Use UUID/ULID only for new tables:

# Keep existing integer IDs
# users: id (integer)
# posts: id (integer), user_id (integer)

# New tables use UUID/ULID
create_table :invoices, id: :uuid do |t|
  t.references :user  # Still integer (auto-detected)
  t.decimal :amount
end

How It Works

  1. Type Registration - Registers :uuid and :ulid with ActiveRecord's SQLite3 adapter
  2. Validation - UUIDs: 36-char format, ULIDs: 26-char format
  3. Migration Helpers - t.uuid() and t.ulid() in migrations
  4. Smart References - t.references detects parent table ID type
  5. Model Extensions - generates_uuid/generates_ulid for auto-generation
  6. Schema Dumper - Clean output: id: :uuid instead of verbose definitions

Development

bundle install
bundle exec rspec                    # Run tests
bundle exec standardrb               # Lint
bundle exec rspec --tag performance  # Benchmarks

Contributing

See CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE.txt