active_record_extended 2.0.3 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +140 -77
  3. data/lib/active_record_extended/arel/nodes.rb +1 -1
  4. data/lib/active_record_extended/arel/{sql_literal.rb → sql_literal_patch.rb} +2 -2
  5. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +16 -12
  6. data/lib/active_record_extended/arel.rb +1 -1
  7. data/lib/active_record_extended/patch/array_handler_patch.rb +22 -0
  8. data/lib/active_record_extended/patch/relation_patch.rb +82 -0
  9. data/lib/active_record_extended/patch/where_clause_patch.rb +13 -0
  10. data/lib/active_record_extended/query_methods/any_of.rb +7 -31
  11. data/lib/active_record_extended/query_methods/either.rb +5 -7
  12. data/lib/active_record_extended/query_methods/{select.rb → foster_select.rb} +4 -4
  13. data/lib/active_record_extended/query_methods/json.rb +2 -2
  14. data/lib/active_record_extended/query_methods/unionize.rb +3 -3
  15. data/lib/active_record_extended/query_methods/where_chain.rb +96 -90
  16. data/lib/active_record_extended/query_methods/window.rb +3 -3
  17. data/lib/active_record_extended/query_methods/with_cte.rb +70 -9
  18. data/lib/active_record_extended/utilities/order_by.rb +1 -1
  19. data/lib/active_record_extended/utilities/support.rb +1 -1
  20. data/lib/active_record_extended/version.rb +1 -1
  21. data/lib/active_record_extended.rb +55 -4
  22. metadata +34 -83
  23. data/lib/active_record_extended/active_record/relation_patch.rb +0 -50
  24. data/lib/active_record_extended/active_record.rb +0 -25
  25. data/lib/active_record_extended/patch/5_1/where_clause.rb +0 -11
  26. data/lib/active_record_extended/patch/5_2/where_clause.rb +0 -11
  27. data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +0 -20
  28. data/spec/active_record_extended_spec.rb +0 -7
  29. data/spec/query_methods/any_of_spec.rb +0 -131
  30. data/spec/query_methods/array_query_spec.rb +0 -64
  31. data/spec/query_methods/either_spec.rb +0 -70
  32. data/spec/query_methods/hash_query_spec.rb +0 -45
  33. data/spec/query_methods/inet_query_spec.rb +0 -112
  34. data/spec/query_methods/json_spec.rb +0 -157
  35. data/spec/query_methods/select_spec.rb +0 -115
  36. data/spec/query_methods/unionize_spec.rb +0 -165
  37. data/spec/query_methods/window_spec.rb +0 -51
  38. data/spec/query_methods/with_cte_spec.rb +0 -50
  39. data/spec/spec_helper.rb +0 -28
  40. data/spec/sql_inspections/any_of_sql_spec.rb +0 -41
  41. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +0 -41
  42. data/spec/sql_inspections/arel/array_spec.rb +0 -63
  43. data/spec/sql_inspections/arel/inet_spec.rb +0 -66
  44. data/spec/sql_inspections/contains_sql_queries_spec.rb +0 -47
  45. data/spec/sql_inspections/either_sql_spec.rb +0 -71
  46. data/spec/sql_inspections/json_sql_spec.rb +0 -82
  47. data/spec/sql_inspections/unionize_sql_spec.rb +0 -124
  48. data/spec/sql_inspections/window_sql_spec.rb +0 -98
  49. data/spec/sql_inspections/with_cte_sql_spec.rb +0 -95
  50. data/spec/support/database_cleaner.rb +0 -15
  51. data/spec/support/models.rb +0 -80
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbcdc3e208ac89fd2d55b8944ad7c0483e57f21a89941a4848beb548b56ce293
4
- data.tar.gz: 10465bfe73686aae4fad33a7da0e533ed22401c68da5655654e1538a8bbb34c0
3
+ metadata.gz: 2611c1b573495ab42f5a15ff8d85bdc8ba95bbe8d077ae037a5eae8e8f29f2c6
4
+ data.tar.gz: 4d6c36a2e09b490969f8f152bda00a45180bbcd0a728fb7344e058faa96da10c
5
5
  SHA512:
