active_record_extended 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b59ef27069d870b734f0494db168936d5536493fd1346469f7a85d9b43bc7655
4
- data.tar.gz: f0e25e53250ec9d562bb75ec06e7d02deda48a9996b8a03e4fb5afac2d113abc
3
+ metadata.gz: 2ecf1213e83b4ef3e177957d668d253541a1de09a784fbc2007a782661acc647
4
+ data.tar.gz: 4fbf39a2fdb8695a7ed787a672c4f0b0988e7b854757955b3883fc546fdcb3d9
5
5
  SHA512:
6
- metadata.gz: ffad0b652cbaac828d25ed1dbbd6e5e5479f6905bf776785090f9b76fe33fefb750fcbce7c329bbcaf077f1f5401b9817e830d89efab130feca3500bbc97c1f8
7
- data.tar.gz: b0f77a0248242cc50e7f87ebd813e90c4e57bf1cfd28e35190ee93fa4ce2ae50878dd6e7f153dc017a692f13d147a9a9f8378ba9583d7705c2b71566fb20e29c
6
+ metadata.gz: 3afca0a92e015572b4e318a3457f9910f43cefad6ab3286a5847bf76c4e659eb3bbdcd0ddce68bd1b1fe876866bfbcf959686e51fb55598bb38c936716715161
7
+ data.tar.gz: 70c7e5c95251002333b2453688f1d2f3e5fcab8c6866837969ed2570f3432771efbe56cc33a7df11ef4f47b29f18aff226fb5695ff3453a8d38f6e48b1fecd62
data/README.md CHANGED
@@ -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.
@@ -54,10 +54,10 @@ Active Record Extended is essentially providing users with the other half of Pos
54
54
  This package is designed align and work with any officially supported Ruby and Rails versions.
55
55
  - Minimum Ruby Version: 2.5.x **(EOL warning!)**
56
56
  - Minimum Rails Version: 5.2.x **(EOL warning!)**
57
- - Minimum Postgres Version: 10.x **(EOL warning!)**
57
+ - Minimum Postgres Version: 11.x **(EOL warning!)**
58
58
  - Latest Ruby supported: 3.1.x
59
59
  - Latest Rails supported: 7.0.x
