active_windows 0.1.7 → 0.1.8
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 +48 -64
- data/docs/REVIEW_AND_PLAN.md +5 -4
- data/lib/active_windows/active_record_extensions.rb +13 -94
- data/lib/active_windows/version.rb +1 -1
- data/lib/active_windows.rb +1 -5
- 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: a3591dbeb9c495d128c8fa3fd9f88cdc4be7d6fabad282e48e47a9d3b9e919d7
|
|
4
|
+
data.tar.gz: ee011c561b8e84a1efe6b6366664616286632ff8facfaa1ff3a1df37a093c434
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc03d55bfc15cf28e76347b6a8f0f04d52a89c24c6b41feb458cff6c8c4694ed1270802710f9686eb68e90f30281f096ef8d0a0fc9c638c356f347aa894015bd
|
|
7
|
+
data.tar.gz: 92e13ee084266b0fe1a8baf40a5275338d95a07888cca71f44546ef2b518700208c758c2302f1fcb2ff4d32608666652f993a89bc96cbb6e8f692bfaacb46a36
|
data/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
[](https://badge.fury.io/rb/active_windows)
|
|
2
|
+
|
|
1
3
|
# ActiveWindows
|
|
2
4
|
|
|
3
5
|
A Ruby DSL for SQL window functions in ActiveRecord. Write expressive window function queries using a fluent, chainable interface instead of raw SQL.
|
|
4
6
|
|
|
5
7
|
```ruby
|
|
6
8
|
# Fluent API
|
|
7
|
-
User.row_number.partition_by(:department).order_by(:salary).as(:rank)
|
|
9
|
+
User.window(:row_number).partition_by(:department).order_by(:salary).as(:rank)
|
|
8
10
|
|
|
9
11
|
# Hash API
|
|
10
|
-
User.window(row_number: {
|
|
12
|
+
User.window(row_number: { partition_by: :department, order_by: :salary, as: :rank })
|
|
11
13
|
|
|
12
14
|
# Both produce:
|
|
13
|
-
# SELECT "users".*, ROW_NUMBER() OVER (PARTITION BY "users"."department" ORDER BY "users"."salary") AS rank
|
|
15
|
+
# SELECT "users".*, ROW_NUMBER() OVER (PARTITION BY "users"."department" ORDER BY "users"."salary") AS "rank"
|
|
14
16
|
# FROM "users"
|
|
15
17
|
```
|
|
16
18
|
|
|
@@ -41,34 +43,31 @@ ActiveWindows provides two equivalent APIs: a **fluent API** with chainable meth
|
|
|
41
43
|
|
|
42
44
|
### Fluent API
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
`window(:function_name, *args)` returns a chainable object with `.partition_by()`, `.order_by()`, and `.as()`:
|
|
45
47
|
|
|
46
48
|
```ruby
|
|
47
|
-
User.row_number.partition_by(:department).order_by(:salary).as(:rank)
|
|
49
|
+
User.window(:row_number).partition_by(:department).order_by(:salary).as(:rank)
|
|
50
|
+
|
|
51
|
+
# Functions with arguments:
|
|
52
|
+
User.window(:lag, :salary, 1, 0).order_by(:hire_date).as(:prev_salary)
|
|
53
|
+
User.window(:ntile, 4).order_by(:salary).as(:quartile)
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
All three chain methods are optional. `.order_by()` sets the window's `ORDER BY`, while ActiveRecord's `.order()` controls the query-level `ORDER BY`. You can use both together:
|
|
51
57
|
|
|
52
58
|
```ruby
|
|
53
|
-
User.row_number.order_by(:salary).as(:rn).order(:name)
|
|
59
|
+
User.window(:row_number).order_by(:salary).as(:rn).order(:name)
|
|
54
60
|
# Window: OVER (ORDER BY salary)
|
|
55
61
|
# Query: ORDER BY name
|
|
56
62
|
```
|
|
57
63
|
|
|
58
|
-
Order can be mixed freely:
|
|
59
|
-
|
|
60
|
-
```ruby
|
|
61
|
-
User.row_number.as(:rn).order_by(:created_at)
|
|
62
|
-
User.dense_rank.order_by(:score).as(:position)
|
|
63
|
-
```
|
|
64
|
-
|
|
65
64
|
### Hash API
|
|
66
65
|
|
|
67
66
|
Pass one or more window function definitions as a hash:
|
|
68
67
|
|
|
69
68
|
```ruby
|
|
70
69
|
User.window(
|
|
71
|
-
row_number: {
|
|
70
|
+
row_number: { partition_by: :department, order_by: :salary, as: :rank }
|
|
72
71
|
)
|
|
73
72
|
```
|
|
74
73
|
|
|
@@ -76,7 +75,7 @@ Available options:
|
|
|
76
75
|
|
|
77
76
|
| Option | Type | Description |
|
|
78
77
|
|--------|------|-------------|
|
|
79
|
-
| `:
|
|
78
|
+
| `:partition_by` | `Symbol`, `Array` | Column(s) for `PARTITION BY` |
|
|
80
79
|
| `:order_by` | `Symbol`, `Array` | Column(s) for `ORDER BY` |
|
|
81
80
|
| `:as` | `Symbol` | Alias for the result column |
|
|
82
81
|
| `:frame` | `String` | Raw SQL frame clause (e.g. `"ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"`) |
|
|
@@ -84,28 +83,15 @@ Available options:
|
|
|
84
83
|
|
|
85
84
|
### Association Names
|
|
86
85
|
|
|
87
|
-
You can use association names instead of
|
|
88
|
-
|
|
89
|
-
**`belongs_to`** — resolves to the foreign key on the current table:
|
|
86
|
+
You can use `belongs_to` association names instead of foreign key columns. ActiveWindows automatically resolves them:
|
|
90
87
|
|
|
91
88
|
```ruby
|
|
92
89
|
# These are equivalent:
|
|
93
|
-
Order.row_number.partition_by(:user).order_by(:amount).as(:rn)
|
|
94
|
-
Order.row_number.partition_by(:user_id).order_by(:amount).as(:rn)
|
|
90
|
+
Order.window(:row_number).partition_by(:user).order_by(:amount).as(:rn)
|
|
91
|
+
Order.window(:row_number).partition_by(:user_id).order_by(:amount).as(:rn)
|
|
95
92
|
|
|
96
93
|
# Works in the hash API too:
|
|
97
|
-
Order.window(row_number: {
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
**`has_one`** — auto-joins the associated table and references its primary key:
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
# User has_one :profile
|
|
104
|
-
# Automatically joins profiles and partitions by profiles.id
|
|
105
|
-
User.row_number.partition_by(:profile).order_by(:salary).as(:rn)
|
|
106
|
-
|
|
107
|
-
# Equivalent to:
|
|
108
|
-
User.joins(:profile).row_number.partition_by("profiles.id").order_by(:salary).as(:rn)
|
|
94
|
+
Order.window(row_number: { partition_by: :user, order_by: :amount, as: :rn })
|
|
109
95
|
```
|
|
110
96
|
|
|
111
97
|
### Chaining with ActiveRecord
|
|
@@ -114,7 +100,7 @@ Window functions integrate naturally with standard ActiveRecord methods:
|
|
|
114
100
|
|
|
115
101
|
```ruby
|
|
116
102
|
User.where(active: true)
|
|
117
|
-
.row_number
|
|
103
|
+
.window(:row_number)
|
|
118
104
|
.partition_by(:department)
|
|
119
105
|
.order_by(:salary)
|
|
120
106
|
.as(:rank)
|
|
@@ -134,7 +120,7 @@ When no `.select()` is specified, `*` is automatically included so all model col
|
|
|
134
120
|
Window function values are accessible as attributes on the returned records:
|
|
135
121
|
|
|
136
122
|
```ruby
|
|
137
|
-
results = User.row_number.partition_by(:department).order_by(:salary).as(:rank)
|
|
123
|
+
results = User.window(:row_number).partition_by(:department).order_by(:salary).as(:rank)
|
|
138
124
|
|
|
139
125
|
results.each do |user|
|
|
140
126
|
puts "#{user.name}: rank #{user.attributes['rank']}"
|
|
@@ -147,66 +133,64 @@ end
|
|
|
147
133
|
|
|
148
134
|
```ruby
|
|
149
135
|
# ROW_NUMBER() - sequential integer within partition
|
|
150
|
-
User.row_number.partition_by(:department).order_by(:salary).as(:rn)
|
|
136
|
+
User.window(:row_number).partition_by(:department).order_by(:salary).as(:rn)
|
|
151
137
|
|
|
152
138
|
# RANK() - rank with gaps for ties
|
|
153
|
-
User.rank.partition_by(:department).order_by(:salary).as(:salary_rank)
|
|
139
|
+
User.window(:rank).partition_by(:department).order_by(:salary).as(:salary_rank)
|
|
154
140
|
|
|
155
141
|
# DENSE_RANK() - rank without gaps for ties
|
|
156
|
-
User.dense_rank.partition_by(:department).order_by(:salary).as(:dense_salary_rank)
|
|
142
|
+
User.window(:dense_rank).partition_by(:department).order_by(:salary).as(:dense_salary_rank)
|
|
157
143
|
|
|
158
144
|
# PERCENT_RANK() - relative rank as a fraction (0 to 1)
|
|
159
|
-
User.percent_rank.order_by(:salary).as(:percentile)
|
|
145
|
+
User.window(:percent_rank).order_by(:salary).as(:percentile)
|
|
160
146
|
|
|
161
147
|
# CUME_DIST() - cumulative distribution (fraction of rows <= current row)
|
|
162
|
-
User.cume_dist.order_by(:salary).as(:cumulative)
|
|
148
|
+
User.window(:cume_dist).order_by(:salary).as(:cumulative)
|
|
163
149
|
|
|
164
150
|
# NTILE(n) - divide rows into n roughly equal buckets
|
|
165
|
-
User.ntile
|
|
151
|
+
User.window(:ntile, 4).order_by(:salary).as(:quartile)
|
|
166
152
|
```
|
|
167
153
|
|
|
168
154
|
### Value Functions
|
|
169
155
|
|
|
170
156
|
```ruby
|
|
171
157
|
# LAG(column, offset, default) - value from a preceding row
|
|
172
|
-
User.
|
|
173
|
-
User.
|
|
174
|
-
User.
|
|
158
|
+
User.window(:lag, :salary).order_by(:hire_date).as(:prev_salary)
|
|
159
|
+
User.window(:lag, :salary, 2).order_by(:hire_date).as(:two_back) # custom offset
|
|
160
|
+
User.window(:lag, :salary, 1, 0).order_by(:hire_date).as(:prev_or_zero) # with default
|
|
175
161
|
|
|
176
162
|
# LEAD(column, offset, default) - value from a following row
|
|
177
|
-
User.
|
|
178
|
-
User.
|
|
163
|
+
User.window(:lead, :salary).order_by(:hire_date).as(:next_salary)
|
|
164
|
+
User.window(:lead, :salary, 2, 0).order_by(:hire_date).as(:two_ahead)
|
|
179
165
|
|
|
180
166
|
# FIRST_VALUE(column) - first value in the window frame
|
|
181
|
-
User.
|
|
167
|
+
User.window(:first_value, :name).partition_by(:department).order_by(:salary).as(:lowest_paid)
|
|
182
168
|
|
|
183
169
|
# LAST_VALUE(column) - last value in the window frame
|
|
184
|
-
User.
|
|
170
|
+
User.window(:last_value, :name).partition_by(:department).order_by(:salary).as(:highest_paid)
|
|
185
171
|
|
|
186
172
|
# NTH_VALUE(column, n) - nth value in the window frame
|
|
187
|
-
User.
|
|
173
|
+
User.window(:nth_value, :name, 2).partition_by(:department).order_by(:salary).as(:second_lowest)
|
|
188
174
|
```
|
|
189
175
|
|
|
190
176
|
### Aggregate Window Functions
|
|
191
177
|
|
|
192
|
-
Prefixed with `window_` to avoid conflicts with ActiveRecord's built-in aggregate methods:
|
|
193
|
-
|
|
194
178
|
```ruby
|
|
195
179
|
# SUM(column) OVER(...)
|
|
196
|
-
User.
|
|
180
|
+
User.window(:sum, :salary).partition_by(:department).as(:dept_total)
|
|
197
181
|
|
|
198
182
|
# AVG(column) OVER(...)
|
|
199
|
-
User.
|
|
183
|
+
User.window(:avg, :salary).partition_by(:department).as(:dept_avg)
|
|
200
184
|
|
|
201
185
|
# COUNT(column) OVER(...)
|
|
202
|
-
User.
|
|
203
|
-
User.
|
|
186
|
+
User.window(:count, :id).partition_by(:department).as(:dept_size)
|
|
187
|
+
User.window(:count, "*").partition_by(:department).as(:dept_size) # COUNT(*)
|
|
204
188
|
|
|
205
189
|
# MIN(column) OVER(...)
|
|
206
|
-
User.
|
|
190
|
+
User.window(:min, :salary).partition_by(:department).as(:min_salary)
|
|
207
191
|
|
|
208
192
|
# MAX(column) OVER(...)
|
|
209
|
-
User.
|
|
193
|
+
User.window(:max, :salary).partition_by(:department).as(:max_salary)
|
|
210
194
|
```
|
|
211
195
|
|
|
212
196
|
### Window Frames
|
|
@@ -216,7 +200,7 @@ Pass a raw SQL frame clause via the hash API:
|
|
|
216
200
|
```ruby
|
|
217
201
|
User.window(sum: {
|
|
218
202
|
value: :salary,
|
|
219
|
-
|
|
203
|
+
partition_by: :department,
|
|
220
204
|
order_by: :hire_date,
|
|
221
205
|
frame: "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
|
|
222
206
|
as: :running_total
|
|
@@ -228,7 +212,7 @@ User.window(sum: {
|
|
|
228
212
|
### Rank employees by salary within each department
|
|
229
213
|
|
|
230
214
|
```ruby
|
|
231
|
-
User.rank
|
|
215
|
+
User.window(:rank)
|
|
232
216
|
.partition_by(:department)
|
|
233
217
|
.order_by(:salary)
|
|
234
218
|
.as(:salary_rank)
|
|
@@ -248,7 +232,7 @@ User.window(sum: {
|
|
|
248
232
|
### Compare each salary to the department average
|
|
249
233
|
|
|
250
234
|
```ruby
|
|
251
|
-
users = User.
|
|
235
|
+
users = User.window(:avg, :salary).partition_by(:department).as(:dept_avg)
|
|
252
236
|
|
|
253
237
|
users.each do |user|
|
|
254
238
|
diff = user.salary - user.attributes["dept_avg"].to_f
|
|
@@ -259,7 +243,7 @@ end
|
|
|
259
243
|
### Find the previous and next hire in each department
|
|
260
244
|
|
|
261
245
|
```ruby
|
|
262
|
-
User.
|
|
246
|
+
User.window(:lag, :name)
|
|
263
247
|
.partition_by(:department)
|
|
264
248
|
.order_by(:hire_date)
|
|
265
249
|
.as(:previous_hire)
|
|
@@ -268,7 +252,7 @@ User.lag(:name)
|
|
|
268
252
|
### Divide employees into salary quartiles
|
|
269
253
|
|
|
270
254
|
```ruby
|
|
271
|
-
User.ntile
|
|
255
|
+
User.window(:ntile, 4).order_by(:salary).as(:quartile)
|
|
272
256
|
```
|
|
273
257
|
|
|
274
258
|
### Rank users by total order amount (with joins)
|
|
@@ -285,7 +269,7 @@ User.joins(:orders)
|
|
|
285
269
|
```ruby
|
|
286
270
|
# Rank orders by amount within each user
|
|
287
271
|
Order.joins(:user)
|
|
288
|
-
.row_number
|
|
272
|
+
.window(:row_number)
|
|
289
273
|
.partition_by("users.id")
|
|
290
274
|
.order_by(amount: :desc)
|
|
291
275
|
.as(:order_rank)
|
|
@@ -293,7 +277,7 @@ Order.joins(:user)
|
|
|
293
277
|
# Number each user's orders chronologically
|
|
294
278
|
Order.joins(:user)
|
|
295
279
|
.select("orders.*, users.name AS user_name")
|
|
296
|
-
.row_number
|
|
280
|
+
.window(:row_number)
|
|
297
281
|
.partition_by(:user_id)
|
|
298
282
|
.order_by(:created_at)
|
|
299
283
|
.as(:order_number)
|
|
@@ -307,7 +291,7 @@ Order.joins(:user)
|
|
|
307
291
|
.where(users: { department: "Engineering" })
|
|
308
292
|
.window(sum: {
|
|
309
293
|
value: :amount,
|
|
310
|
-
|
|
294
|
+
partition_by: :user_id,
|
|
311
295
|
order_by: :created_at,
|
|
312
296
|
frame: "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
|
|
313
297
|
as: :running_total
|
data/docs/REVIEW_AND_PLAN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overall Assessment
|
|
4
4
|
|
|
5
|
-
The gem provides a fluent DSL for SQL window functions in ActiveRecord. Core functionality is implemented and tested, with 16 window functions available via
|
|
5
|
+
The gem provides a fluent DSL for SQL window functions in ActiveRecord. Core functionality is implemented and tested, with 16 window functions available via fluent, `window(:symbol)`, and hash APIs. 82 tests passing with 399 assertions. CI runs against SQLite, PostgreSQL, and MySQL.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ The gem provides a fluent DSL for SQL window functions in ActiveRecord. Core fun
|
|
|
26
26
|
- ~~**RuboCop dependency**~~ — Removed.
|
|
27
27
|
- ~~**LICENSE**~~ — Cleaned up. Full name, no year, `.txt` extension removed.
|
|
28
28
|
- ~~**Boilerplate files**~~ — Removed. `CHANGELOG.md`, `CODE_OF_CONDUCT.md`, `bin/console`, `bin/setup`, `sig/active_windows.rbs`.
|
|
29
|
-
- ~~**Ordering direction**~~ — Fixed. Supports `
|
|
29
|
+
- ~~**Ordering direction**~~ — Fixed. Supports `order_by(salary: :desc)`, `order_by({ col: :asc }, { col: :desc })`, and hash API `order_by: { salary: :desc }`.
|
|
30
30
|
- ~~**Gemfile/gemspec duplication**~~ — Fixed. Dev gems (`minitest`, `rake`) defined only in Gemfile under `test, development` group. No more bundler override warnings.
|
|
31
31
|
- ~~**Cross-platform lockfile**~~ — Fixed. Added `aarch64-linux`, `arm-linux`, `arm64-darwin`, `x86_64-darwin`, `x86_64-linux` platforms.
|
|
32
32
|
- ~~**Multiple window functions in one call**~~ — Tested. Single `window()` with multiple keys and chaining separate `window()` calls both work.
|
|
@@ -34,8 +34,9 @@ The gem provides a fluent DSL for SQL window functions in ActiveRecord. Core fun
|
|
|
34
34
|
- ~~**PostgreSQL CI**~~ — Added. GitHub Actions workflow tests against PostgreSQL 17 with service container.
|
|
35
35
|
- ~~**MySQL CI**~~ — Added. GitHub Actions workflow tests against MySQL 8.0 with service container.
|
|
36
36
|
- ~~**MySQL compatibility**~~ — Fixed. Aliases now use `klass.connection.quote_column_name` to properly quote reserved words (e.g., `rank`) with backticks on MySQL and double quotes on PostgreSQL/SQLite. Test assertions use adapter-agnostic `q()` and `col()` helpers.
|
|
37
|
-
- ~~**WindowChain `order` naming collision**~~ — Fixed. Renamed to `
|
|
38
|
-
- ~~**Association name resolution**~~ — Added. `partition_by(:user)`
|
|
37
|
+
- ~~**WindowChain `order` naming collision**~~ — Fixed. Renamed to `order_by` to avoid conflict with ActiveRecord's `.order()`. Both fluent (`.order_by(:salary)`) and hash (`order_by: :salary`) APIs use `order_by`. WindowChain delegates `.order()` to the relation for query-level ordering. Uses `method_missing` for full relation method coverage.
|
|
38
|
+
- ~~**Association name resolution**~~ — Added. `belongs_to`: `partition_by(:user)` resolves to `user_id`. Works in both fluent and hash APIs.
|
|
39
|
+
- ~~**Unified `window()` entry point**~~ — Added. `window(:row_number)` returns a WindowChain (fluent), `window(:lag, :salary, 1, 0)` passes function args, `window(row_number: { ... })` is hash API. Single method, three modes. 82 tests, 399 assertions.
|
|
39
40
|
|
|
40
41
|
---
|
|
41
42
|
|
|
@@ -32,7 +32,7 @@ module ActiveWindows
|
|
|
32
32
|
|
|
33
33
|
def to_window_hash
|
|
34
34
|
options = {}
|
|
35
|
-
options[:
|
|
35
|
+
options[:partition_by] = @partition_columns unless @partition_columns.empty?
|
|
36
36
|
options[:order_by] = @order_columns unless @order_columns.empty?
|
|
37
37
|
options[:as] = @alias_name if @alias_name
|
|
38
38
|
options[:value] = @function_args unless @function_args.empty?
|
|
@@ -65,23 +65,24 @@ module ActiveWindows
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
module QueryMethods
|
|
68
|
-
VALID_WINDOW_OPTIONS = %i[value
|
|
68
|
+
VALID_WINDOW_OPTIONS = %i[value partition_by order_by frame as].freeze
|
|
69
69
|
|
|
70
|
-
#
|
|
70
|
+
# Fluent: window(:row_number) returns a WindowChain
|
|
71
|
+
# Fluent with args: window(:lag, :salary, 1, 0) returns a WindowChain
|
|
72
|
+
# Hash: window(row_number: { partition: :department, order_by: :salary, as: :rank })
|
|
71
73
|
def window(*args)
|
|
72
74
|
raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" if args.empty?
|
|
73
75
|
|
|
76
|
+
# Fluent API: window(:function_name, *function_args)
|
|
77
|
+
if args.first.is_a?(Symbol) && (args.length == 1 || !args[1].is_a?(Hash))
|
|
78
|
+
function_name = args.shift
|
|
79
|
+
return WindowChain.new(function_name, spawn, function_args: args.map(&:to_s))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Hash API
|
|
74
83
|
processed = process_window_args(args)
|
|
75
84
|
|
|
76
85
|
result = spawn
|
|
77
|
-
|
|
78
|
-
# Auto-join has_one associations referenced in partition/order
|
|
79
|
-
joins_needed = processed.flat_map do |_name, options|
|
|
80
|
-
next [] unless options.is_a?(Hash)
|
|
81
|
-
association_joins_for(options[:partition]) + association_joins_for(options[:order_by])
|
|
82
|
-
end.uniq
|
|
83
|
-
result = result.joins(*joins_needed) if joins_needed.any?
|
|
84
|
-
|
|
85
86
|
arel_nodes = processed.map { |name, options| build_window_function(name, options || {}) }
|
|
86
87
|
|
|
87
88
|
# Ensure we keep all columns alongside the window function columns
|
|
@@ -89,83 +90,12 @@ module ActiveWindows
|
|
|
89
90
|
result.select(*arel_nodes)
|
|
90
91
|
end
|
|
91
92
|
|
|
92
|
-
# Ranking window functions
|
|
93
|
-
def row_number
|
|
94
|
-
WindowChain.new(:row_number, spawn)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def rank
|
|
98
|
-
WindowChain.new(:rank, spawn)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def dense_rank
|
|
102
|
-
WindowChain.new(:dense_rank, spawn)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def percent_rank
|
|
106
|
-
WindowChain.new(:percent_rank, spawn)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def cume_dist
|
|
110
|
-
WindowChain.new(:cume_dist, spawn)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def ntile(num_buckets)
|
|
114
|
-
WindowChain.new(:ntile, spawn, function_args: [num_buckets.to_s])
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Value window functions
|
|
118
|
-
def lag(column, offset = 1, default = nil)
|
|
119
|
-
args = [column.to_s, offset.to_s]
|
|
120
|
-
args << default.to_s unless default.nil?
|
|
121
|
-
WindowChain.new(:lag, spawn, function_args: args)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def lead(column, offset = 1, default = nil)
|
|
125
|
-
args = [column.to_s, offset.to_s]
|
|
126
|
-
args << default.to_s unless default.nil?
|
|
127
|
-
WindowChain.new(:lead, spawn, function_args: args)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def first_value(column)
|
|
131
|
-
WindowChain.new(:first_value, spawn, function_args: [column.to_s])
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def last_value(column)
|
|
135
|
-
WindowChain.new(:last_value, spawn, function_args: [column.to_s])
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def nth_value(column, n)
|
|
139
|
-
WindowChain.new(:nth_value, spawn, function_args: [column.to_s, n.to_s])
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Aggregate window functions
|
|
143
|
-
def window_sum(column)
|
|
144
|
-
WindowChain.new(:sum, spawn, function_args: [column.to_s])
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def window_avg(column)
|
|
148
|
-
WindowChain.new(:avg, spawn, function_args: [column.to_s])
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def window_count(column = "*")
|
|
152
|
-
WindowChain.new(:count, spawn, function_args: [column.to_s])
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def window_min(column)
|
|
156
|
-
WindowChain.new(:min, spawn, function_args: [column.to_s])
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def window_max(column)
|
|
160
|
-
WindowChain.new(:max, spawn, function_args: [column.to_s])
|
|
161
|
-
end
|
|
162
|
-
|
|
163
93
|
private
|
|
164
94
|
|
|
165
95
|
def build_window_function(name, options)
|
|
166
96
|
window = Arel::Nodes::Window.new
|
|
167
97
|
|
|
168
|
-
apply_window_partition(window, options[:
|
|
98
|
+
apply_window_partition(window, options[:partition_by])
|
|
169
99
|
apply_window_order(window, options[:order_by])
|
|
170
100
|
apply_window_frame(window, options[:frame]) if options[:frame]
|
|
171
101
|
|
|
@@ -237,22 +167,11 @@ module ActiveWindows
|
|
|
237
167
|
reflection = klass.reflect_on_association(name_sym)
|
|
238
168
|
if reflection&.macro == :belongs_to
|
|
239
169
|
klass.arel_table[reflection.foreign_key.to_sym]
|
|
240
|
-
elsif reflection&.macro == :has_one
|
|
241
|
-
reflection.klass.arel_table[reflection.klass.primary_key.to_sym]
|
|
242
170
|
else
|
|
243
171
|
klass.arel_table[name_sym]
|
|
244
172
|
end
|
|
245
173
|
end
|
|
246
174
|
|
|
247
|
-
def association_joins_for(columns)
|
|
248
|
-
Array(columns).filter_map do |col|
|
|
249
|
-
next unless col.is_a?(Symbol) || col.is_a?(String)
|
|
250
|
-
|
|
251
|
-
reflection = klass.reflect_on_association(col.to_sym)
|
|
252
|
-
col.to_sym if reflection&.macro == :has_one
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
175
|
def arel_order(expr)
|
|
257
176
|
case expr
|
|
258
177
|
when Arel::Nodes::Node, Arel::Nodes::SqlLiteral
|
data/lib/active_windows.rb
CHANGED
|
@@ -7,9 +7,5 @@ require "active_windows/railtie" if defined?(Rails::Railtie)
|
|
|
7
7
|
module ActiveWindows
|
|
8
8
|
class Error < StandardError; end
|
|
9
9
|
|
|
10
|
-
QUERY_METHODS = %i[
|
|
11
|
-
window row_number rank dense_rank percent_rank cume_dist ntile
|
|
12
|
-
lag lead first_value last_value nth_value
|
|
13
|
-
window_sum window_avg window_count window_min window_max
|
|
14
|
-
].freeze
|
|
10
|
+
QUERY_METHODS = %i[window].freeze
|
|
15
11
|
end
|