6
- metadata.gz: 1ed8f4d6fe19c384bbb8add11443cc78b6f23cf4adc1808bdd0a1b6328a2dbeac22d4e0fa8fc6257dde505646d1c3613ed90711bbd36321cbb74231cf621958d
7
- data.tar.gz: e139454fc996ad112d70332e39b002ba164d2172100f6b61f2494f090a0a3afe16c0e5e51998ab8f8cb8e839c6b0cb1104d152f821ae65d55bec4c83ec9c990a
6
+ metadata.gz: 8bbb84f095b990140d2cc7f6e11de1c7b11a039d4f6cdf02f910c470c9dd5cf71df71dcd344140e02baaec9969000c0d665845e830fff7aeddf3111339b578c2
7
+ data.tar.gz: cc11bebd6832cdb82cacc31d5af01d8dc9123414ef5f2ed3c7c8e995a3e2005352444cd247c79de081f473d9c8fcde7758d7cbdfe156c2b909bcea2f78a24c6d
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
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)
2
+ [![Build Status](https://github.com/GeorgeKaraszi/ActiveRecordExtended/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/GeorgeKaraszi/ActiveRecordExtended/actions/workflows/test.yml?query=branch%3Amaster+)
3
3
  [![Maintainability](https://api.codeclimate.com/v1/badges/98ecffc0239417098cbc/maintainability)](https://codeclimate.com/github/GeorgeKaraszi/active_record_extended/maintainability)
4
4
  [![Test Coverage](https://api.codeclimate.com/v1/badges/f22154211bb3a8feb89f/test_coverage)](https://codeclimate.com/github/GeorgeKaraszi/ActiveRecordExtended/test_coverage)
5
5
  ## Index
@@ -44,7 +44,7 @@
44
44
 
45
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
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.
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
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
49
 
50
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.
@@ -52,12 +52,12 @@ Active Record Extended is essentially providing users with the other half of Pos
52
52
  ## Compatibility
53
53
 
54
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)
55
+ - Minimum Ruby Version: 2.7.x **(EOL warning!)**
56
+ - Minimum Rails Version: 5.2.x **(EOL warning!)**
57
+ - Minimum Postgres Version: 11.x **(EOL warning!)**
58
+ - Latest Ruby supported: 3.2.x
59
+ - Latest Rails supported: 7.0.x
60
+ - Postgres: 11-current(15) (probably works with most older versions to a certain point)
61
61
 
62
62
  ## Installation
63
63
 
@@ -78,14 +78,14 @@ And then execute:
78
78
  #### Any
79
79
  [Postgres 'ANY' expression](https://www.postgresql.org/docs/10/static/functions-comparisons.html#id-1.5.8.28.16)
80
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.
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
82
 
83
83
  ```ruby
84
84
  alice = User.create!(tags: [1])
85
85
  bob = User.create!(tags: [1, 2])
86
86
  randy = User.create!(tags: [3])
87
87
 
88
- User.where.any(tags: 1) #=> [alice, bob]
88
+ User.where.any(tags: 1) #=> [alice, bob]
89
89
 
90
90
  ```
91
91
 
@@ -95,14 +95,14 @@ This only accepts a single value. So querying for example multiple tag numbers `
95
95
  #### All
96
96
  [Postgres 'ALL' expression](https://www.postgresql.org/docs/10/static/functions-comparisons.html#id-1.5.8.28.17)
97
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.
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
99
 
100
100
  ```ruby
101
101
  alice = User.create!(tags: [1])
102
102
  bob = User.create!(tags: [1, 2])
103
103
  randy = User.create!(tags: [3])
104
104
 
105
- User.where.all(tags: 1) #=> [alice]
105
+ User.where.all(tags: 1) #=> [alice]
106
106
 
107
107
  ```
108
108
 
@@ -114,7 +114,7 @@ This only accepts a single value to a given attribute. So querying for example m
114
114
  [Postgres '@>' (JSONB/HSTORE type) Contains expression](https://www.postgresql.org/docs/10/static/functions-json.html#FUNCTIONS-JSONB-OP-TABLE)
115
115
 
116
116
 
117
- The `contains/1` method is used for finding any elements in an `Array`, `JSONB`, or `HSTORE` column type.
117
+ The `contains/1` method is used for finding any elements in an `Array`, `JSONB`, or `HSTORE` column type.
118
118
  That contains all of the provided values.
119
119
 
120
120
  Array Type:
@@ -155,7 +155,7 @@ User.where.overlap(tags: [1, 3, 8]) #=> [alice, bob, randy]
155
155
  ##### Inet Contains
156
156
  [Postgres >> (contains) Network Expression](https://www.postgresql.org/docs/current/static/functions-net.html)
157
157
 
158
- The `inet_contains` method works by taking a column(inet type) that has a submask prepended to it.
158
+ The `inet_contains` method works by taking a column(inet type) that has a submask prepended to it.
159
159
  And tries to find related records that fall within a given IP's range.
160
160
 
161
161
  ```ruby
@@ -188,7 +188,7 @@ For the `inet_contained_within` method, we try to find IP's that fall within a s
188
188
 
189
189
  ```ruby
190
190
  alice = User.create!(ip: "127.0.0.1")
191
- bob = User.create!(ip: "127.0.0.44")
191
+ bob = User.create!(ip: "127.0.0.44")
192
192
  randy = User.create!(ip: "127.0.55.20")
193
193
 
194
194
  User.where.inet_contained_within(ip: "127.0.0.1/24") #=> [alice, bob]
@@ -229,11 +229,11 @@ User.where.inet_contains_or_is_contained_within(ip: "127.0.0.80/8") #=> [alice,
229
229
  ### Conditional Methods
230
230
  #### Any_of / None_of
231
231
  `any_of/1` simplifies the process of finding records that require multiple `or` conditions.
232
-
232
+
233
233
  `none_of/1` is the inverse of `any_of/1`. It'll find records where none of the contains are matched.
234
-
234
+
235
235
  Both accepts An array of: ActiveRecord Objects, Query Strings, and basic attribute names.
236
-
236
+
237
237
  Querying With Attributes:
238
238
  ```ruby
239
239
  alice = User.create!(uid: 1)
@@ -278,7 +278,7 @@ The `#either_join/2` method is a base ActiveRecord querying method that will joi
278
278
  class User < ActiveRecord::Base
279
279
  has_one :profile_l, class: "ProfileL"
280
280
  has_one :profile_r, class: "ProfileR"
281
-
281
+
282
282
  scope :completed_profile, -> { either_joins(:profile_l, :profile_r) }
283
283
  end
284
284
 
@@ -332,9 +332,9 @@ User.with(highly_liked: ProfileL.where("likes > 300"))
332
332
  Query output:
333
333
 
334
334
  ```sql
335
- WITH "highly_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
336
- SELECT "users".*
337
- FROM "users"
335
+ WITH "highly_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
336
+ SELECT "users".*
337
+ FROM "users"
338
338
  JOIN highly_liked ON highly_liked.user_id = users.id
339
339
  ```
340
340
 
@@ -356,31 +356,79 @@ User.with(highly_liked: ProfileL.where("likes > 300"))
356
356
  Query output:
357
357
 
358
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
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
364
  JOIN less_liked ON less_liked.user_id = users.id
365
365
  ```
366
366
 
367
+ There are three methods you can chain to the `with/1` to add modifiers to the query.
368
+ #### `recursive`
369
+
370
+ ```ruby
371
+ User.with.recursive(highly_liked: ProfileL.where("likes > 300"))
372
+ .joins("JOIN highly_liked ON highly_liked.user_id = users.id")
373
+ ```
374
+
375
+ Query output:
376
+
377
+ ```sql
378
+ WITH RECURSIVE "highly_liked" AS (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
379
+ SELECT "users".*
380
+ FROM "users"
381
+ JOIN highly_liked ON highly_liked.user_id = users.id
382
+ ```
383
+ #### `materialized` (**Note**: MATERIALIZED modifier is only available in [PG versions 12+](https://www.postgresql.org/docs/release/12.0/).)
384
+
385
+
386
+ ```ruby
387
+ User.with.materialized(highly_liked: ProfileL.where("likes > 300"))
388
+ .joins("JOIN highly_liked ON highly_liked.user_id = users.id")
389
+ ```
390
+
391
+ Query output:
392
+
393
+ ```sql
394
+ WITH "highly_liked" AS MATERIALIZED (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
395
+ SELECT "users".*
396
+ FROM "users"
397
+ JOIN highly_liked ON highly_liked.user_id = users.id
398
+ ```
399
+ #### `not_materialized` (**Note**: NOT MATERIALIZED modifier is only available in [PG versions 12+](https://www.postgresql.org/docs/release/12.0/).)
400
+
401
+ ```ruby
402
+ User.with.not_materialized(highly_liked: ProfileL.where("likes > 300"))
403
+ .joins("JOIN highly_liked ON highly_liked.user_id = users.id")
404
+ ```
405
+
406
+ Query output:
407
+
408
+ ```sql
409
+ WITH "highly_liked" AS NOT MATERIALIZED (SELECT "profile_ls".* FROM "profile_ls" WHERE (likes >= 300))
410
+ SELECT "users".*
411
+ FROM "users"
412
+ JOIN highly_liked ON highly_liked.user_id = users.id
413
+ ```
414
+
367
415
  #### 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
-
416
+ In order keep queries PG valid, subquery explicit methods (like Unions and JSON methods)
417
+ will be subject to "Piping" the CTE clauses up to the parents query level.
418
+
371
419
  This also means there's potential for having duplicate CTE names.
372
420
  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.
421
+
422
+ This also means that this is a "First come First Served" implementation.
423
+ 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
424
  It will process and favor the one that comes first.
377
-
425
+
378
426
  Example:
379
427
  ```ruby
380
428
  sub_query = Person.with(dupped_cte: Person.where(id: 1)).select("dup_cte.id").from(:dup_cte)
381
429
  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
430
+
431
+ # Will favor this CTE below, over the `sub_query`'s CTE
384
432
  Person.with(dupped_cte: Person.where.not(id: 1..4)).union(sub_query, other_subquery)
385
433
  ```
386
434
 
@@ -404,7 +452,7 @@ WITH "unique_cte" AS (
404
452
  FROM unique_cte
405
453
  ) )) people
406
454
  ```
407
-
455
+
408
456
 
409
457
  ### JSON Query Methods
410
458
  If any or all of your json sub-queries include a CTE, read the [Subquery CTE Gotchas](#subquery-cte-gotchas) warnings.
@@ -412,54 +460,69 @@ If any or all of your json sub-queries include a CTE, read the [Subquery CTE Got
412
460
  #### Row To JSON
413
461
  [Postgres 'ROW_TO_JSON' function](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-CREATION-TABLE)
414
462
 
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
463
+ The implementation of the`.select_row_to_json/2` method is designed to be used with sub-queries. As a means for taking complex
464
+ query logic and transform them into a single or multiple json responses. These responses are required to be assigned
417
465
  to an aliased column on the parent(callee) level.
418
466
 
419
467
  While quite the mouthful of an explanation. The implementation of combining unrelated or semi-related queries is quite smooth(imo).
420
468
 
469
+ **Arguments:**
470
+ - `from` [String, Arel, or ActiveRecord::Relation]: A subquery that can be nested into a `ROW_TO_JSON` clause
471
+
472
+ **Options:**
473
+ - `as` [Symbol or String] (default="results"): What the column will be aliased to
474
+ - `key` [Symbol or String] (default=[random letter]): Internal query alias name.
475
+ * This is useful if you would like to add additional mid-level predicate clauses
476
+ - `cast_with` [Symbol or Array\<Symbol>]:
477
+ * `:to_jsonb`
478
+ * `:array`
479
+ * `:array_agg`
480
+ * `:distinct` (auto applies `:array_agg` & `:to_jsonb`)
481
+ - `order_by` [Symbol or Hash]: Applies an ordering operation (similar to ActiveRecord #order)
482
+ * **Note**: this option will be ignored if you need to order a DISTINCT Aggregated Array.
483
+
421
484
  ```ruby
422
485
  physical_cat = Category.create!(name: "Physical")
423
486
  products = 3.times.map { Product.create! }
424
487
  products.each { |product| 100.times { Variant.create!(product: product, category: physical_cat) } }
425
-
488
+
426
489
  # Since we plan to nest this query, you have access top level information. (I.E categories table)
427
490
  item_query = Variant.select(:name, :id, :category_id, :product_id).where("categories.id = variants.category_id")
428
-
491
+
429
492
  # 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 =
493
+ # This is ideal if you are dealing with but not limited to, CTE's being applied multiple times and require additional constraints
494
+ product_query =
432
495
  Product.select(:id)
433
496
  .joins(:items)
434
497
  .select_row_to_json(item_query, key: :outer_items, as: :items, cast_with: :array) do |item_scope|
435
498
  item_scope.where("outer_items.product_id = products.id")
436
- # Results to:
437
- # SELECT ..., ARRAY(SELECT ROW_TO_JSON("outer_items")
499
+ # Results to:
500
+ # SELECT ..., ARRAY(SELECT ROW_TO_JSON("outer_items")
438
501
  # FROM ([:item_query:]) outer_items
439
502
  # WHERE outer_items.product_id = products.id
440
503
  # ) AS items
441
504
  end
442
-
443
- # Not defining a key will automatically generate a random key between a-z
505
+
506
+ # Not defining a key will automatically generate a random key between a-z
444
507
  category_query = Category.select(:name, :id).select_row_to_json(product_query, as: :products, cast_with: :array)
445
508
  Category.json_build_object(:physical_category, category_query.where(id: physical_cat.id)).results
446
509
  #=> {
447
510
  # "physical_category" => {
448
511
  # "name" => "Physical",
449
- # "id" => 1,
512
+ # "id" => 1,
450
513
  # "products" => [
451
514
  # {
452
515
  # "id" => 2,
453
- # "items" => [{"name" => "Bojangels", "id" => 3, "category_id" => 1, "product_id" => 2}, ...]
516
+ # "items" => [{"name" => "Bojangels", "id" => 3, "category_id" => 1, "product_id" => 2}, ...]
454
517
  # },
455
- # ...
456
- # ]
457
- # }
458
- # }
518
+ # ...
519
+ # ]
520
+ # }
521
+ # }
459
522
  #
460
523
  ```
461
524
 
462
- Query Output
525
+ Query Output
463
526
  ```sql
464
527
  SELECT (JSON_BUILD_OBJECT('physical_category', "physical_category")) AS "results"
465
528
  FROM (
@@ -488,7 +551,7 @@ FROM (
488
551
  #### JSON/B Build Object
489
552
  [Postgres 'json(b)_build_object' function](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-CREATION-TABLE)
490
553
 
491
- The implementation of the`.json_build_object/2` and `.jsonb_build_object/2` methods are designed to be used with sub-queries.
554
+ The implementation of the`.json_build_object/2` and `.jsonb_build_object/2` methods are designed to be used with sub-queries.
492
555
  As a means for taking complex query logic and transform them into a single or multiple json responses.
493
556
 
494
557
  **Arguments:**
@@ -506,21 +569,21 @@ See the included example on [Row To JSON](#row-to-json) to see it in action.
506
569
 
507
570
  The implementation of the`.json_build_literal/1` and `.jsonb_build_literal/1` is designed for creating static json objects
508
571
  that don't require subquery interfacing.
509
-
572
+
510
573
  **Arguments:**
511
574
  - Requires an Array or Hash set of values
512
575
 
513
576
  **Options:**
514
577
  - `as`: [Symbol or String] (defaults to `"results"`): What the column will be aliased to
515
-
578
+
516
579
  ```ruby
517
580
  User.json_build_literal(number: 1, last_name: "json", pi: 3.14).take.results
518
581
  #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
519
-
582
+
520
583
  # Or as array elements
521
584
  User.json_build_literal(:number, 1, :last_name, "json", :pi, 3.14).take.results
522
- #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
523
-
585
+ #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
586
+
524
587
  ```
525
588
 
526
589
  Query Output
@@ -539,10 +602,10 @@ If any or all of your union queries include a CTE, read the [Subquery CTE Gotcha
539
602
 
540
603
 
541
604
  #### Known issue
542
- There's an issue with providing a single union clause and chaining it with a different union clause.
605
+ There's an issue with providing a single union clause and chaining it with a different union clause.
543
606
  This is due to requirements of grouping SQL statements. The issue is being working on, but with no ETA.
544
607
 
545
- This issue only applies to the first initial set of unions and is recommended that you union two relations right off the bat.
608
+ This issue only applies to the first initial set of unions and is recommended that you union two relations right off the bat.
546
609
  Afterwords you can union/chain single relations.
547
610
 
548
611
  Example
@@ -617,13 +680,13 @@ user_1 = Person.where(id: 1)
617
680
  user_2 = Person.where(id: 2)
618
681
  users = Person.where(id: 1..3)
619
682
 
620
- Person.union_all(user_1, user_2, users)
683
+ Person.union_all(user_1, user_2, users)
621
684
  #=> [#<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 3,..>]
622
685
 
623
686
  # You can also chain union's
624
687
  Person.union_all(user_1).union_all(user_2).union_all(users)
625
688
  # Or
626
- Person.union.all(user1, user_2).union.all(users)
689
+ Person.union.all(user1, user_2).union.all(users)
627
690
  ```
628
691
 
629
692
  Query Output
@@ -649,12 +712,12 @@ SELECT "people".*
649
712
 
650
713
  ```ruby
651
714
  users = Person.where(id: 1..5)
652
- expect_these_users = Person.where(id: 2..4)
715
+ except_these_users = Person.where(id: 2..4)
653
716
 
654
- Person.union_except(users, expect_these_users) #=> [#<Person id: 1, ..>, #<Person id: 5,..>]
717
+ Person.union_except(users, except_these_users) #=> [#<Person id: 1, ..>, #<Person id: 5,..>]
655
718
 
656
719
  # You can also chain union's
657
- Person.union.except(users, expect_these_users).union(Person.where(id: 20))
720
+ Person.union.except(users, except_these_users).union(Person.where(id: 20))
658
721
  ```
659
722
 
660
723
  Query Output
@@ -689,7 +752,7 @@ Person.union_intersect(likes_100, likes_less_than_150) #=> [randy]
689
752
  # You can also chain union's
690
753
  Person.union_intersect(likes_100).union_intersect(likes_less_than_150) #=> [randy]
691
754
  # Or
692
- Person.union.intersect(likes_100, likes_less_than_150) #=> [randy]
755
+ Person.union.intersect(likes_100, likes_less_than_150) #=> [randy]
693
756
 
694
757
  ```
695
758
 
@@ -794,8 +857,8 @@ SELECT "people".*
794
857
  #### Window Functions
795
858
  [Postgres Window Functions](https://www.postgresql.org/docs/current/tutorial-window.html)
796
859
 
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.
860
+ Let's address the elephant in the room. Arel has had, for a long time now, window function capabilities;
861
+ However they've never seen the lime light in ActiveRecord's query logic.
799
862
  The following brings the dormant Arel methods up to the ActiveRecord Querying level.
800
863
 
801
864
  #### Define Window
@@ -803,9 +866,9 @@ The following brings the dormant Arel methods up to the ActiveRecord Querying le
803
866
  To set up a window function, we first must establish the window and we do this by using the `.define_window/1` method.
804
867
  This method also requires you to call chain `.partition_by/2`
805
868
 
806
- `.define_window/1` - Establishes the name of the window you'll reference later on in [.select_window](#select-window)
869
+ `.define_window/1` - Establishes the name of the window you'll reference later on in [.select_window](#select-window)
807
870
  - Aliased name of window
808
-
871
+
809
872
  `.partition_by/2` - Establishes the windows operations a [pre-defined window function](https://www.postgresql.org/docs/current/functions-window.html) will leverage.
810
873
  - column name being partitioned against
811
874
  - (**optional**) `order_by`: Processes how the window should be ordered
@@ -851,17 +914,17 @@ User
851
914
  # { id: 2, name: "Randy", row_id: 1, first_value_name: "Randy" }
852
915
  # { id: 3, name: "Bob", row_id: 1, first_value_name: "Bob" }
853
916
  # ]
854
- #
917
+ #
855
918
 
856
919
  ```
857
920
 
858
921
  Query Output
859
922
  ```sql
860
- SELECT "users"."id",
861
- "users"."name",
862
- (ROW_NUMBER() OVER number_window) AS "row_id",
923
+ SELECT "users"."id",
924
+ "users"."name",
925
+ (ROW_NUMBER() OVER number_window) AS "row_id",
863
926
  (FIRST_VALUE(name) OVER number_window) AS "first_value_name"
864
- FROM "users"
927
+ FROM "users"
865
928
  WINDOW number_window AS (PARTITION BY number ORDER BY id DESC)
866
929
  ```
867
930
 
@@ -5,7 +5,7 @@ require "arel/nodes/function"
5
5
 
6
6
  module Arel
7
7
  module Nodes
8
- if Gem::Requirement.new("< 6.1").satisfied_by?(ActiveRecord.gem_version)
8
+ unless ActiveRecordExtended::AR_VERSION_GTE_6_1
9
9
  ["Contains", "Overlaps"].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
10
10
  end
11
11
 
@@ -5,7 +5,7 @@ require "arel/nodes/sql_literal"
5
5
  # CTE alias fix for Rails 6.1
6
6
  module Arel
7
7
  module Nodes
8
- module SqlLiteralDecorator
8
+ module SqlLiteralPatch
9
9
  def name
10
10
  self
11
11
  end
@@ -13,4 +13,4 @@ module Arel
13
13
  end
14
14
  end
15
15
 
16
- Arel::Nodes::SqlLiteral.prepend(Arel::Nodes::SqlLiteralDecorator)
16
+ Arel::Nodes::SqlLiteral.prepend(Arel::Nodes::SqlLiteralPatch)
@@ -9,21 +9,25 @@ module ActiveRecordExtended
9
9
 
10
10
  # rubocop:disable Naming/MethodName
11
11
 
12
- def visit_Arel_Nodes_Overlaps(object, collector)
13
- infix_value object, collector, " && "
12
+ unless ActiveRecordExtended::AR_VERSION_GTE_6_1
13
+ def visit_Arel_Nodes_Overlaps(object, collector)
14
+ infix_value object, collector, " && "
15
+ end
14
16
  end
15
17
 
16
- def visit_Arel_Nodes_Contains(object, collector)
17
- left_column = object.left.relation.name.classify.constantize.columns.detect do |col|
18
- matchable_column?(col, object)
19
- end
18
+ unless ActiveRecordExtended::AR_VERSION_GTE_6_1
19
+ def visit_Arel_Nodes_Contains(object, collector)
20
+ left_column = object.left.relation.name.classify.constantize.columns.detect do |col|
21
+ matchable_column?(col, object)
22
+ end
20
23
 
21
- if [:hstore, :jsonb].include?(left_column&.type)
22
- visit_Arel_Nodes_ContainsHStore(object, collector)
23
- elsif left_column.try(:array)
24
- visit_Arel_Nodes_ContainsArray(object, collector)
25
- else
26
- visit_Arel_Nodes_Inet_Contains(object, collector)
24
+ if [:hstore, :jsonb].include?(left_column&.type)
25
+ visit_Arel_Nodes_ContainsHStore(object, collector)
26
+ elsif left_column.try(:array)
27
+ visit_Arel_Nodes_ContainsArray(object, collector)
28
+ else
29
+ visit_Arel_Nodes_Inet_Contains(object, collector)
30
+ end
27
31
  end
28
32
  end
29
33
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record_extended/arel/nodes"
4
- require "active_record_extended/arel/sql_literal"
4
+ require "active_record_extended/arel/sql_literal_patch"
5
5
  require "active_record_extended/arel/aggregate_function_name"
6
6
  require "active_record_extended/arel/predications"
7
7
  require "active_record_extended/arel/visitors/postgresql_decorator"
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/relation/predicate_builder"
4
+ require "active_record/relation/predicate_builder/array_handler"
5
+
6
+ module ActiveRecordExtended
7
+ module Patch
8
+ module ArrayHandlerPatch
9
+ def call(attribute, value)
10
+ cache = ActiveRecord::Base.connection.schema_cache
11
+ if cache.data_source_exists?(attribute.relation.name)
12
+ column = cache.columns(attribute.relation.name).detect { |col| col.name.to_s == attribute.name.to_s }
13
+ return attribute.eq(value) if column.try(:array)
14
+ end
15
+
16
+ super(attribute, value)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveRecord::PredicateBuilder::ArrayHandler.prepend(ActiveRecordExtended::Patch::ArrayHandlerPatch)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module Patch
5
+ module RelationPatch
6
+ module QueryDelegation
7
+ AR_EX_QUERY_METHODS = (
8
+ [
9
+ :with, :define_window, :select_window, :foster_select,
10
+ :either_join, :either_joins, :either_order, :either_orders
11
+ ] +
12
+ ActiveRecordExtended::QueryMethods::Unionize::UNIONIZE_METHODS +
13
+ ActiveRecordExtended::QueryMethods::Json::JSON_QUERY_METHODS
14
+ ).freeze
15
+
16
+ delegate(*AR_EX_QUERY_METHODS, to: :all)
17
+ end
18
+
19
+ module Merger
20
+ def merge
21
+ merge_ctes!
22
+ merge_union!
23
+ merge_windows!
24
+ super
25
+ end
26
+
27
+ def merge_union!
28
+ return if other.unionize_storage.empty?
29
+
30
+ relation.union_values += other.union_values
31
+ relation.union_operations += other.union_operations
32
+ relation.union_ordering_values += other.union_ordering_values
33
+ end
34
+
35
+ def merge_windows!
36
+ return unless other.window_values?
37
+
38
+ relation.window_values |= other.window_values
39
+ end
40
+
41
+ def merge_ctes!
42
+ return unless other.with_values?
43
+
44
+ return relation.with!.recursive(other.cte) if other.recursive_value? && !relation.recursive_value?
45
+
46
+ merge_by_values(relation, other)
47
+ end
48
+
49
+ private
50
+
51
+ # Merge other's with_values one at a time to ensure materialized keys are set properly
52
+ def merge_by_values(relation, other)
53
+ other.cte.with_values.each do |name, expression|
54
+ relation = if other.cte.materialized_key?(name)
55
+ relation.with!.materialized(name => expression)
56
+ elsif other.cte.not_materialized_key?(name)
57
+ relation.with!.not_materialized(name => expression)
58
+ else
59
+ relation.with!(name => expression)
60
+ end
61
+ end
62
+
63
+ relation
64
+ end
65
+ end
66
+
67
+ module ArelBuildPatch
68
+ def build_arel(*aliases)
69
+ super.tap do |arel|
70
+ build_windows(arel) if window_values?
71
+ build_unions(arel) if union_values?
72
+ build_with(arel) if with_values?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ ActiveRecord::Relation.prepend(ActiveRecordExtended::Patch::RelationPatch::ArelBuildPatch)
81
+ ActiveRecord::Relation::Merger.prepend(ActiveRecordExtended::Patch::RelationPatch::Merger)
82
+ ActiveRecord::Base.extend(ActiveRecordExtended::Patch::RelationPatch::QueryDelegation)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordExtended
4
+ module Patch
5
+ module WhereClausePatch
6
+ def modified_predicates(&block)
7
+ ActiveRecord::Relation::WhereClause.new(predicates.map(&block))
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ ActiveRecord::Relation::WhereClause.prepend(ActiveRecordExtended::Patch::WhereClausePatch)