60
- - Postgres: 10-current(14) (probably works with most older versions to a certain point)
60
+ - Postgres: 11-current(14) (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,8 +460,8 @@ 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`.select_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).
@@ -437,44 +485,44 @@ While quite the mouthful of an explanation. The implementation of combining unre
437
485
  physical_cat = Category.create!(name: "Physical")
438
486
  products = 3.times.map { Product.create! }
439
487
  products.each { |product| 100.times { Variant.create!(product: product, category: physical_cat) } }
440
-
488
+
441
489
  # Since we plan to nest this query, you have access top level information. (I.E categories table)
442
490
  item_query = Variant.select(:name, :id, :category_id, :product_id).where("categories.id = variants.category_id")
443
-
491
+
444
492
  # You can provide addition scopes that will be applied to the nested query (but will not effect the actual inner query)
445
- # This is ideal if you are dealing with but not limited to, CTE's being applied multiple times and require additional constraints
446
- 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 =
447
495
  Product.select(:id)
448
496
  .joins(:items)
449
497
  .select_row_to_json(item_query, key: :outer_items, as: :items, cast_with: :array) do |item_scope|
450
498
  item_scope.where("outer_items.product_id = products.id")
451
- # Results to:
452
- # SELECT ..., ARRAY(SELECT ROW_TO_JSON("outer_items")
499
+ # Results to:
500
+ # SELECT ..., ARRAY(SELECT ROW_TO_JSON("outer_items")
453
501
  # FROM ([:item_query:]) outer_items
454
502
  # WHERE outer_items.product_id = products.id
455
503
  # ) AS items
456
504
  end
457
-
458
- # 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
459
507
  category_query = Category.select(:name, :id).select_row_to_json(product_query, as: :products, cast_with: :array)
460
508
  Category.json_build_object(:physical_category, category_query.where(id: physical_cat.id)).results
461
509
  #=> {
462
510
  # "physical_category" => {
463
511
  # "name" => "Physical",
464
- # "id" => 1,
512
+ # "id" => 1,
465
513
  # "products" => [
466
514
  # {
467
515
  # "id" => 2,
468
- # "items" => [{"name" => "Bojangels", "id" => 3, "category_id" => 1, "product_id" => 2}, ...]
516
+ # "items" => [{"name" => "Bojangels", "id" => 3, "category_id" => 1, "product_id" => 2}, ...]
469
517
  # },
470
- # ...
471
- # ]
472
- # }
473
- # }
518
+ # ...
519
+ # ]
520
+ # }
521
+ # }
474
522
  #
475
523
  ```
476
524
 
477
- Query Output
525
+ Query Output
478
526
  ```sql
479
527
  SELECT (JSON_BUILD_OBJECT('physical_category', "physical_category")) AS "results"
480
528
  FROM (
@@ -503,7 +551,7 @@ FROM (
503
551
  #### JSON/B Build Object
504
552
  [Postgres 'json(b)_build_object' function](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSON-CREATION-TABLE)
505
553
 
506
- 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.
507
555
  As a means for taking complex query logic and transform them into a single or multiple json responses.
508
556
 
509
557
  **Arguments:**
@@ -521,21 +569,21 @@ See the included example on [Row To JSON](#row-to-json) to see it in action.
521
569
 
522
570
  The implementation of the`.json_build_literal/1` and `.jsonb_build_literal/1` is designed for creating static json objects
523
571
  that don't require subquery interfacing.
524
-
572
+
525
573
  **Arguments:**
526
574
  - Requires an Array or Hash set of values
527
575
 
528
576
  **Options:**
529
577
  - `as`: [Symbol or String] (defaults to `"results"`): What the column will be aliased to
530
-
578
+
531
579
  ```ruby
532
580
  User.json_build_literal(number: 1, last_name: "json", pi: 3.14).take.results
533
581
  #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
534
-
582
+
535
583
  # Or as array elements
536
584
  User.json_build_literal(:number, 1, :last_name, "json", :pi, 3.14).take.results
537
- #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
538
-
585
+ #=> { "number" => 1, "last_name" => "json", "pi" => 3.14 }
586
+
539
587
  ```
540
588
 
541
589
  Query Output
@@ -554,10 +602,10 @@ If any or all of your union queries include a CTE, read the [Subquery CTE Gotcha
554
602
 
555
603
 
556
604
  #### Known issue
557
- 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.
558
606
  This is due to requirements of grouping SQL statements. The issue is being working on, but with no ETA.
559
607
 
560
- 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.
561
609
  Afterwords you can union/chain single relations.
562
610
 
563
611
  Example
@@ -632,13 +680,13 @@ user_1 = Person.where(id: 1)
632
680
  user_2 = Person.where(id: 2)
633
681
  users = Person.where(id: 1..3)
634
682
 
635
- Person.union_all(user_1, user_2, users)
683
+ Person.union_all(user_1, user_2, users)
636
684
  #=> [#<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 1, ..>, #<Person id: 2,..>, #<Person id: 3,..>]
637
685
 
638
686
  # You can also chain union's
639
687
  Person.union_all(user_1).union_all(user_2).union_all(users)
640
688
  # Or
641
- Person.union.all(user1, user_2).union.all(users)
689
+ Person.union.all(user1, user_2).union.all(users)
642
690
  ```
643
691
 
644
692
  Query Output
@@ -704,7 +752,7 @@ Person.union_intersect(likes_100, likes_less_than_150) #=> [randy]
704
752
  # You can also chain union's
705
753
  Person.union_intersect(likes_100).union_intersect(likes_less_than_150) #=> [randy]
706
754
  # Or
707
- Person.union.intersect(likes_100, likes_less_than_150) #=> [randy]
755
+ Person.union.intersect(likes_100, likes_less_than_150) #=> [randy]
708
756
 
709
757
  ```
710
758
 
@@ -809,8 +857,8 @@ SELECT "people".*
809
857
  #### Window Functions
810
858
  [Postgres Window Functions](https://www.postgresql.org/docs/current/tutorial-window.html)
811
859
 
812
- Let's address the elephant in the room. Arel has had, for a long time now, window function capabilities;
813
- 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.
814
862
  The following brings the dormant Arel methods up to the ActiveRecord Querying level.
815
863
 
816
864
  #### Define Window
@@ -818,9 +866,9 @@ The following brings the dormant Arel methods up to the ActiveRecord Querying le
818
866
  To set up a window function, we first must establish the window and we do this by using the `.define_window/1` method.
819
867
  This method also requires you to call chain `.partition_by/2`
820
868
 
821
- `.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)
822
870
  - Aliased name of window
