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.
- checksums.yaml +4 -4
- data/README.md +140 -77
- data/lib/active_record_extended/arel/nodes.rb +1 -1
- data/lib/active_record_extended/arel/{sql_literal.rb → sql_literal_patch.rb} +2 -2
- data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +16 -12
- data/lib/active_record_extended/arel.rb +1 -1
- data/lib/active_record_extended/patch/array_handler_patch.rb +22 -0
- data/lib/active_record_extended/patch/relation_patch.rb +82 -0
- data/lib/active_record_extended/patch/where_clause_patch.rb +13 -0
- data/lib/active_record_extended/query_methods/any_of.rb +7 -31
- data/lib/active_record_extended/query_methods/either.rb +5 -7
- data/lib/active_record_extended/query_methods/{select.rb → foster_select.rb} +4 -4
- data/lib/active_record_extended/query_methods/json.rb +2 -2
- data/lib/active_record_extended/query_methods/unionize.rb +3 -3
- data/lib/active_record_extended/query_methods/where_chain.rb +96 -90
- data/lib/active_record_extended/query_methods/window.rb +3 -3
- data/lib/active_record_extended/query_methods/with_cte.rb +70 -9
- data/lib/active_record_extended/utilities/order_by.rb +1 -1
- data/lib/active_record_extended/utilities/support.rb +1 -1
- data/lib/active_record_extended/version.rb +1 -1
- data/lib/active_record_extended.rb +55 -4
- metadata +34 -83
- data/lib/active_record_extended/active_record/relation_patch.rb +0 -50
- data/lib/active_record_extended/active_record.rb +0 -25
- data/lib/active_record_extended/patch/5_1/where_clause.rb +0 -11
- data/lib/active_record_extended/patch/5_2/where_clause.rb +0 -11
- data/lib/active_record_extended/predicate_builder/array_handler_decorator.rb +0 -20
- data/spec/active_record_extended_spec.rb +0 -7
- data/spec/query_methods/any_of_spec.rb +0 -131
- data/spec/query_methods/array_query_spec.rb +0 -64
- data/spec/query_methods/either_spec.rb +0 -70
- data/spec/query_methods/hash_query_spec.rb +0 -45
- data/spec/query_methods/inet_query_spec.rb +0 -112
- data/spec/query_methods/json_spec.rb +0 -157
- data/spec/query_methods/select_spec.rb +0 -115
- data/spec/query_methods/unionize_spec.rb +0 -165
- data/spec/query_methods/window_spec.rb +0 -51
- data/spec/query_methods/with_cte_spec.rb +0 -50
- data/spec/spec_helper.rb +0 -28
- data/spec/sql_inspections/any_of_sql_spec.rb +0 -41
- data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +0 -41
- data/spec/sql_inspections/arel/array_spec.rb +0 -63
- data/spec/sql_inspections/arel/inet_spec.rb +0 -66
- data/spec/sql_inspections/contains_sql_queries_spec.rb +0 -47
- data/spec/sql_inspections/either_sql_spec.rb +0 -71
- data/spec/sql_inspections/json_sql_spec.rb +0 -82
- data/spec/sql_inspections/unionize_sql_spec.rb +0 -124
- data/spec/sql_inspections/window_sql_spec.rb +0 -98
- data/spec/sql_inspections/with_cte_sql_spec.rb +0 -95
- data/spec/support/database_cleaner.rb +0 -15
- data/spec/support/models.rb +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2611c1b573495ab42f5a15ff8d85bdc8ba95bbe8d077ae037a5eae8e8f29f2c6
|
4
|
+
data.tar.gz: 4d6c36a2e09b490969f8f152bda00a45180bbcd0a728fb7344e058faa96da10c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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://
|
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.
|
56
|
-
- Minimum Rails Version: 5.
|
57
|
-
- Minimum Postgres Version:
|
58
|
-
- Latest Ruby supported: 2.
|
59
|
-
- Latest Rails supported:
|
60
|
-
- Postgres:
|
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`.
|
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
|
-
|
715
|
+
except_these_users = Person.where(id: 2..4)
|
653
716
|
|
654
|
-
Person.union_except(users,
|
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,
|
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
|
-
|
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
|
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::
|
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
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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/
|
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)
|