simple_query 0.4.0 → 0.5.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 +88 -0
- data/lib/simple_query/builder.rb +127 -2
- data/lib/simple_query/clauses/aggregation_clause.rb +155 -0
- data/lib/simple_query/version.rb +1 -1
- data/lib/simple_query.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 856dc1549df83e6c82a7105b3dfa97b9c6f4e69b86f6c2f9ad653ae1f322d1e8
|
4
|
+
data.tar.gz: c10490e13ffbc3da9c629081cde483188efbc0ec3fbdcf27070d565947cc723a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 627f5f7fe8a02a9bdf949f8b4d681fc811a6081b0dc2666f60baacff55d6136a3e28512d8c44a1d5cca1699a6529716b36daa4aca455a1ed070efdca935b078a
|
7
|
+
data.tar.gz: 056dbe8add051110bc32cb79264128e87804d4839b0e1f74172353d6f205a59080a54ed463f04ae82efb239237f4d3d0e4ad8f03bc96bdbc607f1c1a0ec68ff9
|
data/README.md
CHANGED
@@ -104,6 +104,94 @@ User.simple_query
|
|
104
104
|
.execute
|
105
105
|
```
|
106
106
|
|
107
|
+
## Enhanced Aggregation Support
|
108
|
+
|
109
|
+
SimpleQuery provides a comprehensive set of aggregation methods that are more convenient and readable than writing raw SQL:
|
110
|
+
|
111
|
+
### Basic Aggregations
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# Count records
|
115
|
+
User.simple_query.count.execute
|
116
|
+
# => #<struct count=1000>
|
117
|
+
|
118
|
+
# Count specific column (non-null values)
|
119
|
+
User.simple_query.count(:email).execute
|
120
|
+
# => #<struct count_email=995>
|
121
|
+
|
122
|
+
# Sum values
|
123
|
+
Company.simple_query.sum(:annual_revenue).execute
|
124
|
+
# => #<struct sum_annual_revenue=50000000>
|
125
|
+
|
126
|
+
# Average values
|
127
|
+
Company.simple_query.avg(:annual_revenue).execute
|
128
|
+
# => #<struct avg_annual_revenue=1000000.5>
|
129
|
+
|
130
|
+
# Find minimum and maximum
|
131
|
+
Company.simple_query.min(:annual_revenue).max(:annual_revenue).execute
|
132
|
+
# => #<struct min_annual_revenue=100000, max_annual_revenue=5000000>
|
133
|
+
```
|
134
|
+
|
135
|
+
### Statistical Functions
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
# Variance and standard deviation
|
139
|
+
User.simple_query.variance(:score).stddev(:score).execute
|
140
|
+
# => #<struct variance_score=125.67, stddev_score=11.21>
|
141
|
+
|
142
|
+
# Database-specific group concatenation
|
143
|
+
User.simple_query
|
144
|
+
.select(:department)
|
145
|
+
.group_concat(:name, separator: ", ")
|
146
|
+
.group(:department)
|
147
|
+
.execute
|
148
|
+
# => #<struct department="Engineering", group_concat_name="Alice, Bob, Charlie">
|
149
|
+
```
|
150
|
+
|
151
|
+
### Advanced Aggregation Features
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
# Get comprehensive statistics for a column
|
155
|
+
Company.simple_query.stats(:annual_revenue).execute
|
156
|
+
# => #<struct
|
157
|
+
# annual_revenue_count=100,
|
158
|
+
# annual_revenue_sum=50000000,
|
159
|
+
# annual_revenue_avg=500000,
|
160
|
+
# annual_revenue_min=100000,
|
161
|
+
# annual_revenue_max=2000000
|
162
|
+
# >
|
163
|
+
|
164
|
+
# Custom aggregations
|
165
|
+
Company.simple_query
|
166
|
+
.custom_aggregation("COUNT(DISTINCT industry)", "unique_industries")
|
167
|
+
.execute
|
168
|
+
# => #<struct unique_industries=5>
|
169
|
+
|
170
|
+
# Combining with other features
|
171
|
+
Company.simple_query
|
172
|
+
.select(:industry)
|
173
|
+
.count
|
174
|
+
.sum(:annual_revenue)
|
175
|
+
.group(:industry)
|
176
|
+
.execute
|
177
|
+
# => [
|
178
|
+
# #<struct industry="Technology", count=50, sum_annual_revenue=25000000>,
|
179
|
+
# #<struct industry="Finance", count=30, sum_annual_revenue=20000000>
|
180
|
+
# ]
|
181
|
+
```
|
182
|
+
|
183
|
+
### Custom Aliases
|
184
|
+
|
185
|
+
All aggregation methods support custom aliases:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
User.simple_query
|
189
|
+
.count(:id, alias_name: "total_users")
|
190
|
+
.sum(:score, alias_name: "total_score")
|
191
|
+
.execute
|
192
|
+
# => #<struct total_users=1000, total_score=85000>
|
193
|
+
```
|
194
|
+
|
107
195
|
## Custom Read Models
|
108
196
|
By default, SimpleQuery returns results as `Struct` objects for maximum speed. However, you can also define a lightweight model class for more explicit attribute handling or custom logic.
|
109
197
|
|
data/lib/simple_query/builder.rb
CHANGED
@@ -18,6 +18,7 @@ module SimpleQuery
|
|
18
18
|
@orders = OrderClause.new(@arel_table)
|
19
19
|
@limits = LimitOffsetClause.new
|
20
20
|
@distinct_flag = DistinctClause.new
|
21
|
+
@aggregations = AggregationClause.new(@arel_table)
|
21
22
|
|
22
23
|
@query_cache = {}
|
23
24
|
@query_built = false
|
@@ -91,6 +92,98 @@ 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
|
+
alias_name)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Percentage calculations
|
180
|
+
def percentage_of_total(column, alias_name: nil)
|
181
|
+
alias_name ||= "#{column}_percentage"
|
182
|
+
column_expr = resolve_column_name(column)
|
183
|
+
expression = "ROUND((#{column_expr} * 100.0 / SUM(#{column_expr}) OVER ()), 2)"
|
184
|
+
custom_aggregation(expression, alias_name)
|
185
|
+
end
|
186
|
+
|
94
187
|
def map_to(klass)
|
95
188
|
@read_model_class = klass
|
96
189
|
reset_query
|
@@ -145,7 +238,10 @@ module SimpleQuery
|
|
145
238
|
|
146
239
|
@query = Arel::SelectManager.new(Arel::Table.engine)
|
147
240
|
@query.from(@arel_table)
|
148
|
-
|
241
|
+
|
242
|
+
# Combine regular selects with aggregations
|
243
|
+
all_selects = build_select_expressions
|
244
|
+
@query.project(*all_selects)
|
149
245
|
|
150
246
|
apply_distinct
|
151
247
|
apply_where_conditions
|
@@ -160,6 +256,23 @@ module SimpleQuery
|
|
160
256
|
|
161
257
|
private
|
162
258
|
|
259
|
+
def build_select_expressions
|
260
|
+
expressions = []
|
261
|
+
|
262
|
+
# Add regular selects
|
263
|
+
if @selects.any?
|
264
|
+
expressions.concat(@selects)
|
265
|
+
elsif @aggregations.empty?
|
266
|
+
# Only use * if no aggregations and no explicit selects
|
267
|
+
expressions << @arel_table[Arel.star]
|
268
|
+
end
|
269
|
+
|
270
|
+
# Add aggregation expressions
|
271
|
+
expressions.concat(@aggregations.to_arel_expressions)
|
272
|
+
|
273
|
+
expressions
|
274
|
+
end
|
275
|
+
|
163
276
|
def build_where_sql
|
164
277
|
condition = @wheres.to_arel
|
165
278
|
return "" unless condition
|
@@ -182,7 +295,8 @@ module SimpleQuery
|
|
182
295
|
@orders.orders,
|
183
296
|
@limits.limit_value,
|
184
297
|
@limits.offset_value,
|
185
|
-
@distinct_flag.use_distinct
|
298
|
+
@distinct_flag.use_distinct?,
|
299
|
+
@aggregations.aggregations
|
186
300
|
]
|
187
301
|
|
188
302
|
@query_cache[key] ||= build_query.to_sql
|
@@ -267,6 +381,17 @@ module SimpleQuery
|
|
267
381
|
end
|
268
382
|
end
|
269
383
|
|
384
|
+
def resolve_column_name(column)
|
385
|
+
case column
|
386
|
+
when Symbol
|
387
|
+
"#{@arel_table.name}.#{column}"
|
388
|
+
when String
|
389
|
+
column.include?(".") ? column : "#{@arel_table.name}.#{column}"
|
390
|
+
else
|
391
|
+
column.to_s
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
270
395
|
def method_missing(method_name, *args, &block)
|
271
396
|
if (scope_block = find_scope(method_name))
|
272
397
|
instance_exec(*args, &scope_block)
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleQuery
|
4
|
+
class AggregationClause
|
5
|
+
attr_reader :aggregations
|
6
|
+
|
7
|
+
def initialize(table)
|
8
|
+
@table = table
|
9
|
+
@aggregations = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def count(column = nil, alias_name: nil)
|
13
|
+
column_expr = column ? resolve_column(column) : "*"
|
14
|
+
alias_name ||= column ? "count_#{sanitize_alias(column)}" : "count"
|
15
|
+
|
16
|
+
add_aggregation("COUNT", column_expr, alias_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def sum(column, alias_name: nil)
|
20
|
+
raise ArgumentError, "Column is required for SUM aggregation" if column.nil?
|
21
|
+
|
22
|
+
column_expr = resolve_column(column)
|
23
|
+
alias_name ||= "sum_#{sanitize_alias(column)}"
|
24
|
+
|
25
|
+
add_aggregation("SUM", column_expr, alias_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def avg(column, alias_name: nil)
|
29
|
+
raise ArgumentError, "Column is required for AVG aggregation" if column.nil?
|
30
|
+
|
31
|
+
column_expr = resolve_column(column)
|
32
|
+
alias_name ||= "avg_#{sanitize_alias(column)}"
|
33
|
+
|
34
|
+
add_aggregation("AVG", column_expr, alias_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def min(column, alias_name: nil)
|
38
|
+
raise ArgumentError, "Column is required for MIN aggregation" if column.nil?
|
39
|
+
|
40
|
+
column_expr = resolve_column(column)
|
41
|
+
alias_name ||= "min_#{sanitize_alias(column)}"
|
42
|
+
|
43
|
+
add_aggregation("MIN", column_expr, alias_name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def max(column, alias_name: nil)
|
47
|
+
raise ArgumentError, "Column is required for MAX aggregation" if column.nil?
|
48
|
+
|
49
|
+
column_expr = resolve_column(column)
|
50
|
+
alias_name ||= "max_#{sanitize_alias(column)}"
|
51
|
+
|
52
|
+
add_aggregation("MAX", column_expr, alias_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def custom(expression, alias_name)
|
56
|
+
if expression.nil? || alias_name.nil?
|
57
|
+
raise ArgumentError,
|
58
|
+
"Expression and alias are required for custom aggregation"
|
59
|
+
end
|
60
|
+
|
61
|
+
@aggregations << {
|
62
|
+
expression: expression,
|
63
|
+
alias: alias_name
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Statistical functions
|
68
|
+
def variance(column, alias_name: nil)
|
69
|
+
raise ArgumentError, "Column is required for VARIANCE aggregation" if column.nil?
|
70
|
+
|
71
|
+
column_expr = resolve_column(column)
|
72
|
+
alias_name ||= "variance_#{sanitize_alias(column)}"
|
73
|
+
|
74
|
+
add_aggregation("VARIANCE", column_expr, alias_name)
|
75
|
+
end
|
76
|
+
|
77
|
+
def stddev(column, alias_name: nil)
|
78
|
+
raise ArgumentError, "Column is required for STDDEV aggregation" if column.nil?
|
79
|
+
|
80
|
+
column_expr = resolve_column(column)
|
81
|
+
alias_name ||= "stddev_#{sanitize_alias(column)}"
|
82
|
+
|
83
|
+
add_aggregation("STDDEV", column_expr, alias_name)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Group concatenation (database-specific)
|
87
|
+
def group_concat(column, separator: ",", alias_name: nil)
|
88
|
+
raise ArgumentError, "Column is required for GROUP_CONCAT aggregation" if column.nil?
|
89
|
+
|
90
|
+
column_expr = resolve_column(column)
|
91
|
+
alias_name ||= "group_concat_#{sanitize_alias(column)}"
|
92
|
+
|
93
|
+
# Use database-specific group concatenation
|
94
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
95
|
+
|
96
|
+
expression = case adapter
|
97
|
+
when /mysql/
|
98
|
+
"GROUP_CONCAT(#{column_expr} SEPARATOR '#{separator}')"
|
99
|
+
when /postgres/
|
100
|
+
"STRING_AGG(#{column_expr}::text, '#{separator}')"
|
101
|
+
when /sqlite/
|
102
|
+
"GROUP_CONCAT(#{column_expr}, '#{separator}')"
|
103
|
+
else
|
104
|
+
# Fallback for other databases
|
105
|
+
"GROUP_CONCAT(#{column_expr})"
|
106
|
+
end
|
107
|
+
|
108
|
+
@aggregations << {
|
109
|
+
expression: expression,
|
110
|
+
alias: alias_name
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_arel_expressions
|
115
|
+
@aggregations.map do |agg|
|
116
|
+
Arel.sql("#{agg[:expression]} AS #{agg[:alias]}")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def empty?
|
121
|
+
@aggregations.empty?
|
122
|
+
end
|
123
|
+
|
124
|
+
def clear
|
125
|
+
@aggregations.clear
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def add_aggregation(function, column_expr, alias_name)
|
131
|
+
@aggregations << {
|
132
|
+
expression: "#{function}(#{column_expr})",
|
133
|
+
alias: alias_name
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
def resolve_column(column)
|
138
|
+
case column
|
139
|
+
when Symbol
|
140
|
+
"#{@table.name}.#{column}"
|
141
|
+
when String
|
142
|
+
# Allow table.column format or just column name
|
143
|
+
column.include?(".") ? column : "#{@table.name}.#{column}"
|
144
|
+
when Arel::Attributes::Attribute
|
145
|
+
column.to_sql
|
146
|
+
else
|
147
|
+
column.to_s
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def sanitize_alias(column)
|
152
|
+
column.to_s.gsub(".", "_")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/simple_query/version.rb
CHANGED
data/lib/simple_query.rb
CHANGED
@@ -14,6 +14,7 @@ require_relative "simple_query/clauses/distinct_clause"
|
|
14
14
|
require_relative "simple_query/clauses/limit_offset_clause"
|
15
15
|
require_relative "simple_query/clauses/group_having_clause"
|
16
16
|
require_relative "simple_query/clauses/set_clause"
|
17
|
+
require_relative "simple_query/clauses/aggregation_clause"
|
17
18
|
|
18
19
|
module SimpleQuery
|
19
20
|
extend ActiveSupport::Concern
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_query
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Kholodniak
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -44,6 +44,7 @@ files:
|
|
44
44
|
- README.md
|
45
45
|
- lib/simple_query.rb
|
46
46
|
- lib/simple_query/builder.rb
|
47
|
+
- lib/simple_query/clauses/aggregation_clause.rb
|
47
48
|
- lib/simple_query/clauses/distinct_clause.rb
|
48
49
|
- lib/simple_query/clauses/group_having_clause.rb
|
49
50
|
- lib/simple_query/clauses/join_clause.rb
|