active_windows 0.1.6 → 0.1.7

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: b393a3c07582141b6656bbf2f9ffe55193f6fd32c3505af7875a9e6f76fec7d0
4
- data.tar.gz: 7571dfc93bc77a96e495b2c74799100320e7bf6666809ceb754ec5fc64efffc9
3
+ metadata.gz: 78306f0ad28382b0fd83cc679dcfd418fc72a5e0c0a26e1e812ab0cdf3a289e2
4
+ data.tar.gz: a155a298bc07f13ead19b25764df4256996b995918b2362f518512066cccb4f8
5
5
  SHA512:
6
- metadata.gz: 8380c1b66b1a1a3c31f9e9c5a9035c43ff7689e499254b4bd7d9b250fbfaeb36054c8a012e49f4f84756a0a6ab8c42bf3a931bad82fda768b07556f955bdac3a
7
- data.tar.gz: 92128ba7625b79177654e2f09a76f163d5901ddf2105f84174fe591d81dd020d276fdddb428d76649c0648ac7127f2bfcbee4b539c007221c74be1c6d231ae85
6
+ metadata.gz: 2c2bb59c2105382c024acc58192266edd835c08422bc6a13221d480f634320185395af8eb3a742ff92a53ca123ed4775d50b765da48e7d9256f1f5ba3f397044
7
+ data.tar.gz: d9821806ab2b43d4ce12955f7311d0582b1312f76dd126255465c088e3a1bd9417c1d4fb870fc6a986361229e984253ffd6d67e587c001234a157a38a82ba545
data/README.md CHANGED
@@ -4,10 +4,10 @@ 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).window_order(:salary).as(:rank)
7
+ User.row_number.partition_by(:department).order_by(:salary).as(:rank)
8
8
 
9
9
  # Hash API
10
- User.window(row_number: { partition: :department, order: :salary, as: :rank })
10
+ User.window(row_number: { partition: :department, order_by: :salary, as: :rank })
11
11
 
12
12
  # Both produce:
13
13
  # SELECT "users".*, ROW_NUMBER() OVER (PARTITION BY "users"."department" ORDER BY "users"."salary") AS rank
@@ -41,16 +41,16 @@ 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()`, `.window_order()`, and `.as()`:
44
+ Every window function method returns a chainable object with `.partition_by()`, `.order_by()`, and `.as()`:
45
45
 
46
46
  ```ruby
47
- User.row_number.partition_by(:department).window_order(:salary).as(:rank)
47
+ User.row_number.partition_by(:department).order_by(:salary).as(:rank)
48
48
  ```
49
49
 
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:
50
+ 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
51
 
52
52
  ```ruby
53
- User.row_number.window_order(:salary).as(:rn).order(:name)
53
+ User.row_number.order_by(:salary).as(:rn).order(:name)
54
54
  # Window: OVER (ORDER BY salary)
55
55
  # Query: ORDER BY name
56
56
  ```
@@ -58,8 +58,8 @@ User.row_number.window_order(:salary).as(:rn).order(:name)
58
58
  Order can be mixed freely:
59
59
 
60
60
  ```ruby
61
- User.row_number.as(:rn).window_order(:created_at)
62
- User.dense_rank.window_order(:score).as(:position)
61
+ User.row_number.as(:rn).order_by(:created_at)
62
+ User.dense_rank.order_by(:score).as(:position)
63
63
  ```
64
64
 
65
65
  ### Hash API
@@ -68,33 +68,44 @@ Pass one or more window function definitions as a hash:
68
68
 
69
69
  ```ruby
70
70
  User.window(
71
- row_number: { partition: :department, order: :salary, as: :rank }
71
+ row_number: { partition: :department, order_by: :salary, as: :rank }
72
72
  )
73
73
  ```
74
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
-
77
75
  Available options:
78
76
 
79
77
  | Option | Type | Description |
80
78
  |--------|------|-------------|
81
79
  | `:partition` | `Symbol`, `Array` | Column(s) for `PARTITION BY` |
82
- | `:order` | `Symbol`, `Array` | Column(s) for `ORDER BY` |
80
+ | `:order_by` | `Symbol`, `Array` | Column(s) for `ORDER BY` |
83
81
  | `:as` | `Symbol` | Alias for the result column |
84
82
  | `:frame` | `String` | Raw SQL frame clause (e.g. `"ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING"`) |
85
83
  | `:value` | `Symbol`, `String`, `Array` | Expression(s) passed as function arguments |
86
84
 
87
85
  ### Association Names
88
86
 
89
- You can use `belongs_to` association names instead of foreign key columns. ActiveWindows automatically resolves them:
87
+ You can use association names instead of raw column names. ActiveWindows automatically resolves them:
88
+
89
+ **`belongs_to`** — resolves to the foreign key on the current table:
90
90
 
91
91
  ```ruby
92
92
  # These are equivalent:
93
- Order.row_number.partition_by(:user).window_order(:amount).as(:rn)
94
- Order.row_number.partition_by(:user_id).window_order(:amount).as(:rn)
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)
95
95
 
96
96
  # Works in the hash API too:
