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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a62c7c2fe2d6c43a27b9cc2dd22e7ddcf14a01f9ca335144b472db1559f4b0a
4
- data.tar.gz: 172eddfa79634dcf78d3547031af80a9f1e008dc9e0414c0f5671c66b4264617
3
+ metadata.gz: 856dc1549df83e6c82a7105b3dfa97b9c6f4e69b86f6c2f9ad653ae1f322d1e8
4
+ data.tar.gz: c10490e13ffbc3da9c629081cde483188efbc0ec3fbdcf27070d565947cc723a
5
5
  SHA512:
6
- metadata.gz: abbd8b5034e7b3a9a72b5de79f0d522a1ba2ac2b066bcc78387fb068ad9647b685c006318f6efafd31b5b616b48bd18ea677a5c74e7c25647a38c062a934109b
7
- data.tar.gz: e499597efd33f703d136b0e053006ef109dea762e19148acd6bb5dcf6717f14f8b63845d61a3ea7b770353dd37e17178ca8b51ef7785e0714828ee78bda1fb8e
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
 
@@ -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
- @query.project(*(@selects.empty? ? [@arel_table[Arel.star]] : @selects))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleQuery
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.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-03-15 00:00:00.000000000 Z
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