SQLite Crypto
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 |
Why UUIDv7 is Recommended
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.
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.
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
- Type Registration - Registers
:uuidand:ulidwith ActiveRecord's SQLite3 adapter - Validation - UUIDs: 36-char format, ULIDs: 26-char format
- Migration Helpers -
t.uuid()andt.ulid()in migrations - Smart References -
t.referencesdetects parent table ID type - Model Extensions -
generates_uuid/generates_ulidfor auto-generation - Schema Dumper - Clean output:
id: :uuidinstead 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