active_record_extended_telescope 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +870 -0
  3. data/lib/active_record_extended.rb +10 -0
  4. data/lib/active_record_extended/active_record.rb +25 -0
  5. data/lib/active_record_extended/active_record/relation_patch.rb +50 -0
  6. data/lib/active_record_extended/arel.rb +7 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +49 -0
  9. data/lib/active_record_extended/arel/predications.rb +50 -0
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +122 -0
  12. data/lib/active_record_extended/patch/5_1/where_clause.rb +11 -0
  13. data/lib/active_record_extended/patch/5_2/where_clause.rb +11 -0
  14. data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +20 -0
  15. data/lib/active_record_extended/query_methods/any_of.rb +93 -0
  16. data/lib/active_record_extended/query_methods/either.rb +62 -0
  17. data/lib/active_record_extended/query_methods/inet.rb +88 -0
  18. data/lib/active_record_extended/query_methods/json.rb +329 -0
  19. data/lib/active_record_extended/query_methods/select.rb +118 -0
  20. data/lib/active_record_extended/query_methods/unionize.rb +249 -0
  21. data/lib/active_record_extended/query_methods/where_chain.rb +132 -0
  22. data/lib/active_record_extended/query_methods/window.rb +93 -0
  23. data/lib/active_record_extended/query_methods/with_cte.rb +150 -0
  24. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  25. data/lib/active_record_extended/utilities/support.rb +178 -0
  26. data/lib/active_record_extended/version.rb +5 -0
  27. data/lib/active_record_extended_telescope.rb +4 -0
  28. data/spec/active_record_extended_spec.rb +7 -0
  29. data/spec/query_methods/any_of_spec.rb +131 -0
  30. data/spec/query_methods/array_query_spec.rb +64 -0
  31. data/spec/query_methods/either_spec.rb +59 -0
  32. data/spec/query_methods/hash_query_spec.rb +45 -0
  33. data/spec/query_methods/inet_query_spec.rb +112 -0
  34. data/spec/query_methods/json_spec.rb +157 -0
  35. data/spec/query_methods/select_spec.rb +115 -0
  36. data/spec/query_methods/unionize_spec.rb +165 -0
  37. data/spec/query_methods/window_spec.rb +51 -0
  38. data/spec/query_methods/with_cte_spec.rb +50 -0
  39. data/spec/spec_helper.rb +28 -0
  40. data/spec/sql_inspections/any_of_sql_spec.rb +41 -0
  41. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  42. data/spec/sql_inspections/arel/array_spec.rb +63 -0
  43. data/spec/sql_inspections/arel/inet_spec.rb +66 -0
  44. data/spec/sql_inspections/contains_sql_queries_spec.rb +47 -0
  45. data/spec/sql_inspections/either_sql_spec.rb +55 -0
  46. data/spec/sql_inspections/json_sql_spec.rb +82 -0
  47. data/spec/sql_inspections/unionize_sql_spec.rb +124 -0
  48. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  49. data/spec/sql_inspections/with_cte_sql_spec.rb +95 -0
  50. data/spec/support/database_cleaner.rb +15 -0
  51. data/spec/support/models.rb +68 -0
  52. metadata +245 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e3c7d18759476d8a5ae2838caf022efbdc4bb0bf16f061ced3e70b7ee99e1628