97
- Order.window(row_number: { partition: :user, order: :amount, as: :rn })
97
+ Order.window(row_number: { partition: :user, order_by: :amount, as: :rn })
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)
98
109
  ```
99
110
 
100
111
  ### Chaining with ActiveRecord
@@ -105,14 +116,14 @@ Window functions integrate naturally with standard ActiveRecord methods:
105
116
  User.where(active: true)
106
117
  .row_number
107
118
  .partition_by(:department)
108
- .window_order(:salary)
119
+ .order_by(:salary)
109
120
  .as(:rank)
110
121
 
111
122
  User.select(:name, :salary)
112
- .window(row_number: { order: :salary, as: :rn })
123
+ .window(row_number: { order_by: :salary, as: :rn })
113
124
 
114
125
  User.where(department: "Engineering")
115
- .window(rank: { order: :salary, as: :salary_rank })
126
+ .window(rank: { order_by: :salary, as: :salary_rank })
116
127
  .limit(10)
117
128
  ```
118
129
 
@@ -123,7 +134,7 @@ When no `.select()` is specified, `*` is automatically included so all model col
123
134
  Window function values are accessible as attributes on the returned records:
124
135
 
125
136
  ```ruby
126
- results = User.row_number.partition_by(:department).window_order(:salary).as(:rank)
137
+ results = User.row_number.partition_by(:department).order_by(:salary).as(:rank)
127
138
 
128
139
  results.each do |user|
129
140
  puts "#{user.name}: rank #{user.attributes['rank']}"
@@ -136,44 +147,44 @@ end
136
147
 
137
148
  ```ruby
138
149
  # ROW_NUMBER() - sequential integer within partition
139
- User.row_number.partition_by(:department).window_order(:salary).as(:rn)
150
+ User.row_number.partition_by(:department).order_by(:salary).as(:rn)
140
151
 
141
152
  # RANK() - rank with gaps for ties
142
- User.rank.partition_by(:department).window_order(:salary).as(:salary_rank)
153
+ User.rank.partition_by(:department).order_by(:salary).as(:salary_rank)
143
154
 
144
155
  # DENSE_RANK() - rank without gaps for ties
145
- User.dense_rank.partition_by(:department).window_order(:salary).as(:dense_salary_rank)
156
+ User.dense_rank.partition_by(:department).order_by(:salary).as(:dense_salary_rank)
146
157
 
147
158
  # PERCENT_RANK() - relative rank as a fraction (0 to 1)
148
- User.percent_rank.window_order(:salary).as(:percentile)
159
+ User.percent_rank.order_by(:salary).as(:percentile)
149
160
 
150
161
  # CUME_DIST() - cumulative distribution (fraction of rows <= current row)
151
- User.cume_dist.window_order(:salary).as(:cumulative)
162
+ User.cume_dist.order_by(:salary).as(:cumulative)
152
163
 
153
164
  # NTILE(n) - divide rows into n roughly equal buckets
154
- User.ntile(4).window_order(:salary).as(:quartile)
165
+ User.ntile(4).order_by(:salary).as(:quartile)
155
166
  ```
156
167
 
157
168
  ### Value Functions
158
169
 
159
170
  ```ruby
160
171
  # LAG(column, offset, default) - value from a preceding row
161
- User.lag(:salary).window_order(:hire_date).as(:prev_salary)
162
- User.lag(:salary, 2).window_order(:hire_date).as(:two_back) # custom offset
163
- User.lag(:salary, 1, 0).window_order(:hire_date).as(:prev_or_zero) # with default
172
+ User.lag(:salary).order_by(:hire_date).as(:prev_salary)
173
+ User.lag(:salary, 2).order_by(:hire_date).as(:two_back) # custom offset
174
+ User.lag(:salary, 1, 0).order_by(:hire_date).as(:prev_or_zero) # with default
164
175
 
165
176
  # LEAD(column, offset, default) - value from a following row
166
- User.lead(:salary).window_order(:hire_date).as(:next_salary)
167
- User.lead(:salary, 2, 0).window_order(:hire_date).as(:two_ahead)
177
+ User.lead(:salary).order_by(:hire_date).as(:next_salary)
178
+ User.lead(:salary, 2, 0).order_by(:hire_date).as(:two_ahead)
168
179
 
169
180
  # FIRST_VALUE(column) - first value in the window frame
170
- User.first_value(:name).partition_by(:department).window_order(:salary).as(:lowest_paid)
181
+ User.first_value(:name).partition_by(:department).order_by(:salary).as(:lowest_paid)
171
182
 
172
183
  # LAST_VALUE(column) - last value in the window frame
173
- User.last_value(:name).partition_by(:department).window_order(:salary).as(:highest_paid)
184
+ User.last_value(:name).partition_by(:department).order_by(:salary).as(:highest_paid)
174
185
 
175
186
  # NTH_VALUE(column, n) - nth value in the window frame
