querykit 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/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +179 -0
- data/lib/querykit/adapters/adapter.rb +28 -0
- data/lib/querykit/adapters/mysql_adapter.rb +44 -0
- data/lib/querykit/adapters/postgresql_adapter.rb +50 -0
- data/lib/querykit/adapters/sqlite_adapter.rb +43 -0
- data/lib/querykit/case_builder.rb +102 -0
- data/lib/querykit/configuration.rb +160 -0
- data/lib/querykit/connection.rb +211 -0
- data/lib/querykit/delete_query.rb +54 -0
- data/lib/querykit/extensions/case_when.rb +30 -0
- data/lib/querykit/insert_query.rb +58 -0
- data/lib/querykit/query.rb +473 -0
- data/lib/querykit/repository.rb +182 -0
- data/lib/querykit/update_query.rb +59 -0
- data/lib/querykit/version.rb +5 -0
- data/lib/querykit.rb +110 -0
- data/sig/adapters/adapter.rbs +14 -0
- data/sig/adapters/mysql_adapter.rbs +15 -0
- data/sig/adapters/postgresql_adapter.rbs +15 -0
- data/sig/adapters/sqlite_adapter.rbs +15 -0
- data/sig/case_builder.rbs +23 -0
- data/sig/configuration.rbs +22 -0
- data/sig/connection.rbs +36 -0
- data/sig/delete_query.rbs +22 -0
- data/sig/extensions/case_when.rbs +10 -0
- data/sig/insert_query.rbs +19 -0
- data/sig/query.rbs +83 -0
- data/sig/querykit.rbs +25 -0
- data/sig/repository.rbs +46 -0
- data/sig/update_query.rbs +25 -0
- metadata +120 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QueryKit
|
|
4
|
+
# Query builder for constructing SQL SELECT statements.
|
|
5
|
+
#
|
|
6
|
+
# Provides a fluent, chainable API for building complex SQL queries
|
|
7
|
+
# with automatic parameter binding for security.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic query
|
|
10
|
+
# query = Query.new('users')
|
|
11
|
+
# .select('id', 'name', 'email')
|
|
12
|
+
# .where('age', '>', 18)
|
|
13
|
+
# .order_by('name')
|
|
14
|
+
# .limit(10)
|
|
15
|
+
#
|
|
16
|
+
# @example Complex query with joins
|
|
17
|
+
# query = Query.new('users')
|
|
18
|
+
# .join('posts', 'users.id', 'posts.user_id')
|
|
19
|
+
# .where('users.active', true)
|
|
20
|
+
# .where('posts.published', true)
|
|
21
|
+
# .select('users.*', 'COUNT(posts.id) as post_count')
|
|
22
|
+
# .group_by('users.id')
|
|
23
|
+
class Query
|
|
24
|
+
# @return [String, nil] the table name for this query
|
|
25
|
+
attr_reader :table
|
|
26
|
+
|
|
27
|
+
# @return [Array<Hash>] the WHERE conditions
|
|
28
|
+
attr_reader :wheres
|
|
29
|
+
|
|
30
|
+
# @return [Array<String>] the columns to select
|
|
31
|
+
attr_reader :selects
|
|
32
|
+
|
|
33
|
+
# @return [Array<Hash>] the JOIN clauses
|
|
34
|
+
attr_reader :joins
|
|
35
|
+
|
|
36
|
+
# @return [Array<String>] the ORDER BY clauses
|
|
37
|
+
attr_reader :orders
|
|
38
|
+
|
|
39
|
+
# @return [Array<String>] the GROUP BY columns
|
|
40
|
+
attr_reader :groups
|
|
41
|
+
|
|
42
|
+
# @return [Integer, nil] the LIMIT value
|
|
43
|
+
attr_reader :limit_value
|
|
44
|
+
|
|
45
|
+
# @return [Integer, nil] the OFFSET value
|
|
46
|
+
attr_reader :offset_value
|
|
47
|
+
|
|
48
|
+
# @return [Array] the parameter bindings for safe query execution
|
|
49
|
+
attr_accessor :bindings
|
|
50
|
+
|
|
51
|
+
# Initialize a new Query instance.
|
|
52
|
+
#
|
|
53
|
+
# @param table [String, nil] the table name to query
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# query = Query.new('users')
|
|
57
|
+
# query = Query.new # table can be set later with from()
|
|
58
|
+
def initialize(table = nil)
|
|
59
|
+
@table = table
|
|
60
|
+
@selects = []
|
|
61
|
+
@wheres = []
|
|
62
|
+
@joins = []
|
|
63
|
+
@orders = []
|
|
64
|
+
@groups = []
|
|
65
|
+
@havings = []
|
|
66
|
+
@limit_value = nil
|
|
67
|
+
@offset_value = nil
|
|
68
|
+
@bindings = []
|
|
69
|
+
@distinct = false
|
|
70
|
+
@unions = []
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Set the table name for this query.
|
|
74
|
+
#
|
|
75
|
+
# @param table [String] the table name
|
|
76
|
+
# @return [Query] self for method chaining
|
|
77
|
+
#
|
|
78
|
+
# @example
|
|
79
|
+
# query.from('users')
|
|
80
|
+
def from(table)
|
|
81
|
+
@table = table
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Specify columns to select.
|
|
86
|
+
#
|
|
87
|
+
# @param columns [Array<String>] column names to select. Defaults to '*' if none provided.
|
|
88
|
+
# @return [Query] self for method chaining
|
|
89
|
+
#
|
|
90
|
+
# @example Select specific columns
|
|
91
|
+
# query.select('id', 'name', 'email')
|
|
92
|
+
#
|
|
93
|
+
# @example Select with aliases
|
|
94
|
+
# query.select('users.id', 'users.name as user_name')
|
|
95
|
+
#
|
|
96
|
+
# @example Select all columns (default)
|
|
97
|
+
# query.select # equivalent to SELECT *
|
|
98
|
+
def select(*columns)
|
|
99
|
+
columns = ['*'] if columns.empty?
|
|
100
|
+
@selects.concat(columns.flatten)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Set the query to return distinct results.
|
|
105
|
+
#
|
|
106
|
+
# @return [Query] self for method chaining
|
|
107
|
+
#
|
|
108
|
+
# @example
|
|
109
|
+
# query.select('country').distinct
|
|
110
|
+
def distinct
|
|
111
|
+
@distinct = true
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Add a WHERE condition to the query.
|
|
116
|
+
#
|
|
117
|
+
# Supports multiple calling patterns for flexibility.
|
|
118
|
+
# Values are automatically parameterized for SQL injection protection.
|
|
119
|
+
#
|
|
120
|
+
# @overload where(column, value)
|
|
121
|
+
# @param column [String] the column name
|
|
122
|
+
# @param value [Object] the value to compare (assumes = operator)
|
|
123
|
+
# @example
|
|
124
|
+
# query.where('status', 'active')
|
|
125
|
+
#
|
|
126
|
+
# @overload where(column, operator, value)
|
|
127
|
+
# @param column [String] the column name
|
|
128
|
+
# @param operator [String] comparison operator (=, >, <, >=, <=, !=, LIKE)
|
|
129
|
+
# @param value [Object] the value to compare
|
|
130
|
+
# @example
|
|
131
|
+
# query.where('age', '>', 18)
|
|
132
|
+
# query.where('name', 'LIKE', 'John%')
|
|
133
|
+
#
|
|
134
|
+
# @overload where(hash)
|
|
135
|
+
# @param hash [Hash] column-value pairs (all use = operator)
|
|
136
|
+
# @example
|
|
137
|
+
# query.where(status: 'active', country: 'USA')
|
|
138
|
+
#
|
|
139
|
+
# @return [Query] self for method chaining
|
|
140
|
+
def where(column, operator = nil, value = nil)
|
|
141
|
+
# Handle different argument patterns
|
|
142
|
+
if column.is_a?(Hash)
|
|
143
|
+
column.each { |k, v| where(k, '=', v) }
|
|
144
|
+
return self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if value.nil? && !operator.nil?
|
|
148
|
+
value = operator
|
|
149
|
+
operator = '='
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
@wheres << { type: 'basic', column: column, operator: operator, value: value, boolean: 'AND' }
|
|
153
|
+
@bindings << value unless value.nil?
|
|
154
|
+
self
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Add an OR WHERE condition to the query.
|
|
158
|
+
#
|
|
159
|
+
# @param column [String] the column name
|
|
160
|
+
# @param operator [String, Object] the operator or value (if value is nil)
|
|
161
|
+
# @param value [Object, nil] the value to compare
|
|
162
|
+
#
|
|
163
|
+
# @return [Query] self for method chaining
|
|
164
|
+
#
|
|
165
|
+
# @example
|
|
166
|
+
# query.where('status', 'active').or_where('priority', 'high')
|
|
167
|
+
def or_where(column, operator = nil, value = nil)
|
|
168
|
+
if value.nil? && !operator.nil?
|
|
169
|
+
value = operator
|
|
170
|
+
operator = '='
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
@wheres << { type: 'basic', column: column, operator: operator, value: value, boolean: 'OR' }
|
|
174
|
+
@bindings << value unless value.nil?
|
|
175
|
+
self
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# WHERE IN clause
|
|
179
|
+
def where_in(column, values)
|
|
180
|
+
@wheres << { type: 'in', column: column, values: values, boolean: 'AND' }
|
|
181
|
+
@bindings.concat(values)
|
|
182
|
+
self
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# WHERE NOT IN clause
|
|
186
|
+
def where_not_in(column, values)
|
|
187
|
+
@wheres << { type: 'not_in', column: column, values: values, boolean: 'AND' }
|
|
188
|
+
@bindings.concat(values)
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# WHERE IS NULL / IS NOT NULL
|
|
193
|
+
def where_null(column)
|
|
194
|
+
@wheres << { type: 'null', column: column, boolean: 'AND' }
|
|
195
|
+
self
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# WHERE IS NOT NULL
|
|
199
|
+
def where_not_null(column)
|
|
200
|
+
@wheres << { type: 'not_null', column: column, boolean: 'AND' }
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# WHERE BETWEEN
|
|
205
|
+
def where_between(column, min, max)
|
|
206
|
+
@wheres << { type: 'between', column: column, min: min, max: max, boolean: 'AND' }
|
|
207
|
+
@bindings << min << max
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Raw WHERE clause
|
|
212
|
+
def where_raw(sql, *bindings)
|
|
213
|
+
@wheres << { type: 'raw', sql: sql, boolean: 'AND' }
|
|
214
|
+
@bindings.concat(bindings)
|
|
215
|
+
self
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# WHERE EXISTS
|
|
219
|
+
def where_exists(subquery)
|
|
220
|
+
sql = subquery.is_a?(String) ? subquery : subquery.to_sql
|
|
221
|
+
@wheres << { type: 'exists', sql: sql, boolean: 'AND' }
|
|
222
|
+
@bindings.concat(subquery.bindings) if subquery.respond_to?(:bindings)
|
|
223
|
+
self
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# WHERE NOT EXISTS
|
|
227
|
+
def where_not_exists(subquery)
|
|
228
|
+
sql = subquery.is_a?(String) ? subquery : subquery.to_sql
|
|
229
|
+
@wheres << { type: 'not_exists', sql: sql, boolean: 'AND' }
|
|
230
|
+
@bindings.concat(subquery.bindings) if subquery.respond_to?(:bindings)
|
|
231
|
+
self
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# JOIN clauses
|
|
235
|
+
def join(table, first, operator = nil, second = nil)
|
|
236
|
+
if operator.nil?
|
|
237
|
+
operator = '='
|
|
238
|
+
second = first
|
|
239
|
+
end
|
|
240
|
+
@joins << { type: 'INNER', table: table, first: first, operator: operator, second: second }
|
|
241
|
+
self
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# LEFT JOIN
|
|
245
|
+
def left_join(table, first, operator = nil, second = nil)
|
|
246
|
+
if operator.nil?
|
|
247
|
+
operator = '='
|
|
248
|
+
second = first
|
|
249
|
+
end
|
|
250
|
+
@joins << { type: 'LEFT', table: table, first: first, operator: operator, second: second }
|
|
251
|
+
self
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# RIGHT JOIN
|
|
255
|
+
def right_join(table, first, operator = nil, second = nil)
|
|
256
|
+
if operator.nil?
|
|
257
|
+
operator = '='
|
|
258
|
+
second = first
|
|
259
|
+
end
|
|
260
|
+
@joins << { type: 'RIGHT', table: table, first: first, operator: operator, second: second }
|
|
261
|
+
self
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# CROSS JOIN
|
|
265
|
+
def cross_join(table)
|
|
266
|
+
@joins << { type: 'CROSS', table: table }
|
|
267
|
+
self
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# ORDER BY
|
|
271
|
+
def order_by(column, direction = 'ASC')
|
|
272
|
+
@orders << { column: column, direction: direction.upcase }
|
|
273
|
+
self
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Set the query to order results in descending order
|
|
277
|
+
def order_by_desc(column)
|
|
278
|
+
order_by(column, 'DESC')
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# GROUP BY
|
|
282
|
+
def group_by(*columns)
|
|
283
|
+
@groups.concat(columns.flatten)
|
|
284
|
+
self
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# HAVING
|
|
288
|
+
def having(column, operator = nil, value = nil)
|
|
289
|
+
if value.nil? && !operator.nil?
|
|
290
|
+
value = operator
|
|
291
|
+
operator = '='
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
@havings << { column: column, operator: operator, value: value, boolean: 'AND' }
|
|
295
|
+
@bindings << value unless value.nil?
|
|
296
|
+
self
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# LIMIT and OFFSET
|
|
300
|
+
def limit(value)
|
|
301
|
+
@limit_value = value
|
|
302
|
+
self
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Set the query offset
|
|
306
|
+
def offset(value)
|
|
307
|
+
@offset_value = value
|
|
308
|
+
self
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Alias methods for offset and limit
|
|
312
|
+
def skip(value)
|
|
313
|
+
offset(value)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Alias methods for offset and limit
|
|
317
|
+
def take(value)
|
|
318
|
+
limit(value)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Pagination
|
|
322
|
+
def page(page_number, per_page = 15)
|
|
323
|
+
offset((page_number - 1) * per_page).limit(per_page)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Aggregate shortcuts
|
|
327
|
+
def count(column = '*')
|
|
328
|
+
select("COUNT(#{column}) as count")
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def avg(column)
|
|
332
|
+
select("AVG(#{column}) as avg")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def sum(column)
|
|
336
|
+
select("SUM(#{column}) as sum")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def min(column)
|
|
340
|
+
select("MIN(#{column}) as min")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def max(column)
|
|
344
|
+
select("MAX(#{column}) as max")
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# UNION / UNION ALL
|
|
348
|
+
def union(query)
|
|
349
|
+
@unions << { type: 'UNION', query: query }
|
|
350
|
+
self
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def union_all(query)
|
|
354
|
+
@unions << { type: 'UNION ALL', query: query }
|
|
355
|
+
self
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Build SQL
|
|
359
|
+
def to_sql
|
|
360
|
+
raise "No table specified" unless @table
|
|
361
|
+
|
|
362
|
+
sql = []
|
|
363
|
+
sql << "SELECT"
|
|
364
|
+
sql << "DISTINCT" if @distinct
|
|
365
|
+
sql << (@selects.empty? ? '*' : @selects.join(', '))
|
|
366
|
+
sql << "FROM #{@table}"
|
|
367
|
+
|
|
368
|
+
# JOINs
|
|
369
|
+
# JOINs
|
|
370
|
+
@joins.each do |join|
|
|
371
|
+
if join[:type] == 'CROSS'
|
|
372
|
+
sql << "CROSS JOIN #{join[:table]}"
|
|
373
|
+
else
|
|
374
|
+
sql << "#{join[:type]} JOIN #{join[:table]} ON #{join[:first]} #{join[:operator]} #{join[:second]}"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# WHERE
|
|
379
|
+
unless @wheres.empty?
|
|
380
|
+
sql << "WHERE"
|
|
381
|
+
where_clauses = []
|
|
382
|
+
@wheres.each_with_index do |where, index|
|
|
383
|
+
clause = build_where_clause(where)
|
|
384
|
+
if index == 0
|
|
385
|
+
where_clauses << clause
|
|
386
|
+
else
|
|
387
|
+
where_clauses << "#{where[:boolean]} #{clause}"
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
sql << where_clauses.join(' ')
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# GROUP BY
|
|
394
|
+
unless @groups.empty?
|
|
395
|
+
sql << "GROUP BY #{@groups.join(', ')}"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# HAVING
|
|
399
|
+
unless @havings.empty?
|
|
400
|
+
sql << "HAVING"
|
|
401
|
+
having_clauses = []
|
|
402
|
+
@havings.each_with_index do |having, index|
|
|
403
|
+
clause = "#{having[:column]} #{having[:operator]} ?"
|
|
404
|
+
if index == 0
|
|
405
|
+
having_clauses << clause
|
|
406
|
+
else
|
|
407
|
+
having_clauses << "#{having[:boolean]} #{clause}"
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
sql << having_clauses.join(' ')
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# ORDER BY
|
|
414
|
+
unless @orders.empty?
|
|
415
|
+
sql << "ORDER BY #{@orders.map { |o| "#{o[:column]} #{o[:direction]}" }.join(', ')}"
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# LIMIT
|
|
419
|
+
sql << "LIMIT #{@limit_value}" if @limit_value
|
|
420
|
+
|
|
421
|
+
# OFFSET
|
|
422
|
+
sql << "OFFSET #{@offset_value}" if @offset_value
|
|
423
|
+
|
|
424
|
+
# Build main query
|
|
425
|
+
main_sql = sql.join(' ')
|
|
426
|
+
|
|
427
|
+
# UNION / UNION ALL
|
|
428
|
+
unless @unions.empty?
|
|
429
|
+
union_parts = [main_sql]
|
|
430
|
+
@unions.each do |union|
|
|
431
|
+
union_sql = union[:query].is_a?(String) ? union[:query] : union[:query].to_sql
|
|
432
|
+
union_parts << "#{union[:type]} #{union_sql}"
|
|
433
|
+
@bindings.concat(union[:query].bindings) if union[:query].respond_to?(:bindings)
|
|
434
|
+
end
|
|
435
|
+
return union_parts.join(' ')
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
main_sql
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def to_s
|
|
442
|
+
to_sql
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
private
|
|
446
|
+
|
|
447
|
+
# Build individual WHERE clause
|
|
448
|
+
def build_where_clause(where)
|
|
449
|
+
case where[:type]
|
|
450
|
+
when 'basic'
|
|
451
|
+
"#{where[:column]} #{where[:operator]} ?"
|
|
452
|
+
when 'in'
|
|
453
|
+
placeholders = (['?'] * where[:values].size).join(', ')
|
|
454
|
+
"#{where[:column]} IN (#{placeholders})"
|
|
455
|
+
when 'not_in'
|
|
456
|
+
placeholders = (['?'] * where[:values].size).join(', ')
|
|
457
|
+
"#{where[:column]} NOT IN (#{placeholders})"
|
|
458
|
+
when 'null'
|
|
459
|
+
"#{where[:column]} IS NULL"
|
|
460
|
+
when 'not_null'
|
|
461
|
+
"#{where[:column]} IS NOT NULL"
|
|
462
|
+
when 'between'
|
|
463
|
+
"#{where[:column]} BETWEEN ? AND ?"
|
|
464
|
+
when 'raw'
|
|
465
|
+
where[:sql]
|
|
466
|
+
when 'exists'
|
|
467
|
+
"EXISTS (#{where[:sql]})"
|
|
468
|
+
when 'not_exists'
|
|
469
|
+
"NOT EXISTS (#{where[:sql]})"
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QueryKit
|
|
4
|
+
# Base repository class for implementing the repository pattern
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# class UserRepository < QueryKit::Repository
|
|
8
|
+
# table 'users'
|
|
9
|
+
# model User
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# repo = UserRepository.new(db)
|
|
13
|
+
# user = repo.find(1)
|
|
14
|
+
# users = repo.all
|
|
15
|
+
# users = repo.where('age', '>', 18)
|
|
16
|
+
class Repository
|
|
17
|
+
attr_reader :db
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Set the table name for this repository
|
|
21
|
+
def table(name)
|
|
22
|
+
@table_name = name
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Set the model class for this repository
|
|
26
|
+
def model(klass)
|
|
27
|
+
@model_class = klass
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get the table name
|
|
31
|
+
def table_name
|
|
32
|
+
@table_name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Get the model class
|
|
36
|
+
def model_class
|
|
37
|
+
@model_class
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
attr_reader :db
|
|
42
|
+
|
|
43
|
+
# Initialize repository with optional database connection
|
|
44
|
+
# If no connection provided, uses global QueryKit.connection
|
|
45
|
+
# @param db [QueryKit::Connection, nil] Database connection
|
|
46
|
+
def initialize(db = nil)
|
|
47
|
+
@db = db || QueryKit.connection
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Get all records
|
|
51
|
+
def all
|
|
52
|
+
@db.get(query, model_class)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Find a record by ID
|
|
56
|
+
def find(id)
|
|
57
|
+
@db.first(query.where('id', id), model_class)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Find a record by column value
|
|
61
|
+
def find_by(column, value)
|
|
62
|
+
@db.first(query.where(column, value), model_class)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Find multiple records by column value
|
|
66
|
+
def where(column, operator_or_value, value = nil)
|
|
67
|
+
if value.nil?
|
|
68
|
+
# Two arguments: column and value (assumes =)
|
|
69
|
+
@db.get(query.where(column, operator_or_value), model_class)
|
|
70
|
+
else
|
|
71
|
+
# Three arguments: column, operator, value
|
|
72
|
+
@db.get(query.where(column, operator_or_value, value), model_class)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Find records where column is IN array
|
|
77
|
+
def where_in(column, values)
|
|
78
|
+
@db.get(query.where_in(column, values), model_class)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Find records where column is NOT IN array
|
|
82
|
+
def where_not_in(column, values)
|
|
83
|
+
@db.get(query.where_not_in(column, values), model_class)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get first record matching conditions
|
|
87
|
+
def first
|
|
88
|
+
@db.first(query, model_class)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Count all records
|
|
92
|
+
def count
|
|
93
|
+
result = @db.first(query.select('COUNT(*) as count'))
|
|
94
|
+
result ? result['count'] : 0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if any records exist
|
|
98
|
+
def exists?(id = nil)
|
|
99
|
+
if id
|
|
100
|
+
count_query = query.select('COUNT(*) as count').where('id', id)
|
|
101
|
+
else
|
|
102
|
+
count_query = query.select('COUNT(*) as count')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
result = @db.first(count_query)
|
|
106
|
+
result && result['count'] > 0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Insert a new record
|
|
110
|
+
# @param attributes [Hash] The attributes for the new record
|
|
111
|
+
# @return [Integer] The ID of the inserted record
|
|
112
|
+
def insert(attributes)
|
|
113
|
+
@db.execute_insert(@db.insert(table_name).values(attributes))
|
|
114
|
+
end
|
|
115
|
+
alias_method :create, :insert
|
|
116
|
+
|
|
117
|
+
# Update a record by ID
|
|
118
|
+
# @param id [Integer] The record ID
|
|
119
|
+
# @param attributes [Hash] The attributes to update
|
|
120
|
+
# @return [Integer] Number of affected rows
|
|
121
|
+
def update(id, attributes)
|
|
122
|
+
@db.execute_update(@db.update(table_name).set(attributes).where('id', id))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Delete a record by ID
|
|
126
|
+
# @param id [Integer] The record ID
|
|
127
|
+
# @return [Integer] Number of affected rows
|
|
128
|
+
def delete(id)
|
|
129
|
+
@db.execute_delete(@db.delete(table_name).where('id', id))
|
|
130
|
+
end
|
|
131
|
+
alias_method :destroy, :delete
|
|
132
|
+
|
|
133
|
+
# Delete all records matching conditions
|
|
134
|
+
# @param conditions [Hash] WHERE conditions
|
|
135
|
+
# @return [Integer] Number of affected rows
|
|
136
|
+
def delete_where(conditions)
|
|
137
|
+
delete_query = @db.delete(table_name)
|
|
138
|
+
conditions.each { |column, value| delete_query.where(column, value) }
|
|
139
|
+
@db.execute_delete(delete_query)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Execute a custom query with model mapping
|
|
143
|
+
# @param custom_query [QueryKit::Query] A custom query object
|
|
144
|
+
# @return [Array] Array of model instances
|
|
145
|
+
def execute(custom_query)
|
|
146
|
+
@db.get(custom_query, model_class)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Execute a custom query and return first result
|
|
150
|
+
# @param custom_query [QueryKit::Query] A custom query object
|
|
151
|
+
# @return [Object, nil] Model instance or nil
|
|
152
|
+
def execute_first(custom_query)
|
|
153
|
+
@db.first(custom_query, model_class)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Begin a transaction
|
|
157
|
+
def transaction(&block)
|
|
158
|
+
@db.transaction(&block)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
protected
|
|
162
|
+
|
|
163
|
+
# Get a new query object for this repository's table
|
|
164
|
+
def query
|
|
165
|
+
@db.query(table_name)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Get the table name from class configuration
|
|
169
|
+
def table_name
|
|
170
|
+
name = self.class.table_name
|
|
171
|
+
raise "Table name not configured for #{self.class.name}. Use: table 'table_name'" unless name
|
|
172
|
+
name
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Get the model class from class configuration
|
|
176
|
+
def model_class
|
|
177
|
+
klass = self.class.model_class
|
|
178
|
+
raise "Model class not configured for #{self.class.name}. Use: model ModelClass" unless klass
|
|
179
|
+
klass
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QueryKit
|
|
4
|
+
# UpdateQuery class for building SQL UPDATE statements.
|
|
5
|
+
class UpdateQuery
|
|
6
|
+
attr_reader :table, :values, :wheres, :bindings
|
|
7
|
+
|
|
8
|
+
# Initialize a new UpdateQuery instance.
|
|
9
|
+
def initialize(table = nil)
|
|
10
|
+
@table = table
|
|
11
|
+
@values = {}
|
|
12
|
+
@wheres = []
|
|
13
|
+
@bindings = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Set the values to update.
|
|
17
|
+
def set(data)
|
|
18
|
+
@values.merge!(data)
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add a WHERE condition to the update query.
|
|
23
|
+
def where(column, operator = nil, value = nil)
|
|
24
|
+
if value.nil? && !operator.nil?
|
|
25
|
+
value = operator
|
|
26
|
+
operator = '='
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@wheres << { type: 'basic', column: column, operator: operator, value: value, boolean: 'AND' }
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate the SQL UPDATE statement.
|
|
34
|
+
def to_sql
|
|
35
|
+
raise "No table specified" unless @table
|
|
36
|
+
raise "No values to update" if @values.empty?
|
|
37
|
+
|
|
38
|
+
@bindings = @values.values + @wheres.map { |w| w[:value] }
|
|
39
|
+
|
|
40
|
+
sql = []
|
|
41
|
+
sql << "UPDATE #{@table}"
|
|
42
|
+
sql << "SET"
|
|
43
|
+
sql << @values.keys.map { |k| "#{k} = ?" }.join(', ')
|
|
44
|
+
|
|
45
|
+
unless @wheres.empty?
|
|
46
|
+
sql << "WHERE"
|
|
47
|
+
where_clauses = @wheres.map { |w| "#{w[:column]} #{w[:operator]} ?" }
|
|
48
|
+
sql << where_clauses.join(' AND ')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sql.join(' ')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Return the SQL UPDATE statement as a string.
|
|
55
|
+
def to_s
|
|
56
|
+
to_sql
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|