823
-
871
+
824
872
  `.partition_by/2` - Establishes the windows operations a [pre-defined window function](https://www.postgresql.org/docs/current/functions-window.html) will leverage.
825
873
  - column name being partitioned against
826
874
  - (**optional**) `order_by`: Processes how the window should be ordered
@@ -866,17 +914,17 @@ User
866
914
  # { id: 2, name: "Randy", row_id: 1, first_value_name: "Randy" }
867
915
  # { id: 3, name: "Bob", row_id: 1, first_value_name: "Bob" }
868
916
  # ]
869
- #
917
+ #
870
918
 
871
919
  ```
872
920
 
873
921
  Query Output
874
922
  ```sql
875
- SELECT "users"."id",
876
- "users"."name",
877
- (ROW_NUMBER() OVER number_window) AS "row_id",
923
+ SELECT "users"."id",
924
+ "users"."name",
925
+ (ROW_NUMBER() OVER number_window) AS "row_id",
878
926
  (FIRST_VALUE(name) OVER number_window) AS "first_value_name"
879
- FROM "users"
927
+ FROM "users"
880
928
  WINDOW number_window AS (PARTITION BY number ORDER BY id DESC)
881
929
  ```
882
930
 
File without changes
File without changes
File without changes
File without changes
@@ -41,11 +41,26 @@ module ActiveRecordExtended
41
41
  def merge_ctes!
42
42
  return unless other.with_values?
43
43
 
44
- if other.recursive_value? && !relation.recursive_value?
45
- relation.with!.recursive(other.cte)
46
- else
47
- relation.with!(other.cte)
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
48
61
  end
62
+
63
+ relation
49
64
  end
50
65
  end
51
66
 
File without changes
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ar_outer_joins"
4
-
5
3
  module ActiveRecordExtended
6
4
  module QueryMethods
7
5
  module Either
@@ -12,14 +10,14 @@ module ActiveRecordExtended
12
10
  associations = [initial_association, fallback_association]
13
11
  association_options = xor_field_options_for_associations(associations)
14
12
  condition__query = xor_field_sql(association_options) + "= #{table_name}.#{primary_key}"
15
- outer_joins(associations).where(Arel.sql(condition__query))
13
+ left_outer_joins(associations).where(Arel.sql(condition__query))
16
14
  end
17
15
  alias either_joins either_join
18
16
 
19
17
  def either_order(direction, **associations_and_columns)
20
18
  reflected_columns = map_columns_to_tables(associations_and_columns)
21
19
  conditional_query = xor_field_sql(reflected_columns) + sort_order_sql(direction)
22
- outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
20
+ left_outer_joins(associations_and_columns.keys).order(Arel.sql(conditional_query))
23
21
  end
24
22
  alias either_orders either_order
25
23
 
File without changes
File without changes
File without changes
File without changes
@@ -9,7 +9,7 @@ module ActiveRecordExtended
9
9
  extend Forwardable
10
10
 
11
11
  def_delegators :@with_values, :empty?, :blank?, :present?
12
- attr_reader :with_values, :with_keys
12
+ attr_reader :with_values, :with_keys, :materialized_keys, :not_materialized_keys
13
13
 
14
14
  # @param [ActiveRecord::Relation] scope
15
15
  def initialize(scope)
@@ -33,6 +33,16 @@ module ActiveRecordExtended
33
33
  pipe_cte_with!(value)
34
34
  end
35
35
 
36
+ # @return [Boolean]
37
+ def materialized_key?(key)
38
+ materialized_keys.include?(key.to_sym)
39
+ end
40
+
41
+ # @return [Boolean]
42
+ def not_materialized_key?(key)
43
+ not_materialized_keys.include?(key.to_sym)
44
+ end
45
+
36
46
  # @param [Hash, WithCTE] value
37
47
  def pipe_cte_with!(value)
38
48
  return if value.nil? || value.empty?
@@ -44,6 +54,10 @@ module ActiveRecordExtended
44
54
  # Ensure we follow FIFO pattern.
45
55
  # If the parent has similar CTE alias keys, we want to favor the parent's expressions over its children's.
46
56
  if expression.is_a?(ActiveRecord::Relation) && expression.with_values?
57
+ # Add child's materialized keys to the parent
58
+ @materialized_keys += expression.cte.materialized_keys
59
+ @not_materialized_keys += expression.cte.not_materialized_keys
60
+
47
61
  pipe_cte_with!(expression.cte)
48
62
  expression.cte.reset!
49
63
  end
@@ -58,6 +72,8 @@ module ActiveRecordExtended
58
72
  def reset!
59
73
  @with_keys = []
60
74
  @with_values = {}
75
+ @materialized_keys = Set.new
76
+ @not_materialized_keys = Set.new
61
77
  end
62
78
  end
63
79
 
@@ -75,6 +91,32 @@ module ActiveRecordExtended
75
91
  scope.cte.pipe_cte_with!(args)
76
92
  end
77
93
  end
94
+
95
+ # @param [Hash, WithCTE] args
96
+ def materialized(args)
97
+ @scope.tap do |scope|
98
+ args.each_pair do |name, _expression|
99
+ sym_name = name.to_sym
100
+ raise ArgumentError.new("CTE already set as not_materialized") if scope.cte.not_materialized_key?(sym_name)
101
+
102
+ scope.cte.materialized_keys << sym_name
103
+ end
104
+ scope.cte.pipe_cte_with!(args)
105
+ end
106
+ end
107
+
108
+ # @param [Hash, WithCTE] args
109
+ def not_materialized(args)
110
+ @scope.tap do |scope|
111
+ args.each_pair do |name, _expression|
112
+ sym_name = name.to_sym
113
+ raise ArgumentError.new("CTE already set as materialized") if scope.cte.materialized_key?(sym_name)
114
+
115
+ scope.cte.not_materialized_keys << sym_name
116
+ end
117
+ scope.cte.pipe_cte_with!(args)
118
+ end
119
+ end
78
120
  end
79
121
 
80
122
  # @return [WithCTE]
@@ -134,6 +176,9 @@ module ActiveRecordExtended
134
176
  cte_statements = cte.map do |name, expression|
135
177
  grouped_expression = cte.generate_grouping(expression)
136
178
  cte_name = cte.to_arel_sql(cte.double_quote(name.to_s))
179
+
180
+ grouped_expression = add_materialized_modifier(grouped_expression, cte, name)
181
+
137
182
  Arel::Nodes::As.new(cte_name, grouped_expression)
138
183
  end
139
184
 
@@ -143,6 +188,18 @@ module ActiveRecordExtended
143
188
  arel.with(cte_statements)
144
189
  end
145
190
  end
191
+
192
+ private
193
+
194
+ def add_materialized_modifier(expression, cte, name)
195
+ if cte.materialized_key?(name)
196
+ Arel::Nodes::SqlLiteral.new("MATERIALIZED #{expression.to_sql}")
197
+ elsif cte.not_materialized_key?(name)
198
+ Arel::Nodes::SqlLiteral.new("NOT MATERIALIZED #{expression.to_sql}")
199
+ else
200
+ expression
201
+ end
202
+ end
146
203
  end
147
204
  end
148
205
  end
File without changes
File without changes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordExtended
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.0"
5
5
  end
File without changes
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_extended
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Protacio-Karaszi
8
8
  - Dan McClain
9
9
  - Olivier El Mekki
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-08-14 00:00:00.000000000 Z
13
+ date: 2022-12-01 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -32,20 +32,6 @@ dependencies:
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
34
  version: 7.1.0
35
- - !ruby/object:Gem::Dependency
36
- name: ar_outer_joins
37
- requirement: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - "~>"
40
- - !ruby/object:Gem::Version
41
- version: '0.2'
42
- type: :runtime
43
- prerelease: false
44
- version_requirements: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: '0.2'
49
35
  - !ruby/object:Gem::Dependency
50
36
  name: pg
51
37
  requirement: !ruby/object:Gem::Requirement
@@ -173,7 +159,7 @@ licenses:
173
159
  - MIT
174
160
  metadata:
175
161
  rubygems_mfa_required: 'true'
176
- post_install_message:
162
+ post_install_message:
177
163
  rdoc_options: []
178
164
  require_paths:
179
165
  - lib
@@ -188,8 +174,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
174
  - !ruby/object:Gem::Version
189
175
  version: '0'
190
176
  requirements: []
191
- rubygems_version: 3.3.7
192
- signing_key:
177
+ rubygems_version: 3.3.19
178
+ signing_key:
193
179
  specification_version: 4
194
180
  summary: Adds extended functionality to Activerecord Postgres implementation
195
181
  test_files: []