active_windows 0.1.2 → 0.1.3
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 +37 -27
- data/lib/active_windows/active_record_extensions.rb +2 -2
- data/lib/active_windows/version.rb +1 -1
- 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: 40d81b47d0bf35406b6d9004f3eb82b0137b3bfc0dc5bed7e2d33b697753aeae
|
|
4
|
+
data.tar.gz: 11897b5353a3c8b993922c7af8a4819964d417640bce9501182b145750051144
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b858116cd6290e77e23c5688336102efd83a234bb2caca193728e9b9458a91591fe0653420f5cc400238603921f86e472d3d92ab4028342e4ad9eb2396b4d2e
|
|
7
|
+
data.tar.gz: 3974f6c3a73e2efbbb6aeb603c2ab4ddfd65a88eb23d10353fe94ef50746f38fc4c5c7abceec862f2eb53e8b26f710d3a7b9e274f3889132d83c48874d984e3d
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A Ruby DSL for SQL window functions in ActiveRecord. Write expressive window fun
|
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
6
|
# Fluent API
|
|
7
|
-
User.row_number.partition_by(:department).
|
|
7
|
+
User.row_number.partition_by(:department).window_order(:salary).as(:rank)
|
|
8
8
|
|
|
9
9
|
# Hash API
|
|
10
10
|
User.window(row_number: { partition: :department, order: :salary, as: :rank })
|
|
@@ -41,17 +41,25 @@ ActiveWindows provides two equivalent APIs: a **fluent API** with chainable meth
|
|
|
41
41
|
|
|
42
42
|
### Fluent API
|
|
43
43
|
|
|
44
|
-
Every window function method returns a chainable object with `.partition_by()`, `.
|
|
44
|
+
Every window function method returns a chainable object with `.partition_by()`, `.window_order()`, and `.as()`:
|
|
45
45
|
|
|
46
46
|
```ruby
|
|
47
|
-
User.row_number.partition_by(:department).
|
|
47
|
+
User.row_number.partition_by(:department).window_order(:salary).as(:rank)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
All three chain methods are optional.
|
|
50
|
+
All three chain methods are optional. The fluent API uses `.window_order()` (not `.order()`) to avoid collision with ActiveRecord's `.order()`, which controls the query-level `ORDER BY`. This lets you use both together:
|
|
51
51
|
|
|
52
52
|
```ruby
|
|
53
|
-
User.row_number.as(:rn).order(:
|
|
54
|
-
|
|
53
|
+
User.row_number.window_order(:salary).as(:rn).order(:name)
|
|
54
|
+
# Window: OVER (ORDER BY salary)
|
|
55
|
+
# Query: ORDER BY name
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Order can be mixed freely:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
User.row_number.as(:rn).window_order(:created_at)
|
|
62
|
+
User.dense_rank.window_order(:score).as(:position)
|
|
55
63
|
```
|
|
56
64
|
|
|
57
65
|
### Hash API
|
|
@@ -64,6 +72,8 @@ User.window(
|
|
|
64
72
|
)
|
|
65
73
|
```
|
|
66
74
|
|
|
75
|
+
The hash API uses `order:` as a key (not a method call), so there's no naming conflict with ActiveRecord's `.order()`.
|
|
76
|
+
|
|
67
77
|
Available options:
|
|
68
78
|
|
|
69
79
|
| Option | Type | Description |
|
|
@@ -82,7 +92,7 @@ Window functions integrate naturally with standard ActiveRecord methods:
|
|
|
82
92
|
User.where(active: true)
|
|
83
93
|
.row_number
|
|
84
94
|
.partition_by(:department)
|
|
85
|
-
.
|
|
95
|
+
.window_order(:salary)
|
|
86
96
|
.as(:rank)
|
|
87
97
|
|
|
88
98
|
User.select(:name, :salary)
|
|
@@ -100,7 +110,7 @@ When no `.select()` is specified, `*` is automatically included so all model col
|
|
|
100
110
|
Window function values are accessible as attributes on the returned records:
|
|
101
111
|
|
|
102
112
|
```ruby
|
|
103
|
-
results = User.row_number.partition_by(:department).
|
|
113
|
+
results = User.row_number.partition_by(:department).window_order(:salary).as(:rank)
|
|
104
114
|
|
|
105
115
|
results.each do |user|
|
|
106
116
|
puts "#{user.name}: rank #{user.attributes['rank']}"
|
|
@@ -113,44 +123,44 @@ end
|
|
|
113
123
|
|
|
114
124
|
```ruby
|
|
115
125
|
# ROW_NUMBER() - sequential integer within partition
|
|
116
|
-
User.row_number.partition_by(:department).
|
|
126
|
+
User.row_number.partition_by(:department).window_order(:salary).as(:rn)
|
|
117
127
|
|
|
118
128
|
# RANK() - rank with gaps for ties
|
|
119
|
-
User.rank.partition_by(:department).
|
|
129
|
+
User.rank.partition_by(:department).window_order(:salary).as(:salary_rank)
|
|
120
130
|
|
|
121
131
|
# DENSE_RANK() - rank without gaps for ties
|
|
122
|
-
User.dense_rank.partition_by(:department).
|
|
132
|
+
User.dense_rank.partition_by(:department).window_order(:salary).as(:dense_salary_rank)
|
|
123
133
|
|
|
124
134
|
# PERCENT_RANK() - relative rank as a fraction (0 to 1)
|
|
125
|
-
User.percent_rank.
|
|
135
|
+
User.percent_rank.window_order(:salary).as(:percentile)
|
|
126
136
|
|
|
127
137
|
# CUME_DIST() - cumulative distribution (fraction of rows <= current row)
|
|
128
|
-
User.cume_dist.
|
|
138
|
+
User.cume_dist.window_order(:salary).as(:cumulative)
|
|
129
139
|
|
|
130
140
|
# NTILE(n) - divide rows into n roughly equal buckets
|
|
131
|
-
User.ntile(4).
|
|
141
|
+
User.ntile(4).window_order(:salary).as(:quartile)
|
|
132
142
|
```
|
|
133
143
|
|
|
134
144
|
### Value Functions
|
|
135
145
|
|
|
136
146
|
```ruby
|
|
137
147
|
# LAG(column, offset, default) - value from a preceding row
|
|
138
|
-
User.lag(:salary).
|
|
139
|
-
User.lag(:salary, 2).
|
|
140
|
-
User.lag(:salary, 1, 0).
|
|
148
|
+
User.lag(:salary).window_order(:hire_date).as(:prev_salary)
|
|
149
|
+
User.lag(:salary, 2).window_order(:hire_date).as(:two_back) # custom offset
|
|
150
|
+
User.lag(:salary, 1, 0).window_order(:hire_date).as(:prev_or_zero) # with default
|
|
141
151
|
|
|
142
152
|
# LEAD(column, offset, default) - value from a following row
|
|
143
|
-
User.lead(:salary).
|
|
144
|
-
User.lead(:salary, 2, 0).
|
|
153
|
+
User.lead(:salary).window_order(:hire_date).as(:next_salary)
|
|
154
|
+
User.lead(:salary, 2, 0).window_order(:hire_date).as(:two_ahead)
|
|
145
155
|
|
|
146
156
|
# FIRST_VALUE(column) - first value in the window frame
|
|
147
|
-
User.first_value(:name).partition_by(:department).
|
|
157
|
+
User.first_value(:name).partition_by(:department).window_order(:salary).as(:lowest_paid)
|
|
148
158
|
|
|
149
159
|
# LAST_VALUE(column) - last value in the window frame
|
|
150
|
-
User.last_value(:name).partition_by(:department).
|
|
160
|
+
User.last_value(:name).partition_by(:department).window_order(:salary).as(:highest_paid)
|
|
151
161
|
|
|
152
162
|
# NTH_VALUE(column, n) - nth value in the window frame
|
|
153
|
-
User.nth_value(:name, 2).partition_by(:department).
|
|
163
|
+
User.nth_value(:name, 2).partition_by(:department).window_order(:salary).as(:second_lowest)
|
|
154
164
|
```
|
|
155
165
|
|
|
156
166
|
### Aggregate Window Functions
|
|
@@ -196,7 +206,7 @@ User.window(sum: {
|
|
|
196
206
|
```ruby
|
|
197
207
|
User.rank
|
|
198
208
|
.partition_by(:department)
|
|
199
|
-
.
|
|
209
|
+
.window_order(:salary)
|
|
200
210
|
.as(:salary_rank)
|
|
201
211
|
```
|
|
202
212
|
|
|
@@ -227,14 +237,14 @@ end
|
|
|
227
237
|
```ruby
|
|
228
238
|
User.lag(:name)
|
|
229
239
|
.partition_by(:department)
|
|
230
|
-
.
|
|
240
|
+
.window_order(:hire_date)
|
|
231
241
|
.as(:previous_hire)
|
|
232
242
|
```
|
|
233
243
|
|
|
234
244
|
### Divide employees into salary quartiles
|
|
235
245
|
|
|
236
246
|
```ruby
|
|
237
|
-
User.ntile(4).
|
|
247
|
+
User.ntile(4).window_order(:salary).as(:quartile)
|
|
238
248
|
```
|
|
239
249
|
|
|
240
250
|
### Rank users by total order amount (with joins)
|
|
@@ -253,7 +263,7 @@ User.joins(:orders)
|
|
|
253
263
|
Order.joins(:user)
|
|
254
264
|
.row_number
|
|
255
265
|
.partition_by("users.id")
|
|
256
|
-
.
|
|
266
|
+
.window_order(amount: :desc)
|
|
257
267
|
.as(:order_rank)
|
|
258
268
|
|
|
259
269
|
# Number each user's orders chronologically
|
|
@@ -261,7 +271,7 @@ Order.joins(:user)
|
|
|
261
271
|
.select("orders.*, users.name AS user_name")
|
|
262
272
|
.row_number
|
|
263
273
|
.partition_by(:user_id)
|
|
264
|
-
.
|
|
274
|
+
.window_order(:created_at)
|
|
265
275
|
.as(:order_number)
|
|
266
276
|
```
|
|
267
277
|
|
|
@@ -25,7 +25,7 @@ module ActiveWindows
|
|
|
25
25
|
self
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def
|
|
28
|
+
def window_order(*columns)
|
|
29
29
|
@order_columns = columns.flatten
|
|
30
30
|
self
|
|
31
31
|
end
|
|
@@ -46,7 +46,7 @@ module ActiveWindows
|
|
|
46
46
|
|
|
47
47
|
# Delegate common relation/query methods so the chain is transparent
|
|
48
48
|
delegate :to_sql, :to_a, :to_ary, :load, :loaded?, :each, :map, :first, :last, :count,
|
|
49
|
-
:where, :select, :joins, :group, :having, :limit, :offset, :reorder, :pluck,
|
|
49
|
+
:where, :select, :joins, :group, :having, :order, :limit, :offset, :reorder, :pluck,
|
|
50
50
|
:find_each, :find_in_batches, :inspect, :exists?, :any?, :none?, :empty?,
|
|
51
51
|
to: :to_relation
|
|
52
52
|
end
|