176
- User.nth_value(:name, 2).partition_by(:department).window_order(:salary).as(:second_lowest)
187
+ User.nth_value(:name, 2).partition_by(:department).order_by(:salary).as(:second_lowest)
177
188
  ```
178
189
 
179
190
  ### Aggregate Window Functions
@@ -206,7 +217,7 @@ Pass a raw SQL frame clause via the hash API:
206
217
  User.window(sum: {
207
218
  value: :salary,
208
219
  partition: :department,
209
- order: :hire_date,
220
+ order_by: :hire_date,
210
221
  frame: "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
211
222
  as: :running_total
212
223
  })
@@ -219,7 +230,7 @@ User.window(sum: {
219
230
  ```ruby
220
231
  User.rank
221
232
  .partition_by(:department)
222
- .window_order(:salary)
233
+ .order_by(:salary)
223
234
  .as(:salary_rank)
224
235
  ```
225
236
 
@@ -228,7 +239,7 @@ User.rank
228
239
  ```ruby
229
240
  User.window(sum: {
230
241
  value: :salary,
231
- order: :hire_date,
242
+ order_by: :hire_date,
232
243
  frame: "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
233
244
  as: :running_total
234
245
  })
@@ -250,14 +261,14 @@ end
250
261
  ```ruby
251
262
  User.lag(:name)
252
263
  .partition_by(:department)
253
- .window_order(:hire_date)
264
+ .order_by(:hire_date)
254
265
  .as(:previous_hire)
255
266
  ```
256
267
 
257
268
  ### Divide employees into salary quartiles
258
269
 
259
270
  ```ruby
260
- User.ntile(4).window_order(:salary).as(:quartile)
271
+ User.ntile(4).order_by(:salary).as(:quartile)
261
272
  ```
262
273
 
263
274
  ### Rank users by total order amount (with joins)
@@ -266,7 +277,7 @@ User.ntile(4).window_order(:salary).as(:quartile)
266
277
  User.joins(:orders)
267
278
  .group(:id)
268
279
  .select("users.*, SUM(orders.amount) AS total_spent")
269
- .window(rank: { order: "total_spent", as: :spending_rank })
280
+ .window(rank: { order_by: "total_spent", as: :spending_rank })
270
281
  ```
271
282
 
272
283
  ### Window function on joined data
@@ -276,7 +287,7 @@ User.joins(:orders)
276
287
  Order.joins(:user)
277
288
  .row_number
278
289
  .partition_by("users.id")
279
- .window_order(amount: :desc)
290
+ .order_by(amount: :desc)
280
291
  .as(:order_rank)
281
292
 
282
293
  # Number each user's orders chronologically
@@ -284,7 +295,7 @@ Order.joins(:user)
284
295
  .select("orders.*, users.name AS user_name")
285
296
  .row_number
286
297
  .partition_by(:user_id)
287
- .window_order(:created_at)
298
+ .order_by(:created_at)
288
299
  .as(:order_number)
289
300
  ```
290
301
 
@@ -297,7 +308,7 @@ Order.joins(:user)
297
308
  .window(sum: {
298
309
  value: :amount,
299
310
  partition: :user_id,
300
- order: :created_at,
311
+ order_by: :created_at,
301
312
  frame: "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW",
302
313
  as: :running_total
303
314
  })
@@ -25,7 +25,7 @@ module ActiveWindows
25
25
  self
26
26
  end
27
27
 
28
- def window_order(*columns)
28
+ def order_by(*columns)
29
29
  @order_columns = columns.flatten
30
30
  self
31
31
  end
@@ -33,7 +33,7 @@ module ActiveWindows
33
33
  def to_window_hash
34
34
  options = {}
35
35
  options[:partition] = @partition_columns unless @partition_columns.empty?
36
- options[:order] = @order_columns unless @order_columns.empty?
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?
39
39
  { @function => options }
@@ -65,7 +65,7 @@ module ActiveWindows
65
65
  end
66
66
 
67
67
  module QueryMethods
68
- VALID_WINDOW_OPTIONS = %i[value partition order frame as].freeze
68
+ VALID_WINDOW_OPTIONS = %i[value partition order_by frame as].freeze
69
69
 
70
70
  # Non-mutating: returns a new relation with window function projections
71
71
  def window(*args)
@@ -78,7 +78,7 @@ module ActiveWindows
78
78
  # Auto-join has_one associations referenced in partition/order
79
79
  joins_needed = processed.flat_map do |_name, options|
80
80
  next [] unless options.is_a?(Hash)
81
- association_joins_for(options[:partition]) + association_joins_for(options[:order])
81
+ association_joins_for(options[:partition]) + association_joins_for(options[:order_by])
82
82
  end.uniq
83
83
  result = result.joins(*joins_needed) if joins_needed.any?
84
84
 
@@ -166,7 +166,7 @@ module ActiveWindows
166
166
  window = Arel::Nodes::Window.new
167
167
 
168
168
  apply_window_partition(window, options[:partition])
169
- apply_window_order(window, options[:order])
169
+ apply_window_order(window, options[:order_by])
170
170
  apply_window_frame(window, options[:frame]) if options[:frame]
171
171
 
172
172
  expressions = extract_window_value(options[:value])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveWindows
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_windows
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Andriichuk