simple_query 0.4.0 → 0.6.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 +4 -4
- data/README.md +454 -123
- data/lib/simple_query/builder.rb +240 -23
- data/lib/simple_query/clauses/aggregation_clause.rb +158 -0
- data/lib/simple_query/clauses/group_having_clause.rb +6 -6
- data/lib/simple_query/clauses/set_clause.rb +4 -3
- data/lib/simple_query/clauses/where_clause.rb +34 -9
- data/lib/simple_query/stream/mysql_stream.rb +9 -4
- data/lib/simple_query/stream/postgres_stream.rb +45 -11
- data/lib/simple_query/version.rb +1 -1
- data/lib/simple_query.rb +11 -4
- metadata +7 -6
data/lib/simple_query/builder.rb
CHANGED
|
@@ -12,17 +12,18 @@ module SimpleQuery
|
|
|
12
12
|
@arel_table = @model.arel_table
|
|
13
13
|
|
|
14
14
|
@selects = []
|
|
15
|
-
@wheres = WhereClause.new(@arel_table)
|
|
15
|
+
@wheres = WhereClause.new(@arel_table, @model)
|
|
16
16
|
@joins = JoinClause.new
|
|
17
|
-
@group_having = GroupHavingClause.new(@arel_table)
|
|
17
|
+
@group_having = GroupHavingClause.new(@arel_table, @model)
|
|
18
18
|
@orders = OrderClause.new(@arel_table)
|
|
19
19
|
@limits = LimitOffsetClause.new
|
|
20
20
|
@distinct_flag = DistinctClause.new
|
|
21
|
+
@aggregations = AggregationClause.new(@arel_table, @model)
|
|
21
22
|
|
|
22
23
|
@query_cache = {}
|
|
23
24
|
@query_built = false
|
|
24
25
|
@read_model_class = nil
|
|
25
|
-
@
|
|
26
|
+
@result_structs = {}
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def select(*fields)
|
|
@@ -91,6 +92,99 @@ module SimpleQuery
|
|
|
91
92
|
self
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
# Aggregation methods
|
|
96
|
+
def count(column = nil, alias_name: nil)
|
|
97
|
+
@aggregations.count(column, alias_name: alias_name)
|
|
98
|
+
reset_query
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def sum(column, alias_name: nil)
|
|
103
|
+
@aggregations.sum(column, alias_name: alias_name)
|
|
104
|
+
reset_query
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def avg(column, alias_name: nil)
|
|
109
|
+
@aggregations.avg(column, alias_name: alias_name)
|
|
110
|
+
reset_query
|
|
111
|
+
self
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def min(column, alias_name: nil)
|
|
115
|
+
@aggregations.min(column, alias_name: alias_name)
|
|
116
|
+
reset_query
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def max(column, alias_name: nil)
|
|
121
|
+
@aggregations.max(column, alias_name: alias_name)
|
|
122
|
+
reset_query
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def variance(column, alias_name: nil)
|
|
127
|
+
@aggregations.variance(column, alias_name: alias_name)
|
|
128
|
+
reset_query
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def stddev(column, alias_name: nil)
|
|
133
|
+
@aggregations.stddev(column, alias_name: alias_name)
|
|
134
|
+
reset_query
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def group_concat(column, separator: ",", alias_name: nil)
|
|
139
|
+
@aggregations.group_concat(column, separator: separator, alias_name: alias_name)
|
|
140
|
+
reset_query
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def custom_aggregation(expression, alias_name)
|
|
145
|
+
@aggregations.custom(expression, alias_name)
|
|
146
|
+
reset_query
|
|
147
|
+
self
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Convenience methods for common aggregation patterns
|
|
151
|
+
def total_count(alias_name: "total")
|
|
152
|
+
count(alias_name: alias_name)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def stats(column, alias_prefix: nil)
|
|
156
|
+
prefix = alias_prefix || column.to_s
|
|
157
|
+
count(alias_name: "#{prefix}_count")
|
|
158
|
+
sum(column, alias_name: "#{prefix}_sum")
|
|
159
|
+
avg(column, alias_name: "#{prefix}_avg")
|
|
160
|
+
min(column, alias_name: "#{prefix}_min")
|
|
161
|
+
max(column, alias_name: "#{prefix}_max")
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Method to get first/top record by column
|
|
166
|
+
def first_by(column, alias_name: nil)
|
|
167
|
+
alias_name ||= "first_#{column}"
|
|
168
|
+
custom_aggregation("FIRST_VALUE(#{resolve_column_name(column)}) OVER (ORDER BY #{resolve_column_name(column)})",
|
|
169
|
+
alias_name)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Method to get last/bottom record by column
|
|
173
|
+
def last_by(column, alias_name: nil)
|
|
174
|
+
alias_name ||= "last_#{column}"
|
|
175
|
+
custom_aggregation("LAST_VALUE(#{resolve_column_name(column)}) OVER (ORDER BY #{resolve_column_name(column)} " \
|
|
176
|
+
"ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)",
|
|
177
|
+
alias_name)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Percentage calculations
|
|
181
|
+
def percentage_of_total(column, alias_name: nil)
|
|
182
|
+
alias_name ||= "#{column}_percentage"
|
|
183
|
+
column_expr = resolve_column_name(column)
|
|
184
|
+
expression = "ROUND((#{column_expr} * 100.0 / SUM(#{column_expr}) OVER ()), 2)"
|
|
185
|
+
custom_aggregation(expression, alias_name)
|
|
186
|
+
end
|
|
187
|
+
|
|
94
188
|
def map_to(klass)
|
|
95
189
|
@read_model_class = klass
|
|
96
190
|
reset_query
|
|
@@ -98,37 +192,44 @@ module SimpleQuery
|
|
|
98
192
|
end
|
|
99
193
|
|
|
100
194
|
def bulk_update(set:)
|
|
101
|
-
|
|
102
|
-
set_sql = SetClause.new(set).to_sql
|
|
195
|
+
validate_bulk_update_scope!
|
|
103
196
|
|
|
197
|
+
set_sql = SetClause.new(set, @model).to_sql
|
|
104
198
|
raise ArgumentError, "No columns to update" if set_sql.empty?
|
|
105
199
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
200
|
+
update = Arel::UpdateManager.new
|
|
201
|
+
update.table(@arel_table)
|
|
202
|
+
update.set(Arel.sql(set_sql))
|
|
203
|
+
condition = @wheres.to_arel
|
|
204
|
+
update.where(condition) if condition
|
|
109
205
|
|
|
110
|
-
|
|
206
|
+
connection.execute(compile_sql(update))
|
|
111
207
|
end
|
|
112
208
|
|
|
113
209
|
def stream_each(batch_size: 1000, &block)
|
|
114
|
-
|
|
210
|
+
validate_stream_batch_size!(batch_size)
|
|
211
|
+
|
|
212
|
+
adapter_name = connection.adapter_name
|
|
213
|
+
adapter = adapter_name.downcase
|
|
115
214
|
if adapter.include?("postgres")
|
|
116
215
|
stream_each_postgres(batch_size, &block)
|
|
117
216
|
elsif adapter.include?("mysql")
|
|
118
217
|
stream_each_mysql(&block)
|
|
119
218
|
else
|
|
120
|
-
|
|
219
|
+
message = "stream_each is only implemented for PostgreSQL and MySQL adapters " \
|
|
220
|
+
"(current adapter: #{adapter_name})"
|
|
221
|
+
raise UnsupportedAdapterError, message
|
|
121
222
|
end
|
|
122
223
|
end
|
|
123
224
|
|
|
124
225
|
def execute
|
|
125
|
-
records =
|
|
226
|
+
records = connection.select_all(cached_sql)
|
|
126
227
|
build_result_objects_from_rows(records)
|
|
127
228
|
end
|
|
128
229
|
|
|
129
230
|
def lazy_execute
|
|
130
231
|
Enumerator.new do |yielder|
|
|
131
|
-
records =
|
|
232
|
+
records = connection.select_all(cached_sql)
|
|
132
233
|
if @read_model_class
|
|
133
234
|
build_read_models_enumerator(records, yielder)
|
|
134
235
|
else
|
|
@@ -145,7 +246,10 @@ module SimpleQuery
|
|
|
145
246
|
|
|
146
247
|
@query = Arel::SelectManager.new(Arel::Table.engine)
|
|
147
248
|
@query.from(@arel_table)
|
|
148
|
-
|
|
249
|
+
|
|
250
|
+
# Combine regular selects with aggregations
|
|
251
|
+
all_selects = build_select_expressions
|
|
252
|
+
@query.project(*all_selects)
|
|
149
253
|
|
|
150
254
|
apply_distinct
|
|
151
255
|
apply_where_conditions
|
|
@@ -160,11 +264,46 @@ module SimpleQuery
|
|
|
160
264
|
|
|
161
265
|
private
|
|
162
266
|
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
267
|
+
def validate_stream_batch_size!(batch_size)
|
|
268
|
+
return if batch_size.is_a?(Integer) && batch_size.positive?
|
|
269
|
+
|
|
270
|
+
raise ArgumentError, "stream_each batch_size must be a positive Integer"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def validate_bulk_update_scope!
|
|
274
|
+
unsupported = []
|
|
275
|
+
unsupported << "joins" if @joins.joins.any?
|
|
276
|
+
unsupported << "limit/offset" if @limits.limit_value || @limits.offset_value
|
|
277
|
+
unsupported << "group/having" if @group_having.group_fields.any? || @group_having.having_conditions.any?
|
|
278
|
+
return if unsupported.empty?
|
|
279
|
+
|
|
280
|
+
raise ArgumentError, "bulk_update only supports where conditions (found #{unsupported.join(", ")})"
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def build_select_expressions
|
|
284
|
+
expressions = []
|
|
285
|
+
|
|
286
|
+
# Add regular selects
|
|
287
|
+
if @selects.any?
|
|
288
|
+
expressions.concat(@selects)
|
|
289
|
+
elsif @aggregations.empty?
|
|
290
|
+
# Only use * if no aggregations and no explicit selects
|
|
291
|
+
expressions << @arel_table[Arel.star]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Add aggregation expressions
|
|
295
|
+
expressions.concat(@aggregations.to_arel_expressions)
|
|
296
|
+
|
|
297
|
+
expressions
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def connection
|
|
301
|
+
@model.connection
|
|
302
|
+
end
|
|
166
303
|
|
|
167
|
-
|
|
304
|
+
# Compiles with the model's own connection, inlining any bind values.
|
|
305
|
+
def compile_sql(arel)
|
|
306
|
+
connection.unprepared_statement { connection.to_sql(arel) }
|
|
168
307
|
end
|
|
169
308
|
|
|
170
309
|
def reset_query
|
|
@@ -182,10 +321,11 @@ module SimpleQuery
|
|
|
182
321
|
@orders.orders,
|
|
183
322
|
@limits.limit_value,
|
|
184
323
|
@limits.offset_value,
|
|
185
|
-
@distinct_flag.use_distinct
|
|
324
|
+
@distinct_flag.use_distinct?,
|
|
325
|
+
@aggregations.aggregations
|
|
186
326
|
]
|
|
187
327
|
|
|
188
|
-
@query_cache[key] ||= build_query
|
|
328
|
+
@query_cache[key] ||= compile_sql(build_query)
|
|
189
329
|
end
|
|
190
330
|
|
|
191
331
|
def build_result_objects_from_rows(records)
|
|
@@ -226,7 +366,8 @@ module SimpleQuery
|
|
|
226
366
|
end
|
|
227
367
|
|
|
228
368
|
def result_struct(columns)
|
|
229
|
-
|
|
369
|
+
struct_key = columns.map(&:to_s)
|
|
370
|
+
@result_structs[struct_key] ||= Struct.new(*struct_key.map(&:to_sym))
|
|
230
371
|
end
|
|
231
372
|
|
|
232
373
|
def apply_distinct
|
|
@@ -267,9 +408,21 @@ module SimpleQuery
|
|
|
267
408
|
end
|
|
268
409
|
end
|
|
269
410
|
|
|
270
|
-
def
|
|
411
|
+
def resolve_column_name(column)
|
|
412
|
+
case column
|
|
413
|
+
when Symbol
|
|
414
|
+
"#{@arel_table.name}.#{column}"
|
|
415
|
+
when String
|
|
416
|
+
column.include?(".") ? column : "#{@arel_table.name}.#{column}"
|
|
417
|
+
else
|
|
418
|
+
column.to_s
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
271
423
|
if (scope_block = find_scope(method_name))
|
|
272
|
-
|
|
424
|
+
validate_scope_arity!(method_name, scope_block, scope_arguments_for_validation(scope_block, args, kwargs))
|
|
425
|
+
instance_exec(*args, **kwargs, &scope_block)
|
|
273
426
|
self
|
|
274
427
|
else
|
|
275
428
|
super
|
|
@@ -285,5 +438,69 @@ module SimpleQuery
|
|
|
285
438
|
|
|
286
439
|
model._simple_scopes[method_name.to_sym]
|
|
287
440
|
end
|
|
441
|
+
|
|
442
|
+
def validate_scope_arity!(scope_name, scope_block, args)
|
|
443
|
+
range = scope_argument_range(scope_block)
|
|
444
|
+
provided = args.length
|
|
445
|
+
return if scope_arity_matches?(range, provided)
|
|
446
|
+
|
|
447
|
+
raise ArgumentError,
|
|
448
|
+
"simple_scope :#{scope_name} expected #{scope_arity_description(range)}, provided #{provided}"
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def scope_arguments_for_validation(scope_block, args, kwargs)
|
|
452
|
+
return args if kwargs.empty? || scope_uses_keyword_arguments?(scope_block)
|
|
453
|
+
|
|
454
|
+
args + [kwargs]
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def scope_argument_range(scope_block)
|
|
458
|
+
# Keyword argument arity has Ruby-version-specific edge cases; let Ruby raise its native error.
|
|
459
|
+
return nil if scope_uses_keyword_arguments?(scope_block)
|
|
460
|
+
|
|
461
|
+
minimum = minimum_scope_arguments(scope_block)
|
|
462
|
+
maximum = maximum_scope_arguments(scope_block)
|
|
463
|
+
|
|
464
|
+
[minimum, maximum]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def scope_uses_keyword_arguments?(scope_block)
|
|
468
|
+
scope_block.parameters.any? { |kind, _name| [:key, :keyreq, :keyrest].include?(kind) }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def minimum_scope_arguments(scope_block)
|
|
472
|
+
arity = scope_block.arity
|
|
473
|
+
return arity if arity >= 0
|
|
474
|
+
|
|
475
|
+
-arity - 1
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def maximum_scope_arguments(scope_block)
|
|
479
|
+
parameters = scope_block.parameters
|
|
480
|
+
return nil if parameters.any? { |kind, _name| kind == :rest }
|
|
481
|
+
|
|
482
|
+
parameters.count { |kind, _name| [:req, :opt].include?(kind) }
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def scope_arity_matches?(range, provided)
|
|
486
|
+
return true if range.nil?
|
|
487
|
+
|
|
488
|
+
minimum, maximum = range
|
|
489
|
+
return provided >= minimum if maximum.nil?
|
|
490
|
+
|
|
491
|
+
provided.between?(minimum, maximum)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def scope_arity_description(range)
|
|
495
|
+
minimum, maximum = range
|
|
496
|
+
return "at least #{argument_count_description(minimum)}" if maximum.nil?
|
|
497
|
+
return "exactly #{argument_count_description(minimum)}" if minimum == maximum
|
|
498
|
+
|
|
499
|
+
"#{minimum} to #{argument_count_description(maximum)}"
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def argument_count_description(count)
|
|
503
|
+
"#{count} #{count == 1 ? "argument" : "arguments"}"
|
|
504
|
+
end
|
|
288
505
|
end
|
|
289
506
|
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleQuery
|
|
4
|
+
class AggregationClause
|
|
5
|
+
attr_reader :aggregations
|
|
6
|
+
|
|
7
|
+
def initialize(table, model = ActiveRecord::Base)
|
|
8
|
+
@table = table
|
|
9
|
+
@model = model
|
|
10
|
+
@aggregations = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def count(column = nil, alias_name: nil)
|
|
14
|
+
column_expr = column ? resolve_column(column) : "*"
|
|
15
|
+
alias_name ||= column ? "count_#{sanitize_alias(column)}" : "count"
|
|
16
|
+
|
|
17
|
+
add_aggregation("COUNT", column_expr, alias_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sum(column, alias_name: nil)
|
|
21
|
+
raise ArgumentError, "Column is required for SUM aggregation" if column.nil?
|
|
22
|
+
|
|
23
|
+
column_expr = resolve_column(column)
|
|
24
|
+
alias_name ||= "sum_#{sanitize_alias(column)}"
|
|
25
|
+
|
|
26
|
+
add_aggregation("SUM", column_expr, alias_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def avg(column, alias_name: nil)
|
|
30
|
+
raise ArgumentError, "Column is required for AVG aggregation" if column.nil?
|
|
31
|
+
|
|
32
|
+
column_expr = resolve_column(column)
|
|
33
|
+
alias_name ||= "avg_#{sanitize_alias(column)}"
|
|
34
|
+
|
|
35
|
+
add_aggregation("AVG", column_expr, alias_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def min(column, alias_name: nil)
|
|
39
|
+
raise ArgumentError, "Column is required for MIN aggregation" if column.nil?
|
|
40
|
+
|
|
41
|
+
column_expr = resolve_column(column)
|
|
42
|
+
alias_name ||= "min_#{sanitize_alias(column)}"
|
|
43
|
+
|
|
44
|
+
add_aggregation("MIN", column_expr, alias_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def max(column, alias_name: nil)
|
|
48
|
+
raise ArgumentError, "Column is required for MAX aggregation" if column.nil?
|
|
49
|
+
|
|
50
|
+
column_expr = resolve_column(column)
|
|
51
|
+
alias_name ||= "max_#{sanitize_alias(column)}"
|
|
52
|
+
|
|
53
|
+
add_aggregation("MAX", column_expr, alias_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def custom(expression, alias_name)
|
|
57
|
+
if expression.nil? || alias_name.nil?
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"Expression and alias are required for custom aggregation"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@aggregations << {
|
|
63
|
+
expression: expression,
|
|
64
|
+
alias: alias_name
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Statistical functions
|
|
69
|
+
def variance(column, alias_name: nil)
|
|
70
|
+
raise ArgumentError, "Column is required for VARIANCE aggregation" if column.nil?
|
|
71
|
+
|
|
72
|
+
column_expr = resolve_column(column)
|
|
73
|
+
alias_name ||= "variance_#{sanitize_alias(column)}"
|
|
74
|
+
|
|
75
|
+
add_aggregation("VARIANCE", column_expr, alias_name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def stddev(column, alias_name: nil)
|
|
79
|
+
raise ArgumentError, "Column is required for STDDEV aggregation" if column.nil?
|
|
80
|
+
|
|
81
|
+
column_expr = resolve_column(column)
|
|
82
|
+
alias_name ||= "stddev_#{sanitize_alias(column)}"
|
|
83
|
+
|
|
84
|
+
add_aggregation("STDDEV", column_expr, alias_name)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Group concatenation (database-specific)
|
|
88
|
+
def group_concat(column, separator: ",", alias_name: nil)
|
|
89
|
+
raise ArgumentError, "Column is required for GROUP_CONCAT aggregation" if column.nil?
|
|
90
|
+
|
|
91
|
+
column_expr = resolve_column(column)
|
|
92
|
+
alias_name ||= "group_concat_#{sanitize_alias(column)}"
|
|
93
|
+
|
|
94
|
+
# Use database-specific group concatenation
|
|
95
|
+
connection = @model.connection
|
|
96
|
+
adapter = connection.adapter_name.downcase
|
|
97
|
+
quoted_separator = connection.quote(separator.to_s)
|
|
98
|
+
|
|
99
|
+
expression = case adapter
|
|
100
|
+
when /mysql/
|
|
101
|
+
"GROUP_CONCAT(#{column_expr} SEPARATOR #{quoted_separator})"
|
|
102
|
+
when /postgres/
|
|
103
|
+
"STRING_AGG(#{column_expr}::text, #{quoted_separator})"
|
|
104
|
+
when /sqlite/
|
|
105
|
+
"GROUP_CONCAT(#{column_expr}, #{quoted_separator})"
|
|
106
|
+
else
|
|
107
|
+
# Fallback for other databases
|
|
108
|
+
"GROUP_CONCAT(#{column_expr})"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@aggregations << {
|
|
112
|
+
expression: expression,
|
|
113
|
+
alias: alias_name
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def to_arel_expressions
|
|
118
|
+
@aggregations.map do |agg|
|
|
119
|
+
Arel.sql("#{agg[:expression]} AS #{agg[:alias]}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def empty?
|
|
124
|
+
@aggregations.empty?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def clear
|
|
128
|
+
@aggregations.clear
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def add_aggregation(function, column_expr, alias_name)
|
|
134
|
+
@aggregations << {
|
|
135
|
+
expression: "#{function}(#{column_expr})",
|
|
136
|
+
alias: alias_name
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resolve_column(column)
|
|
141
|
+
case column
|
|
142
|
+
when Symbol
|
|
143
|
+
"#{@table.name}.#{column}"
|
|
144
|
+
when String
|
|
145
|
+
# Allow table.column format or just column name
|
|
146
|
+
column.include?(".") ? column : "#{@table.name}.#{column}"
|
|
147
|
+
when Arel::Attributes::Attribute
|
|
148
|
+
column.to_sql
|
|
149
|
+
else
|
|
150
|
+
column.to_s
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def sanitize_alias(column)
|
|
155
|
+
column.to_s.gsub(".", "_")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -4,8 +4,9 @@ module SimpleQuery
|
|
|
4
4
|
class GroupHavingClause
|
|
5
5
|
attr_reader :group_fields, :having_conditions
|
|
6
6
|
|
|
7
|
-
def initialize(table)
|
|
7
|
+
def initialize(table, model = ActiveRecord::Base)
|
|
8
8
|
@table = table
|
|
9
|
+
@model = model
|
|
9
10
|
@group_fields = []
|
|
10
11
|
@having_conditions = []
|
|
11
12
|
end
|
|
@@ -15,15 +16,14 @@ module SimpleQuery
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def add_having(condition)
|
|
18
|
-
@
|
|
19
|
+
clause = WhereClause.new(@table, @model)
|
|
20
|
+
clause.add(condition)
|
|
21
|
+
@having_conditions.concat(clause.conditions)
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def apply_to(query)
|
|
22
25
|
@group_fields.each { |g| query.group(g) }
|
|
23
|
-
if @having_conditions.any?
|
|
24
|
-
combined = @having_conditions.inject { |c, a| c.and(a) }
|
|
25
|
-
query.having(combined)
|
|
26
|
-
end
|
|
26
|
+
query.having(WhereClause.combine(@having_conditions)) if @having_conditions.any?
|
|
27
27
|
query
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleQuery
|
|
4
4
|
class SetClause
|
|
5
|
-
def initialize(set_hash)
|
|
5
|
+
def initialize(set_hash, model = ActiveRecord::Base)
|
|
6
6
|
@set_hash = set_hash
|
|
7
|
+
@model = model
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def to_sql
|
|
@@ -15,11 +16,11 @@ module SimpleQuery
|
|
|
15
16
|
private
|
|
16
17
|
|
|
17
18
|
def quote_column(col)
|
|
18
|
-
|
|
19
|
+
@model.connection.quote_column_name(col)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def quote_value(val)
|
|
22
|
-
|
|
23
|
+
@model.connection.quote(val)
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
end
|
|
@@ -4,8 +4,16 @@ module SimpleQuery
|
|
|
4
4
|
class WhereClause
|
|
5
5
|
attr_reader :conditions
|
|
6
6
|
|
|
7
|
-
def
|
|
7
|
+
def self.combine(conditions)
|
|
8
|
+
return nil if conditions.empty?
|
|
9
|
+
return conditions.first if conditions.one?
|
|
10
|
+
|
|
11
|
+
Arel::Nodes::And.new(conditions.map { |c| c.is_a?(Arel::Nodes::Grouping) ? c : Arel::Nodes::Grouping.new(c) })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(table, model = ActiveRecord::Base)
|
|
8
15
|
@table = table
|
|
16
|
+
@model = model
|
|
9
17
|
@conditions = []
|
|
10
18
|
end
|
|
11
19
|
|
|
@@ -15,11 +23,7 @@ module SimpleQuery
|
|
|
15
23
|
end
|
|
16
24
|
|
|
17
25
|
def to_arel
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@conditions.inject do |combined, current|
|
|
21
|
-
combined.and(current)
|
|
22
|
-
end
|
|
26
|
+
self.class.combine(@conditions)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
private
|
|
@@ -27,15 +31,36 @@ module SimpleQuery
|
|
|
27
31
|
def parse_condition(condition)
|
|
28
32
|
case condition
|
|
29
33
|
when Hash
|
|
30
|
-
condition.map { |field, value|
|
|
31
|
-
when Arel::Nodes::Node
|
|
34
|
+
condition.map { |field, value| hash_predicate(field, value) }
|
|
35
|
+
when Arel::Nodes::Node, Arel::Attributes::Attribute
|
|
32
36
|
[condition]
|
|
33
37
|
when Array
|
|
34
|
-
sanitized_sql =
|
|
38
|
+
sanitized_sql = @model.send(:sanitize_sql_array, condition)
|
|
35
39
|
[Arel.sql(sanitized_sql)]
|
|
36
40
|
else
|
|
37
41
|
[Arel.sql(condition.to_s)]
|
|
38
42
|
end
|
|
39
43
|
end
|
|
44
|
+
|
|
45
|
+
def hash_predicate(field, value)
|
|
46
|
+
attribute = @table[field]
|
|
47
|
+
return attribute.eq(value) if force_equality?(field, value)
|
|
48
|
+
|
|
49
|
+
case value
|
|
50
|
+
when Array
|
|
51
|
+
values = value.compact
|
|
52
|
+
predicate = attribute.in(values)
|
|
53
|
+
values.size == value.size ? predicate : predicate.or(attribute.eq(nil))
|
|
54
|
+
when Range
|
|
55
|
+
attribute.between(value)
|
|
56
|
+
else
|
|
57
|
+
attribute.eq(value)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# PostgreSQL array/range columns compare arrays and ranges by equality, like ActiveRecord does.
|
|
62
|
+
def force_equality?(field, value)
|
|
63
|
+
@table.able_to_type_cast? && @table.type_for_attribute(field.to_s).force_equality?(value)
|
|
64
|
+
end
|
|
40
65
|
end
|
|
41
66
|
end
|
|
@@ -6,12 +6,17 @@ module SimpleQuery
|
|
|
6
6
|
def stream_each_mysql(&block)
|
|
7
7
|
select_sql = cached_sql
|
|
8
8
|
|
|
9
|
-
raw_conn =
|
|
9
|
+
raw_conn = connection.raw_connection
|
|
10
10
|
|
|
11
11
|
result = raw_conn.query(select_sql, stream: true, cache_rows: false, as: :hash)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
begin
|
|
13
|
+
result.each do |mysql_row|
|
|
14
|
+
record = build_row_object_mysql(mysql_row)
|
|
15
|
+
block.call(record)
|
|
16
|
+
end
|
|
17
|
+
ensure
|
|
18
|
+
# Discards unread rows so the connection can run the next query after an error or break.
|
|
19
|
+
result.free
|
|
15
20
|
end
|
|
16
21
|
end
|
|
17
22
|
|