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 +4 -4
- data/README.md +117 -69
- data/lib/active_record_extended/arel/aggregate_function_name.rb +0 -0
- data/lib/active_record_extended/arel/nodes.rb +0 -0
- data/lib/active_record_extended/arel/predications.rb +0 -0
- data/lib/active_record_extended/arel/sql_literal_patch.rb +0 -0
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +0 -0
- data/lib/active_record_extended/arel.rb +0 -0
- data/lib/active_record_extended/patch/array_handler_patch.rb +0 -0
- data/lib/active_record_extended/patch/relation_patch.rb +19 -4
- data/lib/active_record_extended/patch/where_clause_patch.rb +0 -0
- data/lib/active_record_extended/query_methods/any_of.rb +0 -0
- data/lib/active_record_extended/query_methods/either.rb +2 -4
- data/lib/active_record_extended/query_methods/foster_select.rb +0 -0
- data/lib/active_record_extended/query_methods/inet.rb +0 -0
- data/lib/active_record_extended/query_methods/json.rb +0 -0
- data/lib/active_record_extended/query_methods/unionize.rb +0 -0
- data/lib/active_record_extended/query_methods/where_chain.rb +0 -0
- data/lib/active_record_extended/query_methods/window.rb +0 -0
- data/lib/active_record_extended/query_methods/with_cte.rb +58 -1
- data/lib/active_record_extended/utilities/order_by.rb +0 -0
- data/lib/active_record_extended/utilities/support.rb +0 -0
- data/lib/active_record_extended/version.rb +1 -1
- data/lib/active_record_extended.rb +0 -0
- metadata +6 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ecf1213e83b4ef3e177957d668d253541a1de09a784fbc2007a782661acc647
|
4
|
+
data.tar.gz: 4fbf39a2fdb8695a7ed787a672c4f0b0988e7b854757955b3883fc546fdcb3d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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:
|
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
|
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
|
-
|
46
|
-
|
47
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
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.
|
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-
|
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.
|
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: []
|