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
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dorm
|
|
4
|
+
class QueryBuilder
|
|
5
|
+
attr_reader :table_name, :data_class
|
|
6
|
+
|
|
7
|
+
def initialize(table_name, data_class)
|
|
8
|
+
@table_name = table_name
|
|
9
|
+
@data_class = data_class
|
|
10
|
+
@select_fields = ["#{table_name}.*"]
|
|
11
|
+
@joins = []
|
|
12
|
+
@where_conditions = []
|
|
13
|
+
@group_by_fields = []
|
|
14
|
+
@having_conditions = []
|
|
15
|
+
@order_by_fields = []
|
|
16
|
+
@limit_value = nil
|
|
17
|
+
@offset_value = nil
|
|
18
|
+
@params = []
|
|
19
|
+
@param_counter = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# SELECT methods
|
|
23
|
+
def select(*fields)
|
|
24
|
+
clone.tap do |query|
|
|
25
|
+
if fields.empty?
|
|
26
|
+
query.instance_variable_set(:@select_fields, ["#{table_name}.*"])
|
|
27
|
+
else
|
|
28
|
+
formatted_fields = fields.map do |field|
|
|
29
|
+
case field
|
|
30
|
+
when Symbol
|
|
31
|
+
"#{table_name}.#{field}"
|
|
32
|
+
when String
|
|
33
|
+
field.include?(".") ? field : "#{table_name}.#{field}"
|
|
34
|
+
else
|
|
35
|
+
field.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
query.instance_variable_set(:@select_fields, formatted_fields)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def select_raw(sql)
|
|
44
|
+
clone.tap do |query|
|
|
45
|
+
query.instance_variable_set(:@select_fields, [sql])
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# WHERE methods
|
|
50
|
+
def where(conditions = nil, **kwargs, &block)
|
|
51
|
+
clone.tap do |query|
|
|
52
|
+
if block_given?
|
|
53
|
+
# DSL block: where { name.eq("Alice").and(age.gt(18)) }
|
|
54
|
+
dsl = WhereDSL.new(table_name, query.instance_variable_get(:@param_counter))
|
|
55
|
+
condition = dsl.instance_eval(&block)
|
|
56
|
+
query.instance_variable_get(:@where_conditions) << [condition.to_sql, condition.params]
|
|
57
|
+
query.instance_variable_set(:@param_counter,
|
|
58
|
+
query.instance_variable_get(:@param_counter) + condition.params.length)
|
|
59
|
+
elsif conditions.is_a?(Hash) || !kwargs.empty?
|
|
60
|
+
# Hash conditions: where(name: "Alice", age: 25)
|
|
61
|
+
hash_conditions = conditions.is_a?(Hash) ? conditions : kwargs
|
|
62
|
+
query.add_hash_conditions(hash_conditions)
|
|
63
|
+
elsif conditions.is_a?(String)
|
|
64
|
+
# Raw SQL: where("name = ? AND age > ?", "Alice", 18)
|
|
65
|
+
query.add_where_condition(conditions, [])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def where_raw(sql, *params)
|
|
71
|
+
clone.tap do |query|
|
|
72
|
+
query.add_where_condition(sql, params)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# JOIN methods
|
|
77
|
+
def join(table, condition = nil, **kwargs)
|
|
78
|
+
add_join("INNER JOIN", table, condition, kwargs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def left_join(table, condition = nil, **kwargs)
|
|
82
|
+
add_join("LEFT JOIN", table, condition, kwargs)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def right_join(table, condition = nil, **kwargs)
|
|
86
|
+
add_join("RIGHT JOIN", table, condition, kwargs)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def inner_join(table, condition = nil, **kwargs)
|
|
90
|
+
add_join("INNER JOIN", table, condition, kwargs)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# GROUP BY and HAVING
|
|
94
|
+
def group_by(*fields)
|
|
95
|
+
clone.tap do |query|
|
|
96
|
+
formatted_fields = fields.map { |f| format_field(f) }
|
|
97
|
+
query.instance_variable_set(:@group_by_fields,
|
|
98
|
+
query.instance_variable_get(:@group_by_fields) + formatted_fields)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def having(condition, *params)
|
|
103
|
+
clone.tap do |query|
|
|
104
|
+
query.instance_variable_get(:@having_conditions) << [condition, params]
|
|
105
|
+
query.instance_variable_set(:@param_counter,
|
|
106
|
+
query.instance_variable_get(:@param_counter) + params.length)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ORDER BY
|
|
111
|
+
def order_by(*fields)
|
|
112
|
+
clone.tap do |query|
|
|
113
|
+
formatted_fields = fields.map do |field|
|
|
114
|
+
case field
|
|
115
|
+
when Hash
|
|
116
|
+
field.map { |f, direction| "#{format_field(f)} #{direction.to_s.upcase}" }.join(", ")
|
|
117
|
+
else
|
|
118
|
+
format_field(field)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
query.instance_variable_set(:@order_by_fields, formatted_fields)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def order(field, direction = :asc)
|
|
126
|
+
order_by(field => direction)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# LIMIT and OFFSET
|
|
130
|
+
def limit(count)
|
|
131
|
+
clone.tap do |query|
|
|
132
|
+
query.instance_variable_set(:@limit_value, count)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def offset(count)
|
|
137
|
+
clone.tap do |query|
|
|
138
|
+
query.instance_variable_set(:@offset_value, count)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Pagination helpers
|
|
143
|
+
def page(page_num, per_page = 20)
|
|
144
|
+
offset_count = (page_num - 1) * per_page
|
|
145
|
+
limit(per_page).offset(offset_count)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Execution methods
|
|
149
|
+
def to_sql
|
|
150
|
+
build_sql
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def to_a
|
|
154
|
+
execute.value_or([])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def first
|
|
158
|
+
limit(1).execute.bind do |results|
|
|
159
|
+
result = results.first
|
|
160
|
+
result ? Result.success(result) : Result.failure("No records found")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def count
|
|
165
|
+
select_raw("COUNT(*) as count")
|
|
166
|
+
.limit(nil)
|
|
167
|
+
.offset(nil)
|
|
168
|
+
.execute
|
|
169
|
+
.map { |results| results.first&.[]("count")&.to_i || 0 }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def exists?
|
|
173
|
+
select_raw("1")
|
|
174
|
+
.limit(1)
|
|
175
|
+
.execute
|
|
176
|
+
.map { |results| !results.empty? }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def execute
|
|
180
|
+
Result.try do
|
|
181
|
+
sql, params = build_sql_with_params
|
|
182
|
+
result = Database.query(sql, params)
|
|
183
|
+
|
|
184
|
+
case result
|
|
185
|
+
when ->(r) { r.respond_to?(:map) }
|
|
186
|
+
result.map { |row| row_to_data(row) }
|
|
187
|
+
else
|
|
188
|
+
[row_to_data(result)]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Aggregation methods
|
|
194
|
+
def sum(field)
|
|
195
|
+
select_raw("SUM(#{format_field(field)}) as sum")
|
|
196
|
+
.execute
|
|
197
|
+
.map { |results| results.first&.[]("sum")&.to_f || 0 }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def avg(field)
|
|
201
|
+
select_raw("AVG(#{format_field(field)}) as avg")
|
|
202
|
+
.execute
|
|
203
|
+
.map { |results| results.first&.[]("avg")&.to_f || 0 }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def max(field)
|
|
207
|
+
select_raw("MAX(#{format_field(field)}) as max")
|
|
208
|
+
.execute
|
|
209
|
+
.map { |results| results.first&.[]("max") }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def min(field)
|
|
213
|
+
select_raw("MIN(#{format_field(field)}) as min")
|
|
214
|
+
.execute
|
|
215
|
+
.map { |results| results.first&.[]("min") }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def add_where_condition(condition, params)
|
|
219
|
+
@where_conditions << [condition, params]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def add_hash_conditions(hash)
|
|
223
|
+
hash.each do |field, value|
|
|
224
|
+
if value.is_a?(Array)
|
|
225
|
+
placeholders = value.map { next_placeholder }.join(", ")
|
|
226
|
+
@where_conditions << ["#{format_field(field)} IN (#{placeholders})", value]
|
|
227
|
+
elsif value.is_a?(Range)
|
|
228
|
+
condition = "#{format_field(field)} BETWEEN #{next_placeholder} AND #{next_placeholder}"
|
|
229
|
+
@where_conditions << [condition, [value.begin, value.end]]
|
|
230
|
+
elsif value.nil?
|
|
231
|
+
@where_conditions << ["#{format_field(field)} IS NULL", []]
|
|
232
|
+
else
|
|
233
|
+
condition = "#{format_field(field)} = #{next_placeholder}"
|
|
234
|
+
@where_conditions << [condition, [value]]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
def clone
|
|
242
|
+
Marshal.load(Marshal.dump(self))
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def add_join(join_type, table, condition, kwargs)
|
|
246
|
+
clone.tap do |query|
|
|
247
|
+
if condition
|
|
248
|
+
join_clause = "#{join_type} #{table} ON #{condition}"
|
|
249
|
+
elsif kwargs.any?
|
|
250
|
+
# Auto-generate join condition from kwargs
|
|
251
|
+
conditions = kwargs.map do |local_field, foreign_field|
|
|
252
|
+
"#{table_name}.#{local_field} = #{table}.#{foreign_field}"
|
|
253
|
+
end.join(" AND ")
|
|
254
|
+
join_clause = "#{join_type} #{table} ON #{conditions}"
|
|
255
|
+
else
|
|
256
|
+
raise ArgumentError, "Join requires either condition or field mapping"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
query.instance_variable_get(:@joins) << join_clause
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def format_field(field)
|
|
264
|
+
case field
|
|
265
|
+
when Symbol
|
|
266
|
+
"#{table_name}.#{field}"
|
|
267
|
+
when String
|
|
268
|
+
field.include?(".") ? field : "#{table_name}.#{field}"
|
|
269
|
+
else
|
|
270
|
+
field.to_s
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def next_placeholder
|
|
275
|
+
@param_counter += 1
|
|
276
|
+
"$#{@param_counter}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def build_sql_with_params
|
|
280
|
+
sql = build_sql
|
|
281
|
+
params = collect_params
|
|
282
|
+
[sql, params]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def build_sql
|
|
286
|
+
parts = []
|
|
287
|
+
|
|
288
|
+
parts << "SELECT #{@select_fields.join(", ")}"
|
|
289
|
+
parts << "FROM #{table_name}"
|
|
290
|
+
parts.concat(@joins) if @joins.any?
|
|
291
|
+
|
|
292
|
+
if @where_conditions.any?
|
|
293
|
+
where_clause = @where_conditions.map { |condition, _| condition }.join(" AND ")
|
|
294
|
+
parts << "WHERE #{where_clause}"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
parts << "GROUP BY #{@group_by_fields.join(", ")}" if @group_by_fields.any?
|
|
298
|
+
|
|
299
|
+
if @having_conditions.any?
|
|
300
|
+
having_clause = @having_conditions.map { |condition, _| condition }.join(" AND ")
|
|
301
|
+
parts << "HAVING #{having_clause}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
parts << "ORDER BY #{@order_by_fields.join(", ")}" if @order_by_fields.any?
|
|
305
|
+
parts << "LIMIT #{@limit_value}" if @limit_value
|
|
306
|
+
parts << "OFFSET #{@offset_value}" if @offset_value
|
|
307
|
+
|
|
308
|
+
parts.join(" ")
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def collect_params
|
|
312
|
+
params = []
|
|
313
|
+
@where_conditions.each { |_, condition_params| params.concat(condition_params) }
|
|
314
|
+
@having_conditions.each { |_, condition_params| params.concat(condition_params) }
|
|
315
|
+
params
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def row_to_data(row)
|
|
319
|
+
return row unless @data_class && @select_fields == ["#{table_name}.*"]
|
|
320
|
+
|
|
321
|
+
attrs = {}
|
|
322
|
+
@data_class.members.each do |col|
|
|
323
|
+
attrs[col] = deserialize_value(col, row[col.to_s])
|
|
324
|
+
end
|
|
325
|
+
@data_class.new(**attrs)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def deserialize_value(column, value)
|
|
329
|
+
return nil if value.nil?
|
|
330
|
+
|
|
331
|
+
case column
|
|
332
|
+
when :id, /.*_id$/
|
|
333
|
+
value.to_i
|
|
334
|
+
when :created_at, :updated_at
|
|
335
|
+
Time.parse(value.to_s)
|
|
336
|
+
else
|
|
337
|
+
value
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# DSL for building WHERE conditions
|
|
343
|
+
class WhereDSL
|
|
344
|
+
attr_reader :params
|
|
345
|
+
|
|
346
|
+
def initialize(table_name, param_counter)
|
|
347
|
+
@table_name = table_name
|
|
348
|
+
@param_counter = param_counter
|
|
349
|
+
@params = []
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def method_missing(field_name, *args)
|
|
353
|
+
FieldCondition.new(field_name, @table_name, self)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def add_param(value)
|
|
357
|
+
@params << value
|
|
358
|
+
@param_counter += 1
|
|
359
|
+
"$#{@param_counter}"
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
class FieldCondition
|
|
363
|
+
def initialize(field, table_name, dsl)
|
|
364
|
+
@field = field
|
|
365
|
+
@table_name = table_name
|
|
366
|
+
@dsl = dsl
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def eq(value)
|
|
370
|
+
Condition.new("#{@table_name}.#{@field} = #{@dsl.add_param(value)}", @dsl.params.dup)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def ne(value)
|
|
374
|
+
Condition.new("#{@table_name}.#{@field} != #{@dsl.add_param(value)}", @dsl.params.dup)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def gt(value)
|
|
378
|
+
Condition.new("#{@table_name}.#{@field} > #{@dsl.add_param(value)}", @dsl.params.dup)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def gte(value)
|
|
382
|
+
Condition.new("#{@table_name}.#{@field} >= #{@dsl.add_param(value)}", @dsl.params.dup)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def lt(value)
|
|
386
|
+
Condition.new("#{@table_name}.#{@field} < #{@dsl.add_param(value)}", @dsl.params.dup)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def lte(value)
|
|
390
|
+
Condition.new("#{@table_name}.#{@field} <= #{@dsl.add_param(value)}", @dsl.params.dup)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def like(pattern)
|
|
394
|
+
Condition.new("#{@table_name}.#{@field} LIKE #{@dsl.add_param(pattern)}", @dsl.params.dup)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def in(values)
|
|
398
|
+
placeholders = values.map { |v| @dsl.add_param(v) }.join(", ")
|
|
399
|
+
Condition.new("#{@table_name}.#{@field} IN (#{placeholders})", @dsl.params.dup)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def null
|
|
403
|
+
Condition.new("#{@table_name}.#{@field} IS NULL", @dsl.params.dup)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def not_null
|
|
407
|
+
Condition.new("#{@table_name}.#{@field} IS NOT NULL", @dsl.params.dup)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
class Condition
|
|
412
|
+
attr_reader :sql, :params
|
|
413
|
+
|
|
414
|
+
def initialize(sql, params)
|
|
415
|
+
@sql = sql
|
|
416
|
+
@params = params
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def and(other_condition)
|
|
420
|
+
combined_params = @params + other_condition.params
|
|
421
|
+
Condition.new("(#{@sql}) AND (#{other_condition.sql})", combined_params)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def or(other_condition)
|
|
425
|
+
combined_params = @params + other_condition.params
|
|
426
|
+
Condition.new("(#{@sql}) OR (#{other_condition.sql})", combined_params)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def to_sql
|
|
430
|
+
@sql
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|