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 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
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
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"