batchagg 0.1.0 → 0.1.1
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 +5 -0
- data/lib/batchagg/version.rb +1 -1
- data/lib/batchagg.rb +173 -654
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7974966a61d4458721e6c61956c9dda9554a2e4e8d05240e8cb3ef6599a5f16c
|
|
4
|
+
data.tar.gz: e6da51c30121042ecaf42b9862dd910c2fce26db88e5e9b8a54f56faeefa0b2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b8f8167c5de9611402559fa2c79bad9a10ad6c39fc81478e3d8f1c1e446844297a3dcde6f92826dbd68de628d0c7c2f2860a8b38446363c783e14423efc4a70
|
|
7
|
+
data.tar.gz: 32d5a18e5750fcd3a33cf1bea2d17f67b823827dd69f40d307e1d8df47adb2f3290b686dd7024d278e4396e4c5d3bceea362bdbbb8c243accf6d17846aa118e5
|
data/README.md
CHANGED
|
@@ -12,6 +12,11 @@ collection of records without making repeated database queries. It uses
|
|
|
12
12
|
correlated subqueries to fetch all aggregations in a single efficient database
|
|
13
13
|
call, improving application performance.
|
|
14
14
|
|
|
15
|
+
### Unsupported
|
|
16
|
+
|
|
17
|
+
Because of the composing of SQL statements, there will never be support for `group` or `GROUP BY`.
|
|
18
|
+
Please don't open an issue about this, unless you have an idea to solve it.
|
|
19
|
+
|
|
15
20
|
## Installation
|
|
16
21
|
|
|
17
22
|
```ruby
|
data/lib/batchagg/version.rb
CHANGED
data/lib/batchagg.rb
CHANGED
|
@@ -3,734 +3,253 @@
|
|
|
3
3
|
require "active_record"
|
|
4
4
|
|
|
5
5
|
module BatchAgg
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def initialize(name, type, block, column = nil, expression = nil, options = nil)
|
|
11
|
-
@name = name
|
|
12
|
-
@type = type
|
|
13
|
-
@block = block
|
|
14
|
-
@column = column
|
|
15
|
-
@expression = expression
|
|
16
|
-
@options = options
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def column_based?
|
|
20
|
-
type == :column
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def computed?
|
|
24
|
-
type == :computed
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def block?
|
|
28
|
-
!block.nil?
|
|
29
|
-
end
|
|
6
|
+
AggregateDef = Struct.new(:name, :type, :block, :column, :expression, :options, keyword_init: true) do
|
|
7
|
+
def column_based? = type == :column
|
|
8
|
+
def computed? = type == :computed
|
|
9
|
+
def block? = !block.nil?
|
|
30
10
|
end
|
|
31
11
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
reflection
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
12
|
+
class AssocMagic
|
|
13
|
+
def initialize(model, outer_table)
|
|
14
|
+
@model = model
|
|
15
|
+
@outer_table = outer_table
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(name, *args, &)
|
|
19
|
+
reflection = @model.reflect_on_association(name)
|
|
20
|
+
if reflection
|
|
21
|
+
target = reflection.klass
|
|
22
|
+
if reflection.through_reflection
|
|
23
|
+
# has_many :through
|
|
24
|
+
through = reflection.through_reflection
|
|
25
|
+
source = reflection.source_reflection
|
|
26
|
+
final_target = reflection.klass.arel_table
|
|
27
|
+
through_table = through.klass.arel_table
|
|
28
|
+
|
|
29
|
+
cond1 = if source.macro == :belongs_to
|
|
30
|
+
through_table[source.foreign_key].eq(final_target[source.association_primary_key])
|
|
31
|
+
else
|
|
32
|
+
final_target[source.foreign_key].eq(through_table[source.active_record_primary_key])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
cond2 = if through.macro == :belongs_to
|
|
36
|
+
@outer_table[through.foreign_key].eq(through_table[through.association_primary_key])
|
|
37
|
+
else
|
|
38
|
+
through_table[through.foreign_key].eq(@outer_table[through.active_record_primary_key])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
subquery = through_table.project(Arel.sql("1")).where(cond1.and(cond2))
|
|
42
|
+
target.where(subquery.exists)
|
|
43
|
+
elsif reflection.macro == :belongs_to
|
|
44
|
+
target.where(target.arel_table[reflection.association_primary_key].eq(@outer_table[reflection.foreign_key]))
|
|
45
|
+
else
|
|
46
|
+
target.where(target.arel_table[reflection.foreign_key].eq(@outer_table[reflection.active_record_primary_key]))
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
super
|
|
67
50
|
end
|
|
68
51
|
end
|
|
69
52
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def build_belongs_to_condition(reflection)
|
|
75
|
-
target_table = reflection.klass.arel_table
|
|
76
|
-
foreign_key_column = @outer_table_alias[reflection.foreign_key]
|
|
77
|
-
primary_key_column = target_table[reflection.association_primary_key]
|
|
78
|
-
|
|
79
|
-
primary_key_column.eq(foreign_key_column)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def build_has_many_condition(reflection)
|
|
83
|
-
target_table = reflection.klass.arel_table
|
|
84
|
-
foreign_key_column = target_table[reflection.foreign_key]
|
|
85
|
-
primary_key_column = @outer_table_alias[reflection.active_record_primary_key]
|
|
86
|
-
|
|
87
|
-
foreign_key_column.eq(primary_key_column)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def build_has_many_through_condition(reflection)
|
|
91
|
-
through_reflection = reflection.through_reflection
|
|
92
|
-
source_reflection = reflection.source_reflection
|
|
93
|
-
|
|
94
|
-
final_target_table = reflection.klass.arel_table
|
|
95
|
-
through_table = through_reflection.klass.arel_table
|
|
96
|
-
|
|
97
|
-
# Condition 1: Links the through_table to the final_target_table
|
|
98
|
-
# Based on source_reflection (e.g., Appointment.belongs_to :patient)
|
|
99
|
-
cond1_arel = if source_reflection.macro == :belongs_to
|
|
100
|
-
through_table[source_reflection.foreign_key].eq(final_target_table[source_reflection.association_primary_key])
|
|
101
|
-
else # :has_many, :has_one
|
|
102
|
-
final_target_table[source_reflection.foreign_key].eq(through_table[source_reflection.active_record_primary_key])
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Condition 2: Links the through_table to the @outer_table_alias (base model)
|
|
106
|
-
# Based on through_reflection (e.g., Physician.has_many :appointments)
|
|
107
|
-
cond2_arel = if through_reflection.macro == :belongs_to
|
|
108
|
-
@outer_table_alias[through_reflection.foreign_key].eq(through_table[through_reflection.association_primary_key])
|
|
109
|
-
else # :has_many, :has_one
|
|
110
|
-
through_table[through_reflection.foreign_key].eq(@outer_table_alias[through_reflection.active_record_primary_key])
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
subquery = through_table.project(Arel.sql("1"))
|
|
114
|
-
.where(cond1_arel.and(cond2_arel))
|
|
115
|
-
subquery.exists
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Provides method_missing interface for association access in aggregate blocks
|
|
120
|
-
class CorrelatedRelationBuilder
|
|
121
|
-
def initialize(base_model_class, outer_table_alias)
|
|
122
|
-
@association_resolver = AssociationResolver.new(base_model_class, outer_table_alias)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def method_missing(association_name, *args, &block_arg)
|
|
126
|
-
validate_method_call(association_name, args, block_arg)
|
|
127
|
-
@association_resolver.resolve_association(association_name)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
131
|
-
@association_resolver.association?(method_name) || super
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
private
|
|
135
|
-
|
|
136
|
-
def validate_method_call(association_name, args, block_arg)
|
|
137
|
-
return unless args.any? || block_arg
|
|
138
|
-
|
|
139
|
-
raise ArgumentError,
|
|
140
|
-
"Unexpected arguments or block for association '#{association_name}' in aggregate definition."
|
|
53
|
+
def respond_to_missing?(name, *)
|
|
54
|
+
@model.reflect_on_association(name) || super
|
|
141
55
|
end
|
|
142
56
|
end
|
|
143
57
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
@
|
|
148
|
-
@base_model_class = base_model_class
|
|
58
|
+
class AttrMagic
|
|
59
|
+
def initialize(model, outer_table)
|
|
60
|
+
@model = model
|
|
61
|
+
@outer_table = outer_table
|
|
149
62
|
end
|
|
150
63
|
|
|
151
|
-
def method_missing(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def respond_to_missing?(method_name, include_private = false)
|
|
158
|
-
column?(method_name) || super
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
private
|
|
162
|
-
|
|
163
|
-
def valid_attribute_access?(method_name, args, block_arg)
|
|
164
|
-
args.empty? && block_arg.nil? && column?(method_name)
|
|
64
|
+
def method_missing(name, *args, &)
|
|
65
|
+
if @model.columns_hash.key?(name.to_s)
|
|
66
|
+
@outer_table[name]
|
|
67
|
+
else
|
|
68
|
+
super
|
|
69
|
+
end
|
|
165
70
|
end
|
|
166
71
|
|
|
167
|
-
def
|
|
168
|
-
@
|
|
72
|
+
def respond_to_missing?(name, *)
|
|
73
|
+
@model.columns_hash.key?(name.to_s) || super
|
|
169
74
|
end
|
|
170
75
|
end
|
|
171
76
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@
|
|
176
|
-
@attribute_accessor = OuterTableAttributeAccessor.new(outer_table_alias, base_model_class)
|
|
77
|
+
class ColumnProj
|
|
78
|
+
def initialize(outer_table, model)
|
|
79
|
+
@outer_table = outer_table
|
|
80
|
+
@attr_magic = AttrMagic.new(model, outer_table)
|
|
177
81
|
end
|
|
178
82
|
|
|
179
|
-
def
|
|
180
|
-
if
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
private
|
|
188
|
-
|
|
189
|
-
def build_direct_attribute_projection(aggregate_def)
|
|
190
|
-
@outer_table_alias[aggregate_def.column].as(aggregate_def.name.to_s)
|
|
191
|
-
end
|
|
83
|
+
def build(defn, corr)
|
|
84
|
+
if defn.block?
|
|
85
|
+
val = begin
|
|
86
|
+
defn.block.call(@attr_magic)
|
|
87
|
+
rescue StandardError
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
return val.as(defn.name.to_s) if val.is_a?(Arel::Attributes::Attribute) || val.is_a?(Arel::Nodes::SqlLiteral)
|
|
192
91
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
aliased_attribute_projection
|
|
92
|
+
rel = defn.block.call(corr)
|
|
93
|
+
Arel.sql("(#{rel.to_sql})").as(defn.name.to_s)
|
|
196
94
|
else
|
|
197
|
-
|
|
95
|
+
@outer_table[defn.column].as(defn.name.to_s)
|
|
198
96
|
end
|
|
199
97
|
end
|
|
200
|
-
|
|
201
|
-
def try_aliased_attribute(aggregate_def)
|
|
202
|
-
value = aggregate_def.block.call(@attribute_accessor)
|
|
203
|
-
return nil unless arel_attribute_or_literal?(value)
|
|
204
|
-
|
|
205
|
-
value.as(aggregate_def.name.to_s)
|
|
206
|
-
rescue NoMethodError, ArgumentError
|
|
207
|
-
nil
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def arel_attribute_or_literal?(value)
|
|
211
|
-
value.is_a?(Arel::Attributes::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def build_subquery_projection(aggregate_def, correlation_builder)
|
|
215
|
-
relation = aggregate_def.block.call(correlation_builder)
|
|
216
|
-
validate_subquery_relation(relation, aggregate_def.name)
|
|
217
|
-
|
|
218
|
-
subquery_sql = relation.to_sql
|
|
219
|
-
Arel.sql("(#{subquery_sql})").as(aggregate_def.name.to_s)
|
|
220
|
-
rescue StandardError => e
|
|
221
|
-
raise ArgumentError,
|
|
222
|
-
"Block for column aggregate '#{aggregate_def.name}' failed. " \
|
|
223
|
-
"Not a valid aliased attribute or subquery. Error: #{e.message}"
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
def validate_subquery_relation(relation, aggregate_name)
|
|
227
|
-
return if relation.is_a?(ActiveRecord::Relation)
|
|
228
|
-
|
|
229
|
-
raise ArgumentError,
|
|
230
|
-
"Block for column subquery '#{aggregate_name}' must return an ActiveRecord::Relation. " \
|
|
231
|
-
"Got: #{relation.class}"
|
|
232
|
-
end
|
|
233
98
|
end
|
|
234
99
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
case aggregate_def.type
|
|
100
|
+
module AggSQL
|
|
101
|
+
def self.sql(relation, defn)
|
|
102
|
+
q = relation.except(:select)
|
|
103
|
+
t = q.model.arel_table
|
|
104
|
+
case defn.type
|
|
241
105
|
when :count
|
|
242
|
-
|
|
106
|
+
q.select(Arel.star.count).to_sql
|
|
243
107
|
when :count_expression
|
|
244
|
-
|
|
108
|
+
q.select(Arel.sql("COUNT(#{defn.expression})")).to_sql
|
|
245
109
|
when :count_distinct
|
|
246
|
-
|
|
110
|
+
q.select(t[defn.column].count(true)).to_sql
|
|
247
111
|
when :count_distinct_expression
|
|
248
|
-
|
|
112
|
+
q.select(Arel.sql("COUNT(DISTINCT #{defn.expression})")).to_sql
|
|
249
113
|
when :sum
|
|
250
|
-
|
|
114
|
+
q.select(Arel::Nodes::NamedFunction.new("COALESCE", [t[defn.column].sum, Arel::Nodes.build_quoted(0)])).to_sql
|
|
251
115
|
when :sum_expression
|
|
252
|
-
|
|
116
|
+
q.select(Arel.sql("COALESCE(SUM(#{defn.expression}), 0)")).to_sql
|
|
253
117
|
when :avg
|
|
254
|
-
|
|
118
|
+
q.select(Arel::Nodes::NamedFunction.new("COALESCE", [t[defn.column].average, Arel::Nodes.build_quoted(0.0)])).to_sql
|
|
255
119
|
when :avg_expression
|
|
256
|
-
|
|
120
|
+
q.select(Arel.sql("COALESCE(AVG(#{defn.expression}), 0.0)")).to_sql
|
|
257
121
|
when :min
|
|
258
|
-
|
|
122
|
+
q.select(Arel::Nodes::NamedFunction.new("COALESCE", [t[defn.column].minimum, Arel::Nodes.build_quoted(0)])).to_sql
|
|
259
123
|
when :min_expression
|
|
260
|
-
|
|
124
|
+
q.select(Arel.sql("COALESCE(MIN(#{defn.expression}), 0)")).to_sql
|
|
261
125
|
when :max
|
|
262
|
-
|
|
126
|
+
q.select(Arel::Nodes::NamedFunction.new("COALESCE", [t[defn.column].maximum, Arel::Nodes.build_quoted(0)])).to_sql
|
|
263
127
|
when :max_expression
|
|
264
|
-
|
|
128
|
+
q.select(Arel.sql("COALESCE(MAX(#{defn.expression}), 0)")).to_sql
|
|
265
129
|
when :string_agg
|
|
266
|
-
|
|
130
|
+
delim = defn.options&.dig(:delimiter)
|
|
131
|
+
args = [t[defn.column]]
|
|
132
|
+
args << Arel::Nodes.build_quoted(delim) if delim
|
|
133
|
+
q.select(Arel::Nodes::NamedFunction.new("GROUP_CONCAT", args)).to_sql
|
|
267
134
|
when :string_agg_expression
|
|
268
|
-
|
|
135
|
+
delim = defn.options&.dig(:delimiter)
|
|
136
|
+
args = [Arel.sql(defn.expression)]
|
|
137
|
+
args << Arel::Nodes.build_quoted(delim) if delim
|
|
138
|
+
q.select(Arel::Nodes::NamedFunction.new("GROUP_CONCAT", args)).to_sql
|
|
269
139
|
else
|
|
270
|
-
raise
|
|
140
|
+
raise "Unknown aggregate type: #{defn.type}"
|
|
271
141
|
end
|
|
272
142
|
end
|
|
273
|
-
|
|
274
|
-
private
|
|
275
|
-
|
|
276
|
-
def build_count_query(base_query)
|
|
277
|
-
base_query.select(Arel.star.count).to_sql
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def build_count_expression_query(base_query, expression)
|
|
281
|
-
base_query.select(Arel.sql("COUNT(#{expression})")).to_sql
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def build_count_distinct_query(base_query, column)
|
|
285
|
-
table = base_query.model.arel_table
|
|
286
|
-
base_query.select(table[column].count(true)).to_sql
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def build_count_distinct_expression_query(base_query, expression)
|
|
290
|
-
base_query.select(Arel.sql("COUNT(DISTINCT #{expression})")).to_sql
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def build_sum_query(base_query, column)
|
|
294
|
-
table = base_query.model.arel_table
|
|
295
|
-
sum_agg = table[column].sum
|
|
296
|
-
coalesced_sum = Arel::Nodes::NamedFunction.new("COALESCE", [sum_agg, Arel::Nodes.build_quoted(0)])
|
|
297
|
-
base_query.select(coalesced_sum).to_sql
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def build_sum_expression_query(base_query, expression)
|
|
301
|
-
base_query.select(Arel.sql("COALESCE(SUM(#{expression}), 0)")).to_sql
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
def build_avg_query(base_query, column)
|
|
305
|
-
table = base_query.model.arel_table
|
|
306
|
-
avg_agg = table[column].average
|
|
307
|
-
coalesced_avg = Arel::Nodes::NamedFunction.new("COALESCE", [avg_agg, Arel::Nodes.build_quoted(0.0)])
|
|
308
|
-
base_query.select(coalesced_avg).to_sql
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
def build_avg_expression_query(base_query, expression)
|
|
312
|
-
base_query.select(Arel.sql("COALESCE(AVG(#{expression}), 0.0)")).to_sql
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def build_min_query(base_query, column)
|
|
316
|
-
table = base_query.model.arel_table
|
|
317
|
-
min_agg = table[column].minimum
|
|
318
|
-
coalesced_min = Arel::Nodes::NamedFunction.new("COALESCE", [min_agg, Arel::Nodes.build_quoted(0)])
|
|
319
|
-
base_query.select(coalesced_min).to_sql
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def build_min_expression_query(base_query, expression)
|
|
323
|
-
base_query.select(Arel.sql("COALESCE(MIN(#{expression}), 0)")).to_sql
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
def build_max_query(base_query, column)
|
|
327
|
-
table = base_query.model.arel_table
|
|
328
|
-
max_agg = table[column].maximum
|
|
329
|
-
coalesced_max = Arel::Nodes::NamedFunction.new("COALESCE", [max_agg, Arel::Nodes.build_quoted(0)])
|
|
330
|
-
base_query.select(coalesced_max).to_sql
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
def build_max_expression_query(base_query, expression)
|
|
334
|
-
base_query.select(Arel.sql("COALESCE(MAX(#{expression}), 0)")).to_sql
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def build_string_agg_query(base_query, column, options)
|
|
338
|
-
table = base_query.model.arel_table
|
|
339
|
-
delimiter = options&.dig(:delimiter)
|
|
340
|
-
|
|
341
|
-
args = [table[column]]
|
|
342
|
-
args << Arel::Nodes.build_quoted(delimiter) if delimiter
|
|
343
|
-
|
|
344
|
-
base_query.select(Arel::Nodes::NamedFunction.new("GROUP_CONCAT", args)).to_sql
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def build_string_agg_expression_query(base_query, expression, options)
|
|
348
|
-
delimiter = options&.dig(:delimiter)
|
|
349
|
-
|
|
350
|
-
args = [Arel.sql(expression)]
|
|
351
|
-
args << Arel::Nodes.build_quoted(delimiter) if delimiter
|
|
352
|
-
|
|
353
|
-
base_query.select(Arel::Nodes::NamedFunction.new("GROUP_CONCAT", args)).to_sql
|
|
354
|
-
end
|
|
355
143
|
end
|
|
356
144
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
@
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
return processed_columns unless scope.respond_to?(:where_values_hash)
|
|
375
|
-
return processed_columns unless scope.where_values_hash.is_a?(Hash)
|
|
376
|
-
|
|
377
|
-
scope.where_values_hash.each do |column_name, value|
|
|
378
|
-
column_name_str = column_name.to_s
|
|
379
|
-
|
|
380
|
-
if model_column?(column_name_str)
|
|
381
|
-
arel_query.where(@outer_table_alias[column_name.to_sym].eq(value))
|
|
382
|
-
processed_columns << column_name_str
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
processed_columns
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def apply_arel_where_conditions(arel_query, scope, processed_columns)
|
|
390
|
-
where_clauses = extract_where_clauses(scope)
|
|
391
|
-
|
|
392
|
-
where_clauses.each do |constraint|
|
|
393
|
-
next unless simple_table_constraint?(constraint, processed_columns)
|
|
394
|
-
|
|
395
|
-
apply_constraint_to_query(arel_query, constraint)
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
def extract_where_clauses(scope)
|
|
400
|
-
scope.arel.ast.cores.flat_map(&:wheres)
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def simple_table_constraint?(constraint, processed_columns)
|
|
404
|
-
return false unless constraint.left.is_a?(Arel::Attributes::Attribute)
|
|
405
|
-
return false unless constraint.left.relation.name == @base_model.table_name
|
|
406
|
-
return false if processed_columns.include?(constraint.left.name.to_s)
|
|
407
|
-
|
|
408
|
-
true
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def apply_constraint_to_query(arel_query, constraint)
|
|
412
|
-
if constraint.is_a?(Arel::Nodes::Equality)
|
|
413
|
-
apply_equality_constraint(arel_query, constraint)
|
|
414
|
-
elsif constraint.is_a?(Arel::Nodes::In)
|
|
415
|
-
apply_in_constraint(arel_query, constraint)
|
|
145
|
+
class Query
|
|
146
|
+
def initialize(model, aggs)
|
|
147
|
+
@model = model
|
|
148
|
+
@aggs = aggs
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def build(scope)
|
|
152
|
+
outer = @model.arel_table.alias("batchagg_outer_#{@model.table_name}")
|
|
153
|
+
corr = AssocMagic.new(@model, outer)
|
|
154
|
+
colproj = ColumnProj.new(outer, @model)
|
|
155
|
+
arel = Arel::SelectManager.new(scope.klass.connection)
|
|
156
|
+
arel.from(outer)
|
|
157
|
+
arel.project(outer[@model.primary_key].as(@model.primary_key.to_s))
|
|
158
|
+
@aggs.reject(&:computed?).each do |agg|
|
|
159
|
+
proj = agg.column_based? ? colproj.build(agg, corr) : Arel.sql("(#{AggSQL.sql(agg.block.call(corr), agg)})").as(agg.name.to_s)
|
|
160
|
+
arel.project(proj)
|
|
416
161
|
end
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
def apply_in_constraint(arel_query, constraint)
|
|
426
|
-
column_name = constraint.left.name
|
|
427
|
-
values = extract_in_clause_values(constraint.right)
|
|
428
|
-
arel_query.where(@outer_table_alias[column_name].in(values))
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
def extract_in_clause_values(right_operand)
|
|
432
|
-
return right_operand unless right_operand.is_a?(Array)
|
|
433
|
-
|
|
434
|
-
right_operand.map do |value|
|
|
435
|
-
value.is_a?(Arel::Nodes::BindParam) ? value.value.value_before_type_cast : value
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
def model_column?(column_name)
|
|
440
|
-
@base_model.columns_hash.key?(column_name)
|
|
162
|
+
arel.where(
|
|
163
|
+
outer[@model.primary_key].in(
|
|
164
|
+
Arel.sql("(#{scope.select(@model.primary_key).to_sql})")
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
arel
|
|
441
168
|
end
|
|
442
169
|
end
|
|
443
170
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
@
|
|
448
|
-
@
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def build_query_for_scope(scope, aggregates)
|
|
452
|
-
outer_table_alias = create_outer_table_alias
|
|
453
|
-
builders = create_helper_builders(outer_table_alias)
|
|
454
|
-
|
|
455
|
-
arel_query = create_base_query(scope, outer_table_alias)
|
|
456
|
-
# Filter out computed aggregates before building SQL projections
|
|
457
|
-
sql_aggregates = aggregates.reject(&:computed?)
|
|
458
|
-
add_projections_to_query(arel_query, sql_aggregates, builders, outer_table_alias)
|
|
459
|
-
apply_scope_conditions(arel_query, scope, outer_table_alias)
|
|
460
|
-
|
|
461
|
-
arel_query
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
private
|
|
465
|
-
|
|
466
|
-
def create_outer_table_alias
|
|
467
|
-
@base_model.arel_table.alias("batchagg_outer_#{@base_model.table_name}")
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def create_helper_builders(outer_table_alias)
|
|
471
|
-
{
|
|
472
|
-
correlation: CorrelatedRelationBuilder.new(@base_model, outer_table_alias),
|
|
473
|
-
column_projection: ColumnProjectionBuilder.new(outer_table_alias, @base_model)
|
|
474
|
-
}
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
def create_base_query(scope, outer_table_alias)
|
|
478
|
-
arel_query = Arel::SelectManager.new(scope.klass.connection)
|
|
479
|
-
arel_query.from(outer_table_alias)
|
|
480
|
-
|
|
481
|
-
primary_key_projection = outer_table_alias[@base_model.primary_key].as(@base_model.primary_key.to_s)
|
|
482
|
-
arel_query.project(primary_key_projection)
|
|
483
|
-
|
|
484
|
-
arel_query
|
|
171
|
+
class Runner
|
|
172
|
+
def initialize(aggs, model)
|
|
173
|
+
@aggs = aggs
|
|
174
|
+
@model = model
|
|
175
|
+
@query = Query.new(model, aggs)
|
|
176
|
+
@result_class = build_result_class(aggs)
|
|
485
177
|
end
|
|
486
178
|
|
|
487
|
-
def
|
|
488
|
-
|
|
489
|
-
# At this point, aggregate_def should not be a computed? one due to filtering above
|
|
490
|
-
projection = build_projection_for_aggregate(aggregate_def, builders)
|
|
491
|
-
arel_query.project(projection)
|
|
492
|
-
end
|
|
179
|
+
def only(record)
|
|
180
|
+
from(@model.where(@model.primary_key => record.id))
|
|
493
181
|
end
|
|
494
182
|
|
|
495
|
-
def
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
183
|
+
def from(scope)
|
|
184
|
+
rows = scope.klass.connection.select_all(@query.build(scope).to_sql).to_a
|
|
185
|
+
rows.each_with_object({}) do |row, h|
|
|
186
|
+
id = row[@model.primary_key.to_s]
|
|
187
|
+
h[@model.primary_key ? id : row.keys.first] = @result_class.new(row)
|
|
500
188
|
end
|
|
501
189
|
end
|
|
502
190
|
|
|
503
|
-
def build_aggregate_function_projection(aggregate_def, correlation_builder)
|
|
504
|
-
correlated_relation = aggregate_def.block.call(correlation_builder)
|
|
505
|
-
subquery_sql = @subquery_builder.build_subquery_sql(correlated_relation, aggregate_def)
|
|
506
|
-
Arel.sql("(#{subquery_sql})").as(aggregate_def.name.to_s)
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
def apply_scope_conditions(arel_query, scope, outer_table_alias)
|
|
510
|
-
condition_applier = ScopeConditionApplier.new(@base_model, outer_table_alias)
|
|
511
|
-
condition_applier.apply_conditions_to_query(arel_query, scope)
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
# Handles type casting of primary key values for hash keys
|
|
516
|
-
class PrimaryKeyTypeConverter
|
|
517
|
-
def self.cast_for_hash_key(id_value, model_class)
|
|
518
|
-
primary_key_column = find_primary_key_column(model_class)
|
|
519
|
-
type_converter = create_type_converter(primary_key_column)
|
|
520
|
-
type_converter.deserialize(id_value)
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
def self.find_primary_key_column(model_class)
|
|
524
|
-
model_class.columns_hash[model_class.primary_key]
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
def self.create_type_converter(primary_key_column)
|
|
528
|
-
ActiveRecord::Type.lookup(
|
|
529
|
-
primary_key_column.type,
|
|
530
|
-
limit: primary_key_column.limit,
|
|
531
|
-
precision: primary_key_column.precision,
|
|
532
|
-
scale: primary_key_column.scale
|
|
533
|
-
)
|
|
534
|
-
end
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
# Represents aggregate results for a single record
|
|
538
|
-
class AggregateResultForRecord
|
|
539
|
-
def initialize(row_data, all_aggregate_definitions)
|
|
540
|
-
@data = symbolize_keys(row_data)
|
|
541
|
-
@all_aggregate_definitions = all_aggregate_definitions
|
|
542
|
-
@computed_cache = {}
|
|
543
|
-
|
|
544
|
-
sql_aggregate_definitions = @all_aggregate_definitions.reject(&:computed?)
|
|
545
|
-
define_sql_aggregate_methods(sql_aggregate_definitions)
|
|
546
|
-
|
|
547
|
-
computed_aggregate_definitions = @all_aggregate_definitions.select(&:computed?)
|
|
548
|
-
define_computed_aggregate_methods(computed_aggregate_definitions)
|
|
549
|
-
end
|
|
550
|
-
|
|
551
191
|
private
|
|
552
192
|
|
|
553
|
-
def
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
sql_aggregate_definitions.each do |aggregate_def|
|
|
559
|
-
define_singleton_method(aggregate_def.name) do
|
|
560
|
-
@data[aggregate_def.name]
|
|
193
|
+
def build_result_class(aggs)
|
|
194
|
+
klass = Class.new do
|
|
195
|
+
define_method(:initialize) do |row|
|
|
196
|
+
@data = row.transform_keys(&:to_sym)
|
|
197
|
+
@cache = {}
|
|
561
198
|
end
|
|
562
199
|
end
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
def define_computed_aggregate_methods(computed_aggregate_definitions)
|
|
566
|
-
computed_aggregate_definitions.each do |aggregate_def|
|
|
567
|
-
define_singleton_method(aggregate_def.name) do
|
|
568
|
-
return @computed_cache[aggregate_def.name] if @computed_cache.key?(aggregate_def.name)
|
|
569
|
-
|
|
570
|
-
value = instance_exec(self, &aggregate_def.block)
|
|
571
|
-
@computed_cache[aggregate_def.name] = value
|
|
572
|
-
value
|
|
573
|
-
end
|
|
200
|
+
aggs.reject(&:computed?).each do |agg|
|
|
201
|
+
klass.define_method(agg.name) { @data[agg.name] }
|
|
574
202
|
end
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
class AggregateResultProcessor
|
|
580
|
-
def initialize(aggregates, base_model)
|
|
581
|
-
@aggregates = aggregates
|
|
582
|
-
@base_model = base_model
|
|
583
|
-
@query_builder = QueryBuilder.new(base_model)
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
def process_single_record(record)
|
|
587
|
-
scope = create_single_record_scope(record)
|
|
588
|
-
process_scope(scope)
|
|
589
|
-
end
|
|
590
|
-
|
|
591
|
-
def process_scope(scope)
|
|
592
|
-
query_results = execute_query(scope)
|
|
593
|
-
convert_results_to_hash(query_results, scope.klass)
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
private
|
|
597
|
-
|
|
598
|
-
def create_single_record_scope(record)
|
|
599
|
-
@base_model.where(@base_model.primary_key => record.id)
|
|
600
|
-
end
|
|
601
|
-
|
|
602
|
-
def execute_query(scope)
|
|
603
|
-
query_arel = @query_builder.build_query_for_scope(scope, @aggregates)
|
|
604
|
-
scope.klass.connection.select_all(query_arel.to_sql).to_a
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
def convert_results_to_hash(query_results, model_class)
|
|
608
|
-
query_results.each_with_object({}) do |row_hash, result_hash|
|
|
609
|
-
record_id = extract_and_cast_record_id(row_hash, model_class)
|
|
610
|
-
result_hash[record_id] = AggregateResultForRecord.new(row_hash, @aggregates)
|
|
203
|
+
aggs.select(&:computed?).each do |agg|
|
|
204
|
+
klass.define_method(agg.name) do
|
|
205
|
+
@cache[agg.name] ||= instance_exec(self, &agg.block)
|
|
206
|
+
end
|
|
611
207
|
end
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def extract_and_cast_record_id(row_hash, model_class)
|
|
615
|
-
raw_id = row_hash[@base_model.primary_key.to_s]
|
|
616
|
-
PrimaryKeyTypeConverter.cast_for_hash_key(raw_id, model_class)
|
|
208
|
+
klass
|
|
617
209
|
end
|
|
618
210
|
end
|
|
619
211
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
def
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
def
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
def
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
def column(name, &block)
|
|
642
|
-
add_aggregate(:column, name, block, column: name)
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
def count(name, &block)
|
|
646
|
-
add_aggregate(:count, name, block)
|
|
647
|
-
end
|
|
648
|
-
|
|
649
|
-
def count_distinct(name, column, &block)
|
|
650
|
-
add_aggregate(:count_distinct, name, block, column: column)
|
|
651
|
-
end
|
|
652
|
-
|
|
653
|
-
def count_expression(name, expression, &block)
|
|
654
|
-
add_aggregate(:count_expression, name, block, expression: expression)
|
|
655
|
-
end
|
|
656
|
-
|
|
657
|
-
def count_distinct_expression(name, expression, &block)
|
|
658
|
-
add_aggregate(:count_distinct_expression, name, block, expression: expression)
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
def sum(name, column, &block)
|
|
662
|
-
add_aggregate(:sum, name, block, column: column)
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
def sum_expression(name, expression, &block)
|
|
666
|
-
add_aggregate(:sum_expression, name, block, expression: expression)
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
def avg(name, column, &block)
|
|
670
|
-
add_aggregate(:avg, name, block, column: column)
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
def avg_expression(name, expression, &block)
|
|
674
|
-
add_aggregate(:avg_expression, name, block, expression: expression)
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
def min(name, column, &block)
|
|
678
|
-
add_aggregate(:min, name, block, column: column)
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
def min_expression(name, expression, &block)
|
|
682
|
-
add_aggregate(:min_expression, name, block, expression: expression)
|
|
683
|
-
end
|
|
684
|
-
|
|
685
|
-
def max(name, column, &block)
|
|
686
|
-
add_aggregate(:max, name, block, column: column)
|
|
687
|
-
end
|
|
688
|
-
|
|
689
|
-
def max_expression(name, expression, &block)
|
|
690
|
-
add_aggregate(:max_expression, name, block, expression: expression)
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
def string_agg(name, column, delimiter: nil, &block)
|
|
694
|
-
add_aggregate(:string_agg, name, block, column: column, options: { delimiter: delimiter })
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
def string_agg_expression(name, expression, delimiter: nil, &block)
|
|
698
|
-
add_aggregate(:string_agg_expression, name, block, expression: expression, options: { delimiter: delimiter })
|
|
699
|
-
end
|
|
700
|
-
|
|
701
|
-
def computed(name, &block)
|
|
702
|
-
add_aggregate(:computed, name, block)
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
attr_reader :aggregates
|
|
212
|
+
class Collector
|
|
213
|
+
attr_reader :aggs
|
|
214
|
+
|
|
215
|
+
def initialize = @aggs = []
|
|
216
|
+
def column(name, &block) = add(:column, name, block, column: name)
|
|
217
|
+
def count(name, &block) = add(:count, name, block)
|
|
218
|
+
def count_distinct(name, column, &block) = add(:count_distinct, name, block, column: column)
|
|
219
|
+
def count_expression(name, expr, &block) = add(:count_expression, name, block, expression: expr)
|
|
220
|
+
def count_distinct_expression(name, expr, &block) = add(:count_distinct_expression, name, block, expression: expr)
|
|
221
|
+
def sum(name, column, &block) = add(:sum, name, block, column: column)
|
|
222
|
+
def sum_expression(name, expr, &block) = add(:sum_expression, name, block, expression: expr)
|
|
223
|
+
def avg(name, column, &block) = add(:avg, name, block, column: column)
|
|
224
|
+
def avg_expression(name, expr, &block) = add(:avg_expression, name, block, expression: expr)
|
|
225
|
+
def min(name, column, &block) = add(:min, name, block, column: column)
|
|
226
|
+
def min_expression(name, expr, &block) = add(:min_expression, name, block, expression: expr)
|
|
227
|
+
def max(name, column, &block) = add(:max, name, block, column: column)
|
|
228
|
+
def max_expression(name, expr, &block) = add(:max_expression, name, block, expression: expr)
|
|
229
|
+
def string_agg(name, column, delimiter: nil, &block) = add(:string_agg, name, block, column: column, options: { delimiter: delimiter })
|
|
230
|
+
def string_agg_expression(name, expr, delimiter: nil, &block) = add(:string_agg_expression, name, block, expression: expr, options: { delimiter: delimiter })
|
|
231
|
+
def computed(name, &block) = add(:computed, name, block)
|
|
706
232
|
|
|
707
233
|
private
|
|
708
234
|
|
|
709
|
-
def
|
|
710
|
-
@
|
|
235
|
+
def add(type, name, block, column: nil, expression: nil, options: nil)
|
|
236
|
+
@aggs << AggregateDef.new(name: name, type: type, block: block, column: column, expression: expression, options: options)
|
|
711
237
|
end
|
|
712
238
|
end
|
|
713
239
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
def initialize(base_model)
|
|
717
|
-
@base_model = base_model
|
|
718
|
-
end
|
|
240
|
+
class Builder
|
|
241
|
+
def initialize(model) = @model = model
|
|
719
242
|
|
|
720
243
|
def build_class(&)
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
AggregateResultClass.new(aggregates, @base_model)
|
|
244
|
+
col = Collector.new
|
|
245
|
+
col.instance_eval(&)
|
|
246
|
+
Runner.new(col.aggs, @model)
|
|
726
247
|
end
|
|
727
248
|
end
|
|
728
249
|
|
|
729
|
-
# Public DSL module
|
|
730
250
|
module DSL
|
|
731
|
-
def aggregate(
|
|
732
|
-
|
|
733
|
-
builder.build_class(&)
|
|
251
|
+
def aggregate(model, &)
|
|
252
|
+
Builder.new(model).build_class(&)
|
|
734
253
|
end
|
|
735
254
|
end
|
|
736
255
|
end
|