dorm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +8 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/README.md +226 -0
- data/Rakefile +92 -0
- data/examples/connection_pool_example.rb +88 -0
- data/examples/query_builder_examples.rb +202 -0
- data/lib/dorm/connection_pool.rb +218 -0
- data/lib/dorm/database.rb +142 -0
- data/lib/dorm/functional_helpers.rb +141 -0
- data/lib/dorm/query_builder.rb +434 -0
- data/lib/dorm/repository.rb +338 -0
- data/lib/dorm/result.rb +77 -0
- data/lib/dorm/version.rb +5 -0
- data/lib/dorm.rb +25 -0
- data/sig/Dorm.rbs +4 -0
- metadata +159 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: de1f49b44f40ef0f750331d7785409e97829545094a60e475c6f5407293d34e3
|
|
4
|
+
data.tar.gz: 4e388e613ce981392c831b09d0424493f9b2fb61a2972e797d31fad6b8cf6654
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 45b0f4170cf44f50336f06c73e1aa342c8b32508aef823ec6a04310ba6b1346ccc1b4944bc7342355543f20d28e981a89b9270a2ec0d0f570bcf869e2fe138a8
|
|
7
|
+
data.tar.gz: cf1da5106c8acc1d6992d4b5353924e2098938410c4a820f1aeb73fe6b2421dd047806e1340e23431624cbe483f6f19f7a50c354da1860a56f51b1e4ed0df3ee
|
data/.rubocop.yml
ADDED
data/.ruby-gemset
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dorm
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.5
|
data/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Dorm
|
|
2
|
+
|
|
3
|
+
**D**ata **ORM** - A lightweight, functional ORM for Ruby built on the `Data` class introduced in Ruby 3.2. Features immutable records, monadic error handling, and a functional programming approach to database operations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔧 **Immutable Records**: Built on Ruby's `Data` class for immutable, value-based objects
|
|
8
|
+
- 🚂 **Railway-Oriented Programming**: Monadic error handling inspired by dry-monads
|
|
9
|
+
- 🎯 **Functional Approach**: Pure functions in modules instead of stateful classes
|
|
10
|
+
- 🔍 **Automatic CRUD**: Metaprogramming generates standard operations from Data class introspection
|
|
11
|
+
- ✅ **Built-in Validations**: Declarative validation rules with clear error messages
|
|
12
|
+
- 🔄 **Safe Updates**: Use `.with()` for immutable updates that return new objects
|
|
13
|
+
- 🗄️ **Database Agnostic**: Support for PostgreSQL and SQLite3
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'dorm'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
```bash
|
|
25
|
+
$ bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it yourself as:
|
|
29
|
+
```bash
|
|
30
|
+
$ gem install dorm
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Configure Database Connection
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'dorm'
|
|
39
|
+
|
|
40
|
+
Dorm.configure(
|
|
41
|
+
adapter: :postgresql,
|
|
42
|
+
host: 'localhost',
|
|
43
|
+
dbname: 'myapp_development',
|
|
44
|
+
user: 'postgres'
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Define Your Data Structures
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
User = Data.define(:id, :name, :email, :created_at, :updated_at)
|
|
52
|
+
Post = Data.define(:id, :title, :body, :user_id, :created_at, :updated_at)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Create Repositories
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Users = Dorm.repository_for(User,
|
|
59
|
+
validations: {
|
|
60
|
+
name: { required: true, length: 1..100 },
|
|
61
|
+
email: { required: true, format: /@/ }
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
Posts = Dorm.repository_for(Post,
|
|
66
|
+
validations: {
|
|
67
|
+
title: { required: true, length: 1..200 },
|
|
68
|
+
body: { required: true },
|
|
69
|
+
user_id: { required: true }
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 4. Use With Monadic Error Handling
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Chain operations - if any fail, the rest are skipped
|
|
78
|
+
result = Users.create(name: "Alice", email: "alice@example.com")
|
|
79
|
+
.bind { |user| Posts.create(title: "Hello", body: "World", user_id: user.id) }
|
|
80
|
+
.map { |post| "Created post: #{post.title}" }
|
|
81
|
+
|
|
82
|
+
if result.success?
|
|
83
|
+
puts result.value # "Created post: Hello"
|
|
84
|
+
else
|
|
85
|
+
puts "Error: #{result.error}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Safe value extraction with defaults
|
|
89
|
+
user = Users.find(1).value_or(nil)
|
|
90
|
+
|
|
91
|
+
# Check success/failure
|
|
92
|
+
user_result = Users.find(999)
|
|
93
|
+
if user_result.success?
|
|
94
|
+
puts "Found: #{user_result.value.name}"
|
|
95
|
+
else
|
|
96
|
+
puts "Not found: #{user_result.error}"
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. Immutable Updates
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
user = Users.find(1).value
|
|
104
|
+
updated_user = Users.save(user.with(name: "Alice Smith"))
|
|
105
|
+
|
|
106
|
+
if updated_user.success?
|
|
107
|
+
puts "Updated: #{updated_user.value.name}"
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Available Repository Methods
|
|
112
|
+
|
|
113
|
+
Every repository automatically gets these methods:
|
|
114
|
+
|
|
115
|
+
### CRUD Operations
|
|
116
|
+
- `find(id)` - Find record by ID
|
|
117
|
+
- `find_all` - Get all records
|
|
118
|
+
- `create(attrs)` - Create new record
|
|
119
|
+
- `update(record)` - Update existing record
|
|
120
|
+
- `save(record)` - Create or update (based on presence of ID)
|
|
121
|
+
- `delete(record)` - Delete record
|
|
122
|
+
|
|
123
|
+
### Query Methods
|
|
124
|
+
- `where(predicate)` - Filter with a lambda/proc
|
|
125
|
+
- `find_by(**attrs)` - Find first record matching attributes
|
|
126
|
+
- `find_all_by(**attrs)` - Find all records matching attributes
|
|
127
|
+
- `count` - Count total records
|
|
128
|
+
|
|
129
|
+
### Examples
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
# Find operations
|
|
133
|
+
user = Users.find(1)
|
|
134
|
+
all_users = Users.find_all
|
|
135
|
+
|
|
136
|
+
# Query operations
|
|
137
|
+
active_users = Users.where(->(u) { u.active })
|
|
138
|
+
user_by_email = Users.find_by(email: "alice@example.com")
|
|
139
|
+
posts_by_user = Posts.find_all_by(user_id: user.value.id)
|
|
140
|
+
|
|
141
|
+
# CRUD with validation
|
|
142
|
+
new_user = Users.create(name: "Bob", email: "bob@example.com")
|
|
143
|
+
updated = Users.save(new_user.value.with(name: "Robert"))
|
|
144
|
+
deleted = Users.delete(updated.value)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Functional Composition
|
|
148
|
+
|
|
149
|
+
Use the included functional helpers for more complex operations:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
include Dorm::FunctionalHelpers
|
|
153
|
+
|
|
154
|
+
# Pipeline processing
|
|
155
|
+
result = pipe(
|
|
156
|
+
Users.find_all.value,
|
|
157
|
+
partial(method(:filter), ->(u) { u.name.length > 3 }),
|
|
158
|
+
partial(method(:map_over), ->(u) { u.email })
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Validation Rules
|
|
163
|
+
|
|
164
|
+
Support for common validation patterns:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
Users = Dorm.repository_for(User,
|
|
168
|
+
validations: {
|
|
169
|
+
name: {
|
|
170
|
+
required: true,
|
|
171
|
+
length: 1..100
|
|
172
|
+
},
|
|
173
|
+
email: {
|
|
174
|
+
required: true,
|
|
175
|
+
format: /@/
|
|
176
|
+
},
|
|
177
|
+
age: {
|
|
178
|
+
range: 0..150
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Database Support
|
|
185
|
+
|
|
186
|
+
### PostgreSQL
|
|
187
|
+
```ruby
|
|
188
|
+
Dorm.configure(
|
|
189
|
+
adapter: :postgresql,
|
|
190
|
+
host: 'localhost',
|
|
191
|
+
dbname: 'myapp',
|
|
192
|
+
user: 'postgres',
|
|
193
|
+
password: 'secret'
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### SQLite3
|
|
198
|
+
```ruby
|
|
199
|
+
Dorm.configure(
|
|
200
|
+
adapter: :sqlite3,
|
|
201
|
+
database: 'myapp.db'
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Philosophy
|
|
206
|
+
|
|
207
|
+
This ORM embraces functional programming principles:
|
|
208
|
+
|
|
209
|
+
- **Immutability**: Records are immutable Data objects
|
|
210
|
+
- **Pure Functions**: Repository methods are pure functions in modules
|
|
211
|
+
- **Error Handling**: Railway-oriented programming with Result monads
|
|
212
|
+
- **Composability**: Operations can be chained and composed
|
|
213
|
+
- **Explicitness**: No hidden state or magic behavior
|
|
214
|
+
|
|
215
|
+
## Requirements
|
|
216
|
+
|
|
217
|
+
- Ruby >= 3.2.0 (for Data class support)
|
|
218
|
+
- Database adapter gem (`pg` for PostgreSQL, `sqlite3` for SQLite)
|
|
219
|
+
|
|
220
|
+
## Contributing
|
|
221
|
+
|
|
222
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/dorm.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rake/testtask'
|
|
4
|
+
|
|
5
|
+
# Default task
|
|
6
|
+
task default: :test
|
|
7
|
+
|
|
8
|
+
# Test task configuration
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << 'test'
|
|
11
|
+
t.libs << 'lib'
|
|
12
|
+
t.test_files = FileList['test/**/*test*.rb']
|
|
13
|
+
t.verbose = true
|
|
14
|
+
t.warning = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Unit tests only
|
|
18
|
+
Rake::TestTask.new(:test_unit) do |t|
|
|
19
|
+
t.libs << 'test'
|
|
20
|
+
t.libs << 'lib'
|
|
21
|
+
t.test_files = FileList['test/unit/*test*.rb']
|
|
22
|
+
t.verbose = true
|
|
23
|
+
t.warning = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Integration tests only
|
|
27
|
+
Rake::TestTask.new(:test_integration) do |t|
|
|
28
|
+
t.libs << 'test'
|
|
29
|
+
t.libs << 'lib'
|
|
30
|
+
t.test_files = FileList['test/integration/*test*.rb']
|
|
31
|
+
t.verbose = true
|
|
32
|
+
t.warning = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Run specific test file
|
|
36
|
+
# Usage: rake test_file TEST=test/unit/test_result.rb
|
|
37
|
+
Rake::TestTask.new(:test_file) do |t|
|
|
38
|
+
t.libs << 'test'
|
|
39
|
+
t.libs << 'lib'
|
|
40
|
+
t.test_files = FileList[ENV['TEST']] if ENV['TEST']
|
|
41
|
+
t.verbose = true
|
|
42
|
+
t.warning = false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Test coverage (if you add simplecov)
|
|
46
|
+
desc "Run tests with coverage"
|
|
47
|
+
task :test_coverage do
|
|
48
|
+
ENV['COVERAGE'] = 'true'
|
|
49
|
+
Rake::Task[:test].invoke
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Lint code (if you have rubocop)
|
|
53
|
+
task :lint do
|
|
54
|
+
sh 'rubocop lib/ test/' if system('which rubocop > /dev/null 2>&1')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Clean up test artifacts
|
|
58
|
+
task :clean do
|
|
59
|
+
rm_f 'test.db'
|
|
60
|
+
rm_f 'coverage/'
|
|
61
|
+
rm_f '.coverage_results'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
desc "Run all quality checks"
|
|
65
|
+
task quality: [:lint, :test]
|
|
66
|
+
|
|
67
|
+
# Documentation generation
|
|
68
|
+
task :doc do
|
|
69
|
+
sh 'yard doc' if system('which yard > /dev/null 2>&1')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Show available tasks
|
|
73
|
+
desc "Show test statistics"
|
|
74
|
+
task :test_stats do
|
|
75
|
+
unit_tests = FileList['test/unit/*test*.rb'].count
|
|
76
|
+
integration_tests = FileList['test/integration/*test*.rb'].count
|
|
77
|
+
total_tests = unit_tests + integration_tests
|
|
78
|
+
|
|
79
|
+
puts "Test Statistics:"
|
|
80
|
+
puts " Unit tests: #{unit_tests}"
|
|
81
|
+
puts " Integration tests: #{integration_tests}"
|
|
82
|
+
puts " Total test files: #{total_tests}"
|
|
83
|
+
|
|
84
|
+
# Count individual test methods
|
|
85
|
+
test_methods = 0
|
|
86
|
+
FileList['test/**/*test*.rb'].each do |file|
|
|
87
|
+
content = File.read(file)
|
|
88
|
+
test_methods += content.scan(/def test_\w+/).count
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
puts " Total test methods: #{test_methods}"
|
|
92
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Connection Pool Usage Examples
|
|
2
|
+
|
|
3
|
+
require "dorm"
|
|
4
|
+
|
|
5
|
+
# Configure with connection pooling
|
|
6
|
+
Dorm.configure(
|
|
7
|
+
adapter: :postgresql,
|
|
8
|
+
host: "localhost",
|
|
9
|
+
dbname: "myapp_production",
|
|
10
|
+
user: "postgres",
|
|
11
|
+
password: "secret",
|
|
12
|
+
|
|
13
|
+
# Pool configuration
|
|
14
|
+
pool_size: 10, # Maximum connections in pool
|
|
15
|
+
pool_timeout: 5, # Seconds to wait for connection
|
|
16
|
+
max_connection_age: 3600, # 1 hour - connections older than this get reaped
|
|
17
|
+
max_idle_time: 300, # 5 minutes - idle connections get reaped
|
|
18
|
+
reap_frequency: 60 # 1 minute - how often to check for stale connections
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# For SQLite3 with pooling (useful for testing)
|
|
22
|
+
Dorm.configure(
|
|
23
|
+
adapter: :sqlite3,
|
|
24
|
+
database: "myapp.db",
|
|
25
|
+
pool_size: 3, # SQLite doesn't need many connections
|
|
26
|
+
pool_timeout: 2
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Usage is exactly the same - pooling is transparent
|
|
30
|
+
User = Data.define(:id, :name, :email, :created_at, :updated_at)
|
|
31
|
+
Users = Dorm.repository_for(User, validations: {
|
|
32
|
+
name: { required: true },
|
|
33
|
+
email: { required: true, format: /@/ }
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
# All operations automatically use the pool
|
|
37
|
+
user_result = Users.create(name: "Alice", email: "alice@example.com")
|
|
38
|
+
|
|
39
|
+
if user_result.success?
|
|
40
|
+
puts "Created user with pooled connection!"
|
|
41
|
+
|
|
42
|
+
# Multiple concurrent operations will use different connections from pool
|
|
43
|
+
threads = 10.times.map do |i|
|
|
44
|
+
Thread.new do
|
|
45
|
+
Users.create(name: "User #{i}", email: "user#{i}@example.com")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
results = threads.map(&:join).map(&:value)
|
|
50
|
+
successful = results.count(&:success?)
|
|
51
|
+
puts "Successfully created #{successful} users concurrently"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Monitor pool health
|
|
55
|
+
stats = Dorm::Database.pool_stats
|
|
56
|
+
puts "Pool stats: #{stats}"
|
|
57
|
+
# => Pool stats: {:size=>3, :available=>2, :checked_out=>1, :adapter=>:postgresql}
|
|
58
|
+
|
|
59
|
+
# Graceful shutdown - disconnect all connections
|
|
60
|
+
at_exit do
|
|
61
|
+
Dorm::Database.disconnect!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Example with error handling and pool exhaustion
|
|
65
|
+
begin
|
|
66
|
+
# This will timeout if pool is exhausted
|
|
67
|
+
user = Users.find(1).value
|
|
68
|
+
rescue Dorm::ConnectionPool::ConnectionTimeoutError => e
|
|
69
|
+
puts "Pool exhausted: #{e.message}"
|
|
70
|
+
rescue Dorm::ConnectionPool::PoolExhaustedError => e
|
|
71
|
+
puts "No connections available: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Example: Custom pool monitoring
|
|
75
|
+
class PoolMonitor
|
|
76
|
+
def self.log_stats
|
|
77
|
+
stats = Dorm::Database.pool_stats
|
|
78
|
+
puts "[#{Time.now}] Pool: #{stats[:checked_out]}/#{stats[:size]} connections in use"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Log pool stats every 30 seconds
|
|
83
|
+
Thread.new do
|
|
84
|
+
loop do
|
|
85
|
+
sleep(30)
|
|
86
|
+
PoolMonitor.log_stats
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Query Builder Usage Examples
|
|
2
|
+
|
|
3
|
+
require 'dorm'
|
|
4
|
+
|
|
5
|
+
# Setup
|
|
6
|
+
User = Data.define(:id, :name, :email, :age, :created_at, :updated_at)
|
|
7
|
+
Post = Data.define(:id, :title, :body, :user_id, :status, :created_at, :updated_at)
|
|
8
|
+
|
|
9
|
+
Users = Dorm.repository_for(User)
|
|
10
|
+
Posts = Dorm.repository_for(Post)
|
|
11
|
+
|
|
12
|
+
# === BASIC QUERIES ===
|
|
13
|
+
|
|
14
|
+
# Simple where conditions
|
|
15
|
+
active_users = Users.query
|
|
16
|
+
.where(status: 'active')
|
|
17
|
+
.to_a
|
|
18
|
+
|
|
19
|
+
# Multiple conditions (hash style)
|
|
20
|
+
young_active_users = Users.query
|
|
21
|
+
.where(status: 'active', age: 18..30)
|
|
22
|
+
.to_a
|
|
23
|
+
|
|
24
|
+
# Raw SQL conditions
|
|
25
|
+
power_users = Users.query
|
|
26
|
+
.where_raw("post_count > ? AND last_login > ?", 10, 1.week.ago)
|
|
27
|
+
.to_a
|
|
28
|
+
|
|
29
|
+
# === DSL WHERE CONDITIONS ===
|
|
30
|
+
|
|
31
|
+
# Elegant DSL syntax
|
|
32
|
+
sophisticated_query = Users.query
|
|
33
|
+
.where { name.like("%admin%").and(age.gt(21)) }
|
|
34
|
+
.to_a
|
|
35
|
+
|
|
36
|
+
# Complex conditions with OR
|
|
37
|
+
complex_users = Users.query
|
|
38
|
+
.where { name.eq("Alice").or(email.like("%@admin.com")) }
|
|
39
|
+
.to_a
|
|
40
|
+
|
|
41
|
+
# IN conditions
|
|
42
|
+
specific_users = Users.query
|
|
43
|
+
.where { id.in([1, 2, 3, 4, 5]) }
|
|
44
|
+
.to_a
|
|
45
|
+
|
|
46
|
+
# NULL checks
|
|
47
|
+
incomplete_profiles = Users.query
|
|
48
|
+
.where { email.null.or(name.null) }
|
|
49
|
+
.to_a
|
|
50
|
+
|
|
51
|
+
# === SELECT AND PROJECTION ===
|
|
52
|
+
|
|
53
|
+
# Select specific fields
|
|
54
|
+
user_emails = Users.query
|
|
55
|
+
.select(:name, :email)
|
|
56
|
+
.where(status: 'active')
|
|
57
|
+
.to_a
|
|
58
|
+
|
|
59
|
+
# Raw select with calculations
|
|
60
|
+
user_stats = Users.query
|
|
61
|
+
.select_raw("name, email, EXTRACT(year FROM created_at) as signup_year")
|
|
62
|
+
.to_a
|
|
63
|
+
|
|
64
|
+
# === JOINS ===
|
|
65
|
+
|
|
66
|
+
# Inner join with automatic field mapping
|
|
67
|
+
posts_with_authors = Posts.query
|
|
68
|
+
.join(:users, user_id: :id)
|
|
69
|
+
.select("posts.*", "users.name as author_name")
|
|
70
|
+
.to_a
|
|
71
|
+
|
|
72
|
+
# Left join with custom condition
|
|
73
|
+
all_posts_with_optional_authors = Posts.query
|
|
74
|
+
.left_join(:users, "users.id = posts.user_id")
|
|
75
|
+
.to_a
|
|
76
|
+
|
|
77
|
+
# Multiple joins
|
|
78
|
+
posts_with_comments = Posts.query
|
|
79
|
+
.join(:users, user_id: :id)
|
|
80
|
+
.left_join(:comments, "comments.post_id = posts.id")
|
|
81
|
+
.group_by("posts.id", "users.name")
|
|
82
|
+
.select_raw("posts.*, users.name as author, COUNT(comments.id) as comment_count")
|
|
83
|
+
.to_a
|
|
84
|
+
|
|
85
|
+
# === ORDERING AND LIMITING ===
|
|
86
|
+
|
|
87
|
+
# Order by single field
|
|
88
|
+
recent_posts = Posts.query
|
|
89
|
+
.order_by(:created_at => :desc)
|
|
90
|
+
.limit(10)
|
|
91
|
+
.to_a
|
|
92
|
+
|
|
93
|
+
# Multiple order fields
|
|
94
|
+
sorted_users = Users.query
|
|
95
|
+
.order_by(:status, :name => :asc)
|
|
96
|
+
.to_a
|
|
97
|
+
|
|
98
|
+
# Pagination
|
|
99
|
+
page_2_users = Users.query
|
|
100
|
+
.page(2, per_page: 20) # page 2, 20 per page
|
|
101
|
+
.to_a
|
|
102
|
+
|
|
103
|
+
# === AGGREGATIONS ===
|
|
104
|
+
|
|
105
|
+
# Count records
|
|
106
|
+
user_count = Users.query
|
|
107
|
+
.where(status: 'active')
|
|
108
|
+
.count
|
|
109
|
+
.value_or(0)
|
|
110
|
+
|
|
111
|
+
# Sum, average, min, max
|
|
112
|
+
stats = {
|
|
113
|
+
total_age: Users.query.sum(:age).value_or(0),
|
|
114
|
+
avg_age: Users.query.avg(:age).value_or(0),
|
|
115
|
+
oldest: Users.query.max(:age).value_or(0),
|
|
116
|
+
youngest: Users.query.min(:age).value_or(0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Group by with aggregation
|
|
120
|
+
posts_by_status = Posts.query
|
|
121
|
+
.group_by(:status)
|
|
122
|
+
.select_raw("status, COUNT(*) as post_count")
|
|
123
|
+
.to_a
|
|
124
|
+
|
|
125
|
+
# Having clause
|
|
126
|
+
popular_authors = Posts.query
|
|
127
|
+
.join(:users, user_id: :id)
|
|
128
|
+
.group_by("users.id", "users.name")
|
|
129
|
+
.having("COUNT(posts.id) > ?", 5)
|
|
130
|
+
.select_raw("users.name, COUNT(posts.id) as post_count")
|
|
131
|
+
.to_a
|
|
132
|
+
|
|
133
|
+
# === EXECUTION METHODS ===
|
|
134
|
+
|
|
135
|
+
# Convert to array (execute immediately)
|
|
136
|
+
users_array = Users.query.where(status: 'active').to_a
|
|
137
|
+
|
|
138
|
+
# Get first result
|
|
139
|
+
first_admin = Users.query
|
|
140
|
+
.where { name.like("%admin%") }
|
|
141
|
+
.first
|
|
142
|
+
|
|
143
|
+
if first_admin.success?
|
|
144
|
+
puts "Found admin: #{first_admin.value.name}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Check if any records exist
|
|
148
|
+
has_active_users = Users.query
|
|
149
|
+
.where(status: 'active')
|
|
150
|
+
.exists?
|
|
151
|
+
.value_or(false)
|
|
152
|
+
|
|
153
|
+
# Get raw SQL for debugging
|
|
154
|
+
sql = Users.query
|
|
155
|
+
.where(status: 'active')
|
|
156
|
+
.join(:posts, user_id: :id)
|
|
157
|
+
.limit(10)
|
|
158
|
+
.to_sql
|
|
159
|
+
|
|
160
|
+
puts "Generated SQL: #{sql}"
|
|
161
|
+
|
|
162
|
+
# === FUNCTIONAL COMPOSITION ===
|
|
163
|
+
|
|
164
|
+
# Chain query building functionally
|
|
165
|
+
build_user_query = ->(status, min_age) {
|
|
166
|
+
Users.query
|
|
167
|
+
.where(status: status)
|
|
168
|
+
.where { age.gte(min_age) }
|
|
169
|
+
.order_by(:name)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Use the builder
|
|
173
|
+
active_adults = build_user_query.call('active', 18).to_a
|
|
174
|
+
inactive_seniors = build_user_query.call('inactive', 65).limit(5).to_a
|
|
175
|
+
|
|
176
|
+
# === RESULT HANDLING ===
|
|
177
|
+
|
|
178
|
+
# All query results are wrapped in Result monads
|
|
179
|
+
Users.query
|
|
180
|
+
.where(id: 999)
|
|
181
|
+
.first
|
|
182
|
+
.bind { |user| Posts.query.where(user_id: user.id).to_a }
|
|
183
|
+
.map { |posts| posts.map(&:title) }
|
|
184
|
+
.value_or([])
|
|
185
|
+
|
|
186
|
+
# === COMPLEX EXAMPLE ===
|
|
187
|
+
|
|
188
|
+
# Find popular posts by active users with recent activity
|
|
189
|
+
popular_recent_posts = Posts.query
|
|
190
|
+
.join(:users, user_id: :id)
|
|
191
|
+
.where { status.eq('published') }
|
|
192
|
+
.where { created_at.gte(1.month.ago) }
|
|
193
|
+
.where("users.status = ?", 'active')
|
|
194
|
+
.left_join(:comments, "comments.post_id = posts.id")
|
|
195
|
+
.group_by("posts.id", "posts.title", "users.name")
|
|
196
|
+
.having("COUNT(comments.id) > ?", 5)
|
|
197
|
+
.order_by(created_at: :desc)
|
|
198
|
+
.select_raw("posts.*, users.name as author, COUNT(comments.id) as comment_count")
|
|
199
|
+
.limit(20)
|
|
200
|
+
.to_a
|
|
201
|
+
|
|
202
|
+
puts "Found #{popular_recent_posts.length} popular recent posts"
|