4
+ data.tar.gz: 52ac7059885330ad79543140bd4f0557a226336cecbf4cc6d0ff7b20653ac314
5
+ SHA512:
6
+ metadata.gz: a1fdbb68d6dddb7b270629af0317062651e4d0a07d54f1a05ca25229d0f2d671c080f219b9615bebd6f6b1f71004690a367dc393148a5fb0a10019fea0386f4e
7
+ data.tar.gz: 53242044f5405efd7c12b7360a0a9bb64b14eeb5b67d842ee96bc1b525e5f8aa35ff809665a778387045c1e55f7695cfd50b3473d324d4fa64d66d8ba8a14dd1
data/README.md ADDED
@@ -0,0 +1,870 @@
1
+ [![Gem Version](https://badge.fury.io/rb/active_record_extended.svg)](https://badge.fury.io/rb/active_record_extended)
2
+ [![Build Status](https://travis-ci.com/GeorgeKaraszi/ActiveRecordExtended.svg?branch=master)](https://travis-ci.com/GeorgeKaraszi/ActiveRecordExtended)
3
+ [![Maintainability](https://api.codeclimate.com/v1/badges/98ecffc0239417098cbc/maintainability)](https://codeclimate.com/github/GeorgeKaraszi/active_record_extended/maintainability)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/f22154211bb3a8feb89f/test_coverage)](https://codeclimate.com/github/GeorgeKaraszi/ActiveRecordExtended/test_coverage)
5
+ ## Index
6
+ - [Description and history](#description-and-history)
7
+ - [Compatibility](#compatibility)
8
+ - [Installation](#installation)
9
+ - [Usage](#usage)
10
+ - [Predicate Query Methods](#predicate-query-methods)
11
+ - [Any](#any)
12
+ - [All](#all)
13
+ - [Contains](#contains)
14
+ - [Overlap](#overlap)
15
+ - [Inet / IP Address](#inet--ip-address)
16
+ - [Inet Contains](#inet-contains)
17
+ - [Inet Contains or Equals](#inet-contains-or-equals)
18
+ - [Inet Contained Within](#inet-contained-within)
19
+ - [Inet Contained Within or Equals](#inet-contained-within-or-equals)
20
+ - [Inet Contains or Contained Within](#inet-contains-or-contained-within)
21
+ - [Conditional Methods](#conditional-methods)
22
+ - [Any_of / None_of](#any_of--none_of)
23
+ - [Either Join](#either-join)
24
+ - [Either Order](#either-order)
25
+ - [Common Table Expressions (CTE)](#common-table-expressions-cte)
26
+ - [Subquery CTE Gotchas](#subquery-cte-gotchas)
27
+ - [JSON Query Methods](#json-query-methods)
28
+ - [Row To JSON](#row-to-json)
29
+ - [JSON/B Build Object](#jsonb-build-object)
30
+ - [JSON/B Build Literal](#jsonb-build-literal)
31
+ - [Unionization](#unionization)
32
+ - [Union](#union)
33
+ - [Union ALL](#union-all)
34
+ - [Union Except](#union-except)
35
+ - [Union Intersect](#union-intersect)
36
+ - [Union As](#union-as)
37
+ - [Union Order](#union-order)
38
+ - [Union Reorder](#union-reorder)
39
+ - [Window Functions](#window-functions)
40
+ - [Define Window](#define-window)
41
+ - [Select Window](#select-window)
42
+
43
+ ## Description and History
44
+
45
+ Active Record Extended is the continuation of maintaining and improving the work done by **Dan McClain**, the original author of [postgres_ext](https://github.com/DavyJonesLocker/postgres_ext).
46
+
47
+ Overtime the lack of updating to support the latest versions of ActiveRecord 5.x has caused quite a bit of users forking off the project to create their own patches jobs to maintain compatibility.
48
+ The only problem is that this has created a wild west of environments of sorts. The problem has grown to the point no one is attempting to directly contribute to the original source. And forked repositories are finding themselves as equally as dead with little to no activity.
49
+
50
+ Active Record Extended is essentially providing users with the other half of Postgreses querying abilities. Due to Rails/ActiveRecord/Arel being designed to be DB agnostic, there are a lot of left out features; Either by choice or the simple lack of supporting API's for other databases. However some features are not exactly PG explicit. Some are just helper methods to express an idea much more easily.
51
+
52
+ ## Compatibility
53
+
54
+ This package is designed align and work with any officially supported Ruby and Rails versions.
55
+ - Minimum Ruby Version: 2.4.x **(EOL warning!)**
56
+ - Minimum Rails Version: 5.1.x **(EOL warning!)**
57
+ - Minimum Postgres Version: 9.6.x **(EOL warning!)**
58
+ - Latest Ruby supported: 2.7.x
59
+ - Latest Rails supported: 6.1.x
60
+ - Postgres: 9.6-current(13) (probably works with most older versions to a certain point)
61
+
62
+ ## Installation
63
+
64
+ Add this line to your application's Gemfile:
65
+
66
+ ```ruby
67
+ gem 'active_record_extended'
68
+ ```
69
+
70
+ And then execute:
71
+
72
+ $ bundle
73
+
74
+ ## Usage
75
+
76
+ ### Predicate Query Methods
77
+
78
+ #### Any
79
+ [Postgres 'ANY' expression](https://www.postgresql.org/docs/10/static/functions-comparisons.html#id-1.5.8.28.16)
80
+
81
+ In Postgres the `ANY` expression is used for gather record's that have an Array column type that contain a single matchable value within its array.
82
+
83
+ ```ruby
84
+ alice = User.create!(tags: [1])
85
+ bob = User.create!(tags: [1, 2])
86
+ randy = User.create!(tags: [3])
87
+
88
+ User.where.any(tags: 1) #=> [alice, bob]
89
+
90
+ ```
91
+
92
+ This only accepts a single value. So querying for example multiple tag numbers `[1,2]` will return nothing.
93
+
94
+
95
+ #### All
96
+ [Postgres 'ALL' expression](https://www.postgresql.org/docs/10/static/functions-comparisons.html#id-1.5.8.28.17)
97
+
98
+ In Postgres the `ALL` expression is used for gather record's that have an Array column type that contains only a **single** and matchable element.
99
+
100
+ ```ruby
101
+ alice = User.create!(tags: [1])
102
+ bob = User.create!(tags: [1, 2])
103
+ randy = User.create!(tags: [3])
104
+
105
+ User.where.all(tags: 1) #=> [alice]
106
+
107
+ ```
108
+
109
+ This only accepts a single value to a given attribute. So querying for example multiple tag numbers `[1,2]` will return nothing.
110
+
111
+ #### Contains
112
+ [Postgres '@>' (Array type) Contains expression](https://www.postgresql.org/docs/10/static/functions-array.html)
113
+
114
+ [Postgres '@>' (JSONB/HSTORE type) Contains expression](https://www.postgresql.org/docs/10/static/functions-json.html#FUNCTIONS-JSONB-OP-TABLE)
115
+
116
+
117
+ The `contains/1` method is used for finding any elements in an `Array`, `JSONB`, or `HSTORE` column type.
118
+ That contains all of the provided values.
119
+
120
+ Array Type:
121
+ ```ruby
122
+ alice = User.create!(tags: [1, 4])
123
+ bob = User.create!(tags: [3, 1])
124
+ randy = User.create!(tags: [4, 1])
125
+
126
+ User.where.contains(tags: [1, 4]) #=> [alice, randy]
127
+ ```
128
+
129
+ HSTORE / JSONB Type:
130
+ ```ruby
131
+ alice = User.create!(data: { nickname: "ARExtend" })
132
+ bob = User.create!(data: { nickname: "ARExtended" })
133
+ randy = User.create!(data: { nickname: "ARExtended" })
134
+
135
+ User.where.contains(data: { nickname: "ARExtended" }) #=> [bob, randy]
136
+ ```
137
+
138
+ #### Overlap
139
+ [Postgres && (overlap) Expression](https://www.postgresql.org/docs/10/static/functions-array.html)
140
+
141
+ The `overlap/1` method will match an Array column type that contains any of the provided values within its column.
142
+
143
+ ```ruby
144
+ alice = User.create!(tags: [1, 4])
145
+ bob = User.create!(tags: [3, 4])
146
+ randy = User.create!(tags: [4, 8])
147
+
148
+ User.where.overlap(tags: [4]) #=> [alice, bob, randy]
149
+ User.where.overlap(tags: [1, 8]) #=> [alice, randy]
150
+ User.where.overlap(tags: [1, 3, 8]) #=> [alice, bob, randy]
151
+
152
+ ```
153
+
154
+ #### Inet / IP Address
155
+ ##### Inet Contains
156
+ [Postgres >> (contains) Network Expression](https://www.postgresql.org/docs/current/static/functions-net.html)
157
+
158
+ The `inet_contains` method works by taking a column(inet type) that has a submask prepended to it.
159
+ And tries to find related records that fall within a given IP's range.
160
+
161
+ ```ruby
162
+ alice = User.create!(ip: "127.0.0.1/16")
163
+ bob = User.create!(ip: "192.168.0.1/16")
164
+
165
+ User.where.inet_contains(ip: "127.0.0.254") #=> [alice]
166
+ User.where.inet_contains(ip: "192.168.20.44") #=> [bob]
167
+ User.where.inet_contains(ip: "192.255.1.1") #=> []
168
+ ```
169
+
170
+ ##### Inet Contains or Equals
171
+ [Postgres >>= (contains or equals) Network Expression](https://www.postgresql.org/docs/current/static/functions-net.html)
172
+
173
+ The `inet_contains_or_equals` method works much like the [Inet Contains](#inet-contains) method, but will also accept a submask range.
174
+
175
+ ```ruby
176
+ alice = User.create!(ip: "127.0.0.1/10")
177
+ bob = User.create!(ip: "127.0.0.44/24")
178
+
179
+ User.where.inet_contains_or_equals(ip: "127.0.0.1/16") #=> [alice]
180
+ User.where.inet_contains_or_equals(ip: "127.0.0.1/10") #=> [alice]
181
+ User.where.inet_contains_or_equals(ip: "127.0.0.1/32") #=> [alice, bob]
182
+ ```
183
+
184
+ ##### Inet Contained Within
185
+ [Postgres << (contained by) Network Expression](https://www.postgresql.org/docs/current/static/functions-net.html)
186
+
187
+ For the `inet_contained_within` method, we try to find IP's that fall within a submasking range we provide.
188
+
189
+ ```ruby
190
+ alice = User.create!(ip: "127.0.0.1")
191
+ bob = User.create!(ip: "127.0.0.44")
192
+ randy = User.create!(ip: "127.0.55.20")
193
+
194
+ User.where.inet_contained_within(ip: "127.0.0.1/24") #=> [alice, bob]
195
+ User.where.inet_contained_within(ip: "127.0.0.1/16") #=> [alice, bob, randy]
196
+ ```
197
+
198
+ ##### Inet Contained Within or Equals
199
+ [Postgres <<= (contained by or equals) Network Expression](https://www.postgresql.org/docs/current/static/functions-net.html)
200
+
201
+ The `inet_contained_within_or_equals` method works much like the [Inet Contained Within](#inet-contained-within) method, but will also accept a submask range.
202
+
203
+ ```ruby
204
+ alice = User.create!(ip: "127.0.0.1/10")
205
+ bob = User.create!(ip: "127.0.0.44/32")
206
+ randy = User.create!(ip: "127.0.99.1")
207
+
208
+ User.where.inet_contained_within_or_equals(ip: "127.0.0.44/32") #=> [bob]
209
+ User.where.inet_contained_within_or_equals(ip: "127.0.0.1/16") #=> [bob, randy]
210
+ User.where.inet_contained_within_or_equals(ip: "127.0.0.44/8") #=> [alice, bob, randy]
211
+ ```
212
+
213
+ ##### Inet Contains or Contained Within
214
+ [Postgres && (contains or is contained by) Network Expression](https://www.postgresql.org/docs/current/static/functions-net.html)
215
+
216
+ The `inet_contains_or_contained_within` method is a combination of [Inet Contains](#inet-contains) and [Inet Contained Within](#inet-contained-within).
217
+ It essentially (the database) tries to use both methods to find as many records as possible that match either condition on both sides.
218
+
219
+ ```ruby
220
+ alice = User.create!(ip: "127.0.0.1/24")
221
+ bob = User.create!(ip: "127.0.22.44/8")
222
+ randy = User.create!(ip: "127.0.99.1")
223
+
224
+ User.where.inet_contains_or_is_contained_within(ip: "127.0.255.80") #=> [bob]
225
+ User.where.inet_contains_or_is_contained_within(ip: "127.0.0.80") #=> [alice, bob]
226
+ User.where.inet_contains_or_is_contained_within(ip: "127.0.0.80/8") #=> [alice, bob, randy]
227
+ ```
228
+
229
+ ### Conditional Methods
230
+ #### Any_of / None_of
231
+ `any_of/1` simplifies the process of finding records that require multiple `or` conditions.
232
+
233
+ `none_of/1` is the inverse of `any_of/1`. It'll find records where none of the contains are matched.
234
+
235
+ Both accepts An array of: ActiveRecord Objects, Query Strings, and basic attribute names.
236
+
237
+ Querying With Attributes:
238
+ ```ruby
239
+ alice = User.create!(uid: 1)
240
+ bob = User.create!(uid: 2)
241
+ randy = User.create!(uid: 3)
242
+
243
+ User.where.any_of({ uid: 1 }, { uid: 2 }) #=> [alice, bob]
244
+ ```
245
+
246
+ Querying With ActiveRecord Objects:
247
+ ```ruby
248
+ alice = User.create!(uid: 1)
249
+ bob = User.create!(uid: 2)
250
+ randy = User.create!(uid: 3)
251
+
252
+ uid_one = User.where(uid: 1)
253
+ uid_two = User.where(uid: 2)
254
+
255
+ User.where.any_of(uid_one, uid_two) #=> [alice, bob]
256
+ ```
257
+
258
+ Querying with Joined Relationships:
259
+ ```ruby
260
+ alice = User.create!(uid: 1)
261
+ bob = User.create!(uid: 2)
262
+ randy = User.create!(uid: 3)
263
+ tag_alice = Tag.create!(user_id: alice.id)
264
+ tag_bob = Tag.create!(user_id: bob.id)
265
+ tag_randy = Tag.create!(user_id: randy.id)
266
+
267
+ bob_tag_query = Tag.where(users: { id: bob.id }).includes(:user)
268
+ randy_tag_query = Tag.where(users: { id: randy.id }).joins(:user)
269
+
270
+ Tag.joins(:user).where.any_of(bob_tag_query, randy_tag_query) #=> [tag_bob, tag_randy] (with users table joined)
271
+ ```
272
+
273
+ #### Either Join
274
+
275
+ The `#either_join/2` method is a base ActiveRecord querying method that will joins records based on a set of conditionally joinable tables.
276
+
277
+ ```ruby
278
+ class User < ActiveRecord::Base
279
+ has_one :profile_l, class: "ProfileL"
280
+ has_one :profile_r, class: "ProfileR"
281
+
282
+ scope :completed_profile, -> { either_joins(:profile_l, :profile_r) }
283
+ end
284
+
285
+ alice = User.create!
286
+ bob = User.create!
287
+ randy = User.create! # Does not have a single completed profile type
288
+ ProfileL.create!(user_id: alice.id)
289
+ ProfileR.create!(user_id: bob.id)
290
+
291
+ User.completed_profile #=> [alice, bob]
292
+ # alternatively
293
+ User.either_joins(:profile_l, :profile_r) #=> [alice, bob]
294
+ ```
295
+
296
+ #### Either Order
297
+
298
+ The `#either_order/3` method is a base ActiveRecord querying method that will order a set of columns that may or may not exist for each record.
299
+ This works similar to how [Either Join](#either-join) works. This does not however exclude records that do not have relationships.
300
+
301
+ ```ruby
302
+ alice = User.create!
303
+ bob = User.create!
304
+ ProfileL.create!(user_id: alice.id, left_turns: 100)
305
+ ProfileR.create!(user_id: bob.id, right_turns: 50)
306
+
307
+ User.either_order(:asc, profile_l: :left_turns, profile_r: :right_turns) #=> [bob, alice]
308
+ User.either_order(:desc, profile_l: :left_turns, profile_r: :right_turns) #=> [alice, bob]
309
+
310
+ randy = User.create!
311
+ User.either_order(:asc, profile_l: :left_turns, profile_r: :right_turns) #=> [bob, alice, randy]
312
+ User.either_order(:desc, profile_l: :left_turns, profile_r: :right_turns) #=> [randy, alice, bob]
313
+ ```
314
+
315
+ ### Common Table Expressions (CTE)
316
+ [Postgres WITH (CTE) Statement](https://www.postgresql.org/docs/current/static/queries-with.html)
317
+
318
+ The `.with/1` method is a base ActiveRecord querying method that will aid in creating complex queries.
319
+
320
+ ```ruby
321
+ alice = User.create!
322
+ bob = User.create!
323
+ randy = User.create!
324
+ ProfileL.create!(user_id: alice.id, likes: 200)
325
+ ProfileL.create!(user_id: bob.id, likes: 400)
326
+ ProfileL.create!(user_id: randy.id, likes: 600)
327
+
328
+ User.with(highly_liked: ProfileL.where("likes > 300"))
329
+ .joins("JOIN highly_liked ON highly_liked.user_id = users.id") #=> [bob, randy]
330
+ ```
331
+
332
+ Query output:
333
+
334
+ ```sql
335
+ WITH "highly_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
336
+ SELECT "users".*
337
+ FROM "users"
338
+ JOIN highly_liked ON highly_liked.user_id = users.id
339
+ ```
340
+
341
+ You can also chain or provide additional arguments to the `with/1` method for it to merge into a single, `WITH` statement.
342
+
343
+ ```ruby
344
+ User.with(highly_liked: ProfileL.where("likes > 300"), less_liked: ProfileL.where("likes <= 200"))
345
+ .joins("JOIN highly_liked ON highly_liked.user_id = users.id")
346
+ .joins("JOIN less_liked ON less_liked.user_id = users.id")
347
+
348
+ # OR
349
+
350
+ User.with(highly_liked: ProfileL.where("likes > 300"))
351
+ .with(less_liked: ProfileL.where("likes <= 200"))
352
+ .joins("JOIN highly_liked ON highly_liked.user_id = users.id")
353
+ .joins("JOIN less_liked ON less_liked.user_id = users.id")
354
+ ```
355
+
356
+ Query output:
357
+
358
+ ```sql
359
+ WITH "highly_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes > 300)),
360
+ "less_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes <= 200))
361
+ SELECT "users".*
362
+ FROM "users"
363
+ JOIN highly_liked ON highly_liked.user_id = users.id
364
+ JOIN less_liked ON less_liked.user_id = users.id
365
+ ```
366
+
367
+ #### Subquery CTE Gotchas
368
+ In order keep queries PG valid, subquery explicit methods (like Unions and JSON methods)
369
+ will be subject to "Piping" the CTE clauses up to the parents query level.
370
+
371
+ This also means there's potential for having duplicate CTE names.
372
+ In order to combat duplicate CTE references with the same name, **piping will favor the parents CTE over the nested sub-queries**.
373
+
374
+ This also means that this is a "First come First Served" implementation.
375
+ So if you have a parent with no CTE's but two sub-queries with the same CTE name but with different querying statements.
376
+ It will process and favor the one that comes first.
377
+
378
+ Example:
379
+ ```ruby
380
+ sub_query = Person.with(dupped_cte: Person.where(id: 1)).select("dup_cte.id").from(:dup_cte)
381
+ other_subquery = Person.with(unique_cte: Person.where(id: 5)).select("unique_cte.id").from(:unique_cte)
382
+
383
+ # Will favor this CTE below, over the `sub_query`'s CTE
384
+ Person.with(dupped_cte: Person.where.not(id: 1..4)).union(sub_query, other_subquery)
385
+ ```
386
+
387
+ Query Output
388
+ ```sql
389
+ WITH "unique_cte" AS (
390
+ SELECT "people".*
391
+ FROM "people"
392
+ WHERE "people"."id" = 5
393
+ ), "dupped_cte" AS (
394
+ SELECT "people".*
395
+ FROM "people"
396
+ WHERE NOT ("people"."id" BETWEEN 1 AND 4)
397
+ )
398
+ SELECT "people".*
399
+ FROM (( (
400
+ SELECT dup_cte.id
401
+ FROM dup_cte
402
+ ) UNION (
403
+ SELECT unique_cte.id
404
+ FROM unique_cte
405
+ ) )) people
406
+ ```
407
+
408
+
409
+ ### JSON Query Methods
410
+ If any or all of your json sub-queries include a CTE, read the [Subquery CTE Gotchas](#subquery-cte-gotchas) warnings.
411
+
412
+ #### Row To JSON
413
+ [Postgres 'ROW_TO_JSON' function](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-CREATION-TABLE)
414
+
415
+ The implementation of the`.row_to_json/2` method is designed to be used with sub-queries. As a means for taking complex
416
+ query logic and transform them into a single or multiple json responses. These responses are required to be assigned
417
+ to an aliased column on the parent(callee) level.
418
+
419
+ While quite the mouthful of an explanation. The implementation of combining unrelated or semi-related queries is quite smooth(imo).
420
+
421
+ ```ruby
422
+ physical_cat = Category.create!(name: "Physical")
423
+ products = 3.times.map { Product.create! }
424
+ products.each { |product| 100.times { Variant.create!(product: product, category: physical_cat) } }
425
+
426
+ # Since we plan to nest this query, you have access top level information. (I.E categories table)
427
+ item_query = Variant.select(:name, :id, :category_id, :product_id).where("categories.id = variants.category_id")
428
+
429
+ # You can provide addition scopes that will be applied to the nested query (but will not effect the actual inner query)
430
+ # This is ideal if you are dealing with but not limited to, CTE's being applied multiple times and require additional constraints
431
+ product_query =
432
+ Product.select(:id)
433
+ .joins(:items)
434
+ .select_row_to_json(item_query, key: :outer_items, as: :items, cast_with: :array) do |item_scope|
435
+ item_scope.where("outer_items.product_id = products.id")
436
+ # Results to:
437
+ # SELECT ..., ARRAY(SELECT ROW_TO_JSON("outer_items")
438
+ # FROM ([:item_query:]) outer_items
439
+ # WHERE outer_items.product_id = products.id
440
+ # ) AS items
441
+ end
442
+
443
+ # Not defining a key will automatically generate a random key between a-z
444
+ category_query = Category.select(:name, :id).select_row_to_json(product_query, as: :products, cast_with: :array)
445
+ Category.json_build_object(:physical_category, category_query.where(id: physical_cat.id)).results
446
+ #=> {
447
+ # "physical_category" => {
448
+ # "name" => "Physical",
449
+ # "id" => 1,
450
+ # "products" => [
451
+ # {
452
+ # "id" => 2,
453
+ # "items" => [{"name" => "Bojangels", "id" => 3, "category_id" => 1, "product_id" => 2}, ...]
454
+ # },
455
+ # ...
456
+ # ]
457
+ # }
458
+ # }
459
+ #
460
+ ```
461
+
462
+ Query Output
463
+ ```sql
464
+ SELECT (JSON_BUILD_OBJECT('physical_category', "physical_category")) AS "results"
465
+ FROM (
466
+ SELECT "categories"."name", "categories"."id", (ARRAY(
467
+ SELECT ROW_TO_JSON("j")
468
+ FROM (
469
+ SELECT "products"."id", (ARRAY(
470
+ SELECT ROW_TO_JSON("outer_item")
471
+ FROM (
472
+ SELECT "variants"."name", "variants"."id", "variants"."category_id", "variants"."product_id"
473
+ FROM "variants"
474
+ WHERE (categories.id = variants.category_id)
475
+ ) outer_items
476
+ WHERE (outer_items.product_id = products.id)
477
+ )) AS "items"
478
+ FROM "products"
479
+ INNER JOIN "items" ON "products"."id" = "items"."product_id"
480
+ ) j
481
+ )) AS "products"
482
+ FROM "categories"
483
+ WHERE "categories"."id" = 1
484
+ ) AS "physical_category"
485
+ ```
486
+
487
+
488
+ #### JSON/B Build Object
489
+ [Postgres 'json(b)_build_object' function](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-CREATION-TABLE)
490
+
491
+ The implementation of the`.json_build_object/2` and `.jsonb_build_object/2` methods are designed to be used with sub-queries.
492
+ As a means for taking complex query logic and transform them into a single or multiple json responses.
493
+
494
+ **Arguments:**
495
+ - `key`: [Symbol or String]: What should this response return as
496
+ - `from`: [String, Arel, or ActiveRecord::Relation] : A subquery that can be nested into the top-level from clause
497
+
498
+ **Options:**
499
+ - `as`: [Symbol or String] (defaults to `"results"`): What the column will be aliased to
500
+ - `value`: [Symbol or String] (defaults to `key` argument): How the response should handel the json value return
501
+
502
+ See the included example on [Row To JSON](#row-to-json) to see it in action.
503
+
504
+ #### JSON/B Build Literal
505
+ [Postgres 'json(b)_build_object' function](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-CREATION-TABLE)
506
+
507
+ The implementation of the`.json_build_literal/1` and `.jsonb_build_literal/1` is designed for creating static json objects
508
+ that don't require subquery interfacing.
509
+
510
+ **Arguments:**
511
+ - Requires an Array or Hash set of values
512
+
513
+ **Options:**
514
+ - `as`: [Symbol or String] (defaults to `"results"`): What the column will be aliased to
515
+
516
+ ```ruby
517
+ User.json_build_literal(number: 1, last_name: "json", pi: 3.14).take.results
518
+ #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
519
+
520
+ # Or as array elements
521
+ User.json_build_literal(:number, 1, :last_name, "json", :pi, 3.14).take.results
522
+ #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
523
+
524
+ ```
525
+
526
+ Query Output
527
+ ```sql
528
+ SELECT (JSON_BUILD_OBJECT('number', 1, 'last_name', 'json', 'pi', 3.14)) AS "results"
529
+ FROM "users"
530
+ ```
531
+
532
+
533
+ ### Unionization
534
+ If any or all of your union queries include a CTE, read the [Subquery CTE Gotchas](#subquery-cte-gotchas) warnings.
535
+
536
+ #### SQL-Query Helpers
537
+ - `.to_union_sql` : Will return a string of the constructed union query without being nested in the `from` clause.
538
+ - `.to_nice_union_sql`(requires [NiceQL Gem](https://github.com/alekseyl/niceql) to be install): A formatted `.to_union_sql`
539
+
540
+
541
+ #### Known issue
542
+ There's an issue with providing a single union clause and chaining it with a different union clause.
543
+ This is due to requirements of grouping SQL statements. The issue is being working on, but with no ETA.
544
+
545
+ This issue only applies to the first initial set of unions and is recommended that you union two relations right off the bat.
546
+ Afterwords you can union/chain single relations.
547
+
548
+ Example
549
+
550
+ ```ruby
551
+
552
+ Person.union(Person.where(id: 1..4)).union_except(Person.where(id: 3..4)).union(Person.where(id: 4))
553
+ #=> Will include all people with an ID between 1 & 3 (throwing the except on ID 4)
554
+
555
+ # This can be fixed by doing something like
556
+
557
+ Person.union_except(Person.where(id: 1..4), Person.where(id: 3..4)).union(Person.where(id: 4))
558
+ #=> Will include people with the ids of 1, 2, and 4 (properly excluding the user with the ID of 3)
559
+ ```
560
+
561
+ Problem Query Output
562
+ ```sql
563
+ ( ( (
564
+ SELECT "people".*
565
+ FROM "people"
566
+ WHERE "people"."id" BETWEEN 1 AND 4
567
+ ) UNION (
568
+ SELECT "people".*
569
+ FROM "people"
570
+ WHERE "people"."id" BETWEEN 3 AND 4
571
+ ) ) EXCEPT (
572
+ SELECT "people".*
573
+ FROM "people"
574
+ WHERE "people"."id" = 4
575
+ ) )
576
+ ```
577
+
578
+
579
+ #### Union
580
+ [Postgres 'UNION' combination](https://www.postgresql.org/docs/current/queries-union.html)
581
+
582
+ ```ruby
583
+ user_1 = Person.where(id: 1)
584
+ user_2 = Person.where(id: 2)
585
+ users = Person.where(id: 1..3)
586
+
587
+ Person.union(user_1, user_2, users) #=> [#<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 3,..>]
588
+
589
+ # You can also chain union's
590
+ Person.union(user_1).union(user_2).union(users)
591
+ ```
592
+
593
+ Query Output
594
+ ```sql
595
+ SELECT "people".*
596
+ FROM (( ( (
597
+ SELECT "people".*
598
+ FROM "people"
599
+ WHERE "people"."id" = 1
600
+ ) UNION (
601
+ SELECT "people".*
602
+ FROM "people"
603
+ WHERE "people"."id" = 2
604
+ ) ) UNION (
605
+ SELECT "people".*
606
+ FROM "people"
607
+ WHERE "people"."id" BETWEEN 1 AND 3
608
+ ) )) people
609
+ ```
610
+
611
+
612
+ #### Union ALL
613
+ [Postgres 'UNION ALL' combination](https://www.postgresql.org/docs/current/queries-union.html)
614
+
615
+ ```ruby
616
+ user_1 = Person.where(id: 1)
617
+ user_2 = Person.where(id: 2)
618
+ users = Person.where(id: 1..3)
619
+
620
+ Person.union_all(user_1, user_2, users)
621
+ #=> [#<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 3,..>]
622
+
623
+ # You can also chain union's
624
+ Person.union_all(user_1).union_all(user_2).union_all(users)
625
+ # Or
626
+ Person.union.all(user1, user_2).union.all(users)
627
+ ```
628
+
629
+ Query Output
630
+ ```sql
631
+ SELECT "people".*
632
+ FROM (( ( (
633
+ SELECT "people".*
634
+ FROM "people"
635
+ WHERE "people"."id" = 1
636
+ ) UNION ALL (
637
+ SELECT "people".*
638
+ FROM "people"
639
+ WHERE "people"."id" = 2
640
+ ) ) UNION ALL (
641
+ SELECT "people".*
642
+ FROM "people"
643
+ WHERE "people"."id" BETWEEN 1 AND 3
644
+ ) )) people
645
+ ```
646
+
647
+ #### Union Except
648
+ [Postgres 'EXCEPT' combination](https://www.postgresql.org/docs/current/queries-union.html)
649
+
650
+ ```ruby
651
+ users = Person.where(id: 1..5)
652
+ expect_these_users = Person.where(id: 2..4)
653
+
654
+ Person.union_except(users, expect_these_users) #=> [#<Person id: 1, ..>, #<Person id: 5,..>]
655
+
656
+ # You can also chain union's
657
+ Person.union.except(users, expect_these_users).union(Person.where(id: 20))
658
+ ```
659
+
660
+ Query Output
661
+ ```sql
662
+ SELECT "people".*
663
+ FROM (( ( (
664
+ SELECT "people".*
665
+ FROM "people"
666
+ WHERE "people"."id" BETWEEN 1 AND 5
667
+ ) EXCEPT (
668
+ SELECT "people".*
669
+ FROM "people"
670
+ WHERE "people"."id" BETWEEN 2 AND 4
671
+ )) people
672
+ ```
673
+
674
+ #### Union Intersect
675
+ [Postgres 'INTERSECT' combination](https://www.postgresql.org/docs/current/queries-union.html)
676
+
677
+ ```ruby
678
+ randy = Person.create!
679
+ alice = Person.create!
680
+ ProfileL.create!(person: randy, likes: 100)
681
+ ProfileL.create!(person: alice, likes: 120)
682
+
683
+ likes_100 = Person.select(:id, "profile_ls.likes").joins(:profile_l).where(profile_ls: { likes: 100 })
684
+ likes_less_than_150 = Person.select(:id, "profile_ls.likes").joins(:profile_l).where("profile_ls.likes < 150")
685
+ Person.union_intersect(likes_100, likes_less_than_150) #=> [randy]
686
+
687
+
688
+
689
+ # You can also chain union's
690
+ Person.union_intersect(likes_100).union_intersect(likes_less_than_150) #=> [randy]
691
+ # Or
692
+ Person.union.intersect(likes_100, likes_less_than_150) #=> [randy]
693
+
694
+ ```
695
+
696
+ Query Output
697
+ ```sql
698
+ SELECT "people".*
699
+ FROM (( (
700
+ SELECT "people"."id", profile_ls.likes
701
+ FROM "people"
702
+ INNER JOIN "profile_ls" ON "profile_ls"."person_id" = "people"."id"
703
+ WHERE "profile_ls"."likes" = 100
704
+ ) INTERSECT (
705
+ SELECT "people"."id", profile_ls.likes
706
+ FROM "people"
707
+ INNER JOIN "profile_ls" ON "profile_ls"."person_id" = "people"."id"
708
+ WHERE (profile_ls.likes < 150)
709
+ ) )) people
710
+ ```
711
+
712
+ #### Union As
713
+
714
+ By default unions are nested in the from clause and are aliased to the parents table name.
715
+ We can change this behavior by chaining the method `.union_as/1`
716
+
717
+ ```ruby
718
+ Person.select("good_people.id").union(Person.where(id: 1), Person.where(id: 2)).union_as(:good_people)
719
+ ```
720
+
721
+ Query Output
722
+ ```sql
723
+ SELECT good_people.id
724
+ FROM (( (
725
+ SELECT "people".*
726
+ FROM "people"
727
+ WHERE "people"."id" = 1
728
+ ) UNION (
729
+ SELECT "people".*
730
+ FROM "people"
731
+ WHERE "people"."id" = 2
732
+ ) )) good_people
733
+ ```
734
+
735
+
736
+ #### Union Order
737
+
738
+ Unions allow for a final outside `ORDER BY` clause. This will ensure that all the results that come back are ordered in an expected return.
739
+
740
+ ```ruby
741
+ query_1 = Person.where(id: 1..3)
742
+ query_2 = Person.where(id: 3)
743
+ query_3 = Person.where(id: 3..10)
744
+ Person.union_except(query_1, query_2).union(query_3).order_union(:id, tags: :desc)
745
+ ```
746
+
747
+ Query Output
748
+ ```sql
749
+ SELECT "people".*
750
+ FROM (( ( (
751
+ SELECT "people".*
752
+ FROM "people"
753
+ WHERE "people"."id" BETWEEN 1 AND 3
754
+ ) EXCEPT (
755
+ SELECT "people".*
756
+ FROM "people"
757
+ WHERE "people"."id" = 3
758
+ ) ) UNION (
759
+ SELECT "people".*
760
+ FROM "people"
761
+ WHERE "people"."id" BETWEEN 3 AND 10
762
+ ) ) ORDER BY id ASC, tags DESC) people
763
+ ```
764
+
765
+ #### Union Reorder
766
+
767
+ much like Rails `.reorder`; `.reorder_union/1` will clear the previous order in a new instance and/or apply a new ordering scheme
768
+ ```ruby
769
+ query_1 = Person.where(id: 1..3)
770
+ query_2 = Person.where(id: 3)
771
+ query_3 = Person.where(id: 3..10)
772
+ union_query = Person.union_except(query_1, query_2).union(query_3).order_union(:id, tags: :desc)
773
+ union_query.reorder_union(personal_id: :desc, id: :desc)
774
+ ```
775
+
776
+ Query Output
777
+ ```sql
778
+ SELECT "people".*
779
+ FROM (( ( (
780
+ SELECT "people".*
781
+ FROM "people"
782
+ WHERE "people"."id" BETWEEN 1 AND 3
783
+ ) EXCEPT (
784
+ SELECT "people".*
785
+ FROM "people"
786
+ WHERE "people"."id" = 3
787
+ ) ) UNION (
788
+ SELECT "people".*
789
+ FROM "people"
790
+ WHERE "people"."id" BETWEEN 3 AND 10
791
+ ) ) ORDER BY personal_id DESC, id DESC) people
792
+ ```
793
+
794
+ #### Window Functions
795
+ [Postgres Window Functions](https://www.postgresql.org/docs/current/tutorial-window.html)
796
+
797
+ Let's address the elephant in the room. Arel has had, for a long time now, window function capabilities;
798
+ However they've never seen the lime light in ActiveRecord's query logic.
799
+ The following brings the dormant Arel methods up to the ActiveRecord Querying level.
800
+
801
+ #### Define Window
802
+
803
+ To set up a window function, we first must establish the window and we do this by using the `.define_window/1` method.
804
+ This method also requires you to call chain `.partition_by/2`
805
+
806
+ `.define_window/1` - Establishes the name of the window you'll reference later on in [.select_window](#select-window)
807
+ - Aliased name of window
808
+
809
+ `.partition_by/2` - Establishes the windows operations a [pre-defined window function](https://www.postgresql.org/docs/current/functions-window.html) will leverage.
810
+ - column name being partitioned against
811
+ - (**optional**) `order_by`: Processes how the window should be ordered
812
+
813
+ ```ruby
814
+ User
815
+ .define_window(:number_window).partition_by(:number, order_by: { id: :desc })
816
+ .define_window(:name_window).partition_by(:name, order_by: :id)
817
+ .define_window(:no_order_name).partition_by(:name)
818
+ ```
819
+
820
+ Query Output
821
+ ```sql
822
+ SELECT *
823
+ FROM users
824
+ WINDOW number_window AS (PARTITION BY number ORDER BY id DESC),
825
+ name_window AS (PARTITION BY name ORDER BY id),
826
+ no_order_name AS (PARTITION BY name)
827
+ ```
828
+
829
+ #### Select Window
830
+
831
+ Once you've define a window, the next step to to utilize it on one of the many provided postgres window functions.
832
+
833
+ `.select_window/3`
834
+ - [window function name](https://www.postgresql.org/docs/current/functions-window.html)
835
+ - (**optional**) Window function arguments (treated as a splatted array)
836
+ - (**optional**) `as:` : Alias name of the final result
837
+ - `over:` : name of [defined window](#define-window)
838
+
839
+ ```ruby
840
+ User.create!(name: "Alice", number: 100) #=> id: 1
841
+ User.create!(name: "Randy", number: 100) #=> id: 2
842
+ User.create!(name: "Bob", number: 300) #=> id: 3
843
+
844
+ User
845
+ .define_window(:number_window).partition_by(:number, order_by: { id: :desc })
846
+ .select(:id, :name)
847
+ .select_window(:row_number, over: :number_window, as: :row_id)
848
+ .select_window(:first_value, :name, over: :number_window, as: :first_value_name)
849
+ #=> [
850
+ # { id: 1, name: "Alice", row_id: 2, first_value_name: "Randy" }
851
+ # { id: 2, name: "Randy", row_id: 1, first_value_name: "Randy" }
852
+ # { id: 3, name: "Bob", row_id: 1, first_value_name: "Bob" }
853
+ # ]
854
+ #
855
+
856
+ ```
857
+
858
+ Query Output
859
+ ```sql
860
+ SELECT "users"."id",
861
+ "users"."name",
862
+ (ROW_NUMBER() OVER number_window) AS "row_id",
863
+ (FIRST_VALUE(name) OVER number_window) AS "first_value_name"
864
+ FROM "users"
865
+ WINDOW number_window AS (PARTITION BY number ORDER BY id DESC)
866
+ ```
867
+
868
+ ## License
869
+
870
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).