active_record_extended 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -15
  3. data/lib/active_record_extended.rb +2 -1
  4. data/lib/active_record_extended/active_record.rb +2 -9
  5. data/lib/active_record_extended/active_record/relation_patch.rb +21 -4
  6. data/lib/active_record_extended/arel.rb +2 -0
  7. data/lib/active_record_extended/arel/aggregate_function_name.rb +40 -0
  8. data/lib/active_record_extended/arel/nodes.rb +32 -41
  9. data/lib/active_record_extended/arel/predications.rb +4 -1
  10. data/lib/active_record_extended/arel/sql_literal.rb +16 -0
  11. data/lib/active_record_extended/arel/visitors/postgresql_decorator.rb +40 -1
  12. data/lib/active_record_extended/query_methods/any_of.rb +10 -8
  13. data/lib/active_record_extended/query_methods/either.rb +1 -1
  14. data/lib/active_record_extended/query_methods/inet.rb +7 -3
  15. data/lib/active_record_extended/query_methods/json.rb +156 -50
  16. data/lib/active_record_extended/query_methods/select.rb +118 -0
  17. data/lib/active_record_extended/query_methods/unionize.rb +14 -43
  18. data/lib/active_record_extended/query_methods/where_chain.rb +14 -6
  19. data/lib/active_record_extended/query_methods/window.rb +93 -0
  20. data/lib/active_record_extended/query_methods/with_cte.rb +102 -35
  21. data/lib/active_record_extended/utilities/order_by.rb +77 -0
  22. data/lib/active_record_extended/utilities/support.rb +178 -0
  23. data/lib/active_record_extended/version.rb +1 -1
  24. data/spec/query_methods/any_of_spec.rb +40 -40
  25. data/spec/query_methods/array_query_spec.rb +14 -14
  26. data/spec/query_methods/either_spec.rb +14 -14
  27. data/spec/query_methods/hash_query_spec.rb +11 -11
  28. data/spec/query_methods/inet_query_spec.rb +33 -31
  29. data/spec/query_methods/json_spec.rb +42 -27
  30. data/spec/query_methods/select_spec.rb +115 -0
  31. data/spec/query_methods/unionize_spec.rb +56 -56
  32. data/spec/query_methods/window_spec.rb +51 -0
  33. data/spec/query_methods/with_cte_spec.rb +22 -12
  34. data/spec/spec_helper.rb +1 -1
  35. data/spec/sql_inspections/any_of_sql_spec.rb +12 -12
  36. data/spec/sql_inspections/arel/aggregate_function_name_spec.rb +41 -0
  37. data/spec/sql_inspections/arel/array_spec.rb +7 -7
  38. data/spec/sql_inspections/arel/inet_spec.rb +7 -7
  39. data/spec/sql_inspections/contains_sql_queries_spec.rb +14 -14
  40. data/spec/sql_inspections/either_sql_spec.rb +11 -11
  41. data/spec/sql_inspections/json_sql_spec.rb +44 -8
  42. data/spec/sql_inspections/unionize_sql_spec.rb +27 -27
  43. data/spec/sql_inspections/window_sql_spec.rb +98 -0
  44. data/spec/sql_inspections/with_cte_sql_spec.rb +52 -23
  45. data/spec/support/models.rb +24 -4
  46. metadata +31 -20
  47. data/lib/active_record_extended/patch/5_0/predicate_builder_decorator.rb +0 -87
  48. data/lib/active_record_extended/utilities.rb +0 -141
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9600653726c77cad5b6e0a90c1f5a55213c95804a3b094c1595aa085127f2605
4
- data.tar.gz: 0a7204abdd56e9d92c6b304d42d79b7270feb966aed55cc122e2c2ad68d91dab
3
+ metadata.gz: 9934d5a90324b46dc3faa7b496724ac219a2582d264db89e64e2b9be574adab6
4
+ data.tar.gz: c1359988f9a6b83148f36548e842a25cab68a191e7a839caef7e783b1415b346
5
5
  SHA512:
6
- metadata.gz: '008286c5485c0e14d8fd113ac12c78f2079f572ad5d99bf4eee9c8c3fa16c58948c496aa79436718098a120d3481aa832a2d9509f89beb7ee7733e2d96754ae3'
7
- data.tar.gz: e239092f0511f1c75f2eca0a0aed42f193d14a6f4c490dc6549c87c19c231b671d5a3316be6dcaea0641d6c633f33f93ae8618a43f7b5314d1d97ad2abda7fd3
6
+ metadata.gz: e7385b414ef4c8d8b0c5b29eaeb70e77ba79f07828255deef5db79e99cf33f18de7bab696501711dee1f8e056ca3bdf628cdd498b341a91b5ad5cd2c766bd61c
7
+ data.tar.gz: 10e8d4f65c570dba757b2cf47aaaff7f8a5538974e9c8aadb3c267e3e3fccb9508b05a8722eb5d6680937cb15827f6fa4f88d6dd219d8240c58323385fe44eb2
data/README.md CHANGED
@@ -36,6 +36,9 @@
36
36
  - [Union As](#union-as)
37
37
  - [Union Order](#union-order)
38
38
  - [Union Reorder](#union-reorder)
39
+ - [Window Functions](#window-functions)
40
+ - [Define Window](#define-window)
41
+ - [Select Window](#select-window)
39
42
 
40
43
  ## Description and History
41
44
 
@@ -46,15 +49,27 @@ The only problem is that this has created a wild west of environments of sorts.
46
49
 
47
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.
48
51
 
49
-
50
52
  ## Compatibility
51
53
 
52
54
  This package is designed align and work with any officially supported Ruby and Rails versions.
53
- - Minimum Ruby Version: 2.3.x **(EOL warning!)**
54
- - Minimum Rails Version: 5.0.x **(EOL warning!)**
55
- - Latest Ruby supported: 2.6.x
56
- - Latest Rails supported: 6.0.x
57
- - Postgres: 9.6-current(11) (probably works with most older versions to a certain point)
55
+ - Minimum Ruby Version: 2.4.x **(EOL warning!)**
56
+ - Minimum Rails Version: 5.1.x **(EOL warning!)**
57
+ - Minimum Postgres Version: 9.6.x **(EOL warning!)**
58
+ - Latest Ruby supported: 2.7.x
59
+ - Latest Rails supported: 6.1.x
60
+ - Postgres: 9.6-current(13) (probably works with most older versions to a certain point)
61
+
62
+ ## Installation
63
+
64
+ Add this line to your application's Gemfile:
65
+
66
+ ```ruby
67
+ gem 'active_record_extended'
68
+ ```
69
+
70
+ And then execute:
71
+
72
+ $ bundle
58
73
 
59
74
  ## Usage
60
75
 
@@ -416,7 +431,7 @@ While quite the mouthful of an explanation. The implementation of combining unre
416
431
  product_query =
417
432
  Product.select(:id)
418
433
  .joins(:items)
419
- .select_row_to_json(item_query, key: :outer_items, as: :items, cast_as_array: true) do |item_scope|
434
+ .select_row_to_json(item_query, key: :outer_items, as: :items, cast_with: :array) do |item_scope|
420
435
  item_scope.where("outer_items.product_id = products.id")
421
436
  # Results to:
422
437
  # SELECT ..., ARRAY(SELECT ROW_TO_JSON("outer_items")
@@ -426,7 +441,7 @@ While quite the mouthful of an explanation. The implementation of combining unre
426
441
  end
427
442
 
428
443
  # Not defining a key will automatically generate a random key between a-z
429
- category_query = Category.select(:name, :id).select_row_to_json(product_query, as: :products, cast_as_array: true)
444
+ category_query = Category.select(:name, :id).select_row_to_json(product_query, as: :products, cast_with: :array)
430
445
  Category.json_build_object(:physical_category, category_query.where(id: physical_cat.id)).results
431
446
  #=> {
432
447
  # "physical_category" => {
@@ -776,22 +791,79 @@ SELECT "people".*
776
791
  ) ) ORDER BY personal_id DESC, id DESC) people
777
792
  ```
778
793
 
794
+ #### Window Functions
795
+ [Postgres Window Functions](https://www.postgresql.org/docs/current/tutorial-window.html)
779
796
 
780
- ## Installation
797
+ Let's address the elephant in the room. Arel has had, for a long time now, window function capabilities;
798
+ However they've never seen the lime light in ActiveRecord's query logic.
799
+ The following brings the dormant Arel methods up to the ActiveRecord Querying level.
781
800
 
782
- Add this line to your application's Gemfile:
801
+ #### Define Window
802
+
803
+ To set up a window function, we first must establish the window and we do this by using the `.define_window/1` method.
804
+ This method also requires you to call chain `.partition_by/2`
805
+
806
+ `.define_window/1` - Establishes the name of the window you'll reference later on in [.select_window](#select-window)
807
+ - Aliased name of window
808
+
809
+ `.partition_by/2` - Establishes the windows operations a [pre-defined window function](https://www.postgresql.org/docs/current/functions-window.html) will leverage.
810
+ - column name being partitioned against
811
+ - (**optional**) `order_by`: Processes how the window should be ordered
783
812
 
784
813
  ```ruby
785
- gem 'active_record_extended'
814
+ User
815
+ .define_window(:number_window).partition_by(:number, order_by: { id: :desc })
816
+ .define_window(:name_window).partition_by(:name, order_by: :id)
817
+ .define_window(:no_order_name).partition_by(:name)
786
818
  ```
787
819
 
788
- And then execute:
820
+ Query Output
821
+ ```sql
822
+ SELECT *
823
+ FROM users
824
+ WINDOW number_window AS (PARTITION BY number ORDER BY id DESC),
825
+ name_window AS (PARTITION BY name ORDER BY id),
826
+ no_order_name AS (PARTITION BY name)
827
+ ```
789
828
 
790
- $ bundle
829
+ #### Select Window
791
830
 
792
- Or install it yourself as:
831
+ Once you've define a window, the next step to to utilize it on one of the many provided postgres window functions.
793
832
 
794
- $ gem install active_record_extended
833
+ `.select_window/3`
834
+ - [window function name](https://www.postgresql.org/docs/current/functions-window.html)
835
+ - (**optional**) Window function arguments (treated as a splatted array)
836
+ - (**optional**) `as:` : Alias name of the final result
837
+ - `over:` : name of [defined window](#define-window)
838
+
839
+ ```ruby
840
+ User.create!(name: "Alice", number: 100) #=> id: 1
841
+ User.create!(name: "Randy", number: 100) #=> id: 2
842
+ User.create!(name: "Bob", number: 300) #=> id: 3
843
+
844
+ User
845
+ .define_window(:number_window).partition_by(:number, order_by: { id: :desc })
846
+ .select(:id, :name)
847
+ .select_window(:row_number, over: :number_window, as: :row_id)
848
+ .select_window(:first_value, :name, over: :number_window, as: :first_value_name)
849
+ #=> [
850
+ # { id: 1, name: "Alice", row_id: 2, first_value_name: "Randy" }
851
+ # { id: 2, name: "Randy", row_id: 1, first_value_name: "Randy" }
852
+ # { id: 3, name: "Bob", row_id: 1, first_value_name: "Bob" }
853
+ # ]
854
+ #
855
+
856
+ ```
857
+
858
+ Query Output
859
+ ```sql
860
+ SELECT "users"."id",
861
+ "users"."name",
862
+ (ROW_NUMBER() OVER number_window) AS "row_id",
863
+ (FIRST_VALUE(name) OVER number_window) AS "first_value_name"
864
+ FROM "users"
865
+ WINDOW number_window AS (PARTITION BY number ORDER BY id DESC)
866
+ ```
795
867
 
796
868
  ## License
797
869
 
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record_extended/version"
4
- require "active_record_extended/utilities"
4
+ require "active_record_extended/utilities/support"
5
+ require "active_record_extended/utilities/order_by"
5
6
  require "active_record_extended/active_record"
6
7
  require "active_record_extended/arel"
7
8
 
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: Remove this when ruby 2.3 support is dropped
4
- unless Hash.instance_methods(false).include?(:compact!)
5
- require "active_support/all"
6
- end
7
-
8
3
  require "active_record"
9
4
  require "active_record/relation"
10
5
  require "active_record/relation/merger"
@@ -21,11 +16,9 @@ require "active_record_extended/query_methods/any_of"
21
16
  require "active_record_extended/query_methods/either"
22
17
  require "active_record_extended/query_methods/inet"
23
18
  require "active_record_extended/query_methods/json"
19
+ require "active_record_extended/query_methods/select"
24
20
 
25
- if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR <= 1
26
- if ActiveRecord::VERSION::MINOR.zero?
27
- require "active_record_extended/patch/5_0/predicate_builder_decorator"
28
- end
21
+ if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR == 1
29
22
  require "active_record_extended/patch/5_1/where_clause"
30
23
  elsif ActiveRecord::VERSION::MAJOR >= 5
31
24
  require "active_record_extended/patch/5_2/where_clause"
@@ -1,27 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_record_extended/query_methods/window"
3
4
  require "active_record_extended/query_methods/unionize"
4
5
  require "active_record_extended/query_methods/json"
5
6
 
6
7
  module ActiveRecordExtended
7
8
  module RelationPatch
8
9
  module QueryDelegation
9
- delegate :with, to: :all
10
+ delegate :with, :define_window, :select_window, :foster_select, to: :all
10
11
  delegate(*::ActiveRecordExtended::QueryMethods::Unionize::UNIONIZE_METHODS, to: :all)
11
12
  delegate(*::ActiveRecordExtended::QueryMethods::Json::JSON_QUERY_METHODS, to: :all)
12
13
  end
13
14
 
14
15
  module Merger
15
16
  def normal_values
16
- super + [:with, :union]
17
+ super + [:union, :define_window]
18
+ end
19
+
20
+ def merge
21
+ merge_ctes!
22
+ super
23
+ end
24
+
25
+ def merge_ctes!
26
+ return unless other.with_values?
27
+
28
+ if other.recursive_value? && !relation.recursive_value?
29
+ relation.with!(:chain).recursive(other.cte)
30
+ else
31
+ relation.with!(other.cte)
32
+ end
17
33
  end
18
34
  end
19
35
 
20
36
  module ArelBuildPatch
21
37
  def build_arel(*aliases)
22
38
  super.tap do |arel|
23
- build_unions(arel) if union_values?
24
- build_with(arel) if with_values?
39
+ build_windows(arel) if window_values?
40
+ build_unions(arel) if union_values?
41
+ build_with(arel) if with_values?
25
42
  end
26
43
  end
27
44
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record_extended/arel/nodes"
4
+ require "active_record_extended/arel/sql_literal"
5
+ require "active_record_extended/arel/aggregate_function_name"
4
6
  require "active_record_extended/arel/predications"
5
7
  require "active_record_extended/arel/visitors/postgresql_decorator"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel
4
+ module Nodes
5
+ class AggregateFunctionName < ::Arel::Nodes::Node
6
+ include Arel::Predications
7
+ include Arel::WindowPredications
8
+ attr_accessor :name, :expressions, :distinct, :alias, :orderings
9
+
10
+ def initialize(name, expr, distinct = false)
11
+ super()
12
+ @name = name.to_s.upcase
13
+ @expressions = expr
14
+ @distinct = distinct
15
+ end
16
+
17
+ def order_by(expr)
18
+ @orderings = expr
19
+ self
20
+ end
21
+
22
+ def as(aliaz)
23
+ self.alias = SqlLiteral.new(aliaz)
24
+ self
25
+ end
26
+
27
+ def hash
28
+ [@name, @expressions, @distinct, @alias, @orderings].hash
29
+ end
30
+
31
+ def eql?(other)
32
+ self.class == other.class &&
33
+ expressions == other.expressions &&
34
+ orderings == other.orderings &&
35
+ distinct == other.distinct
36
+ end
37
+ alias == eql?
38
+ end
39
+ end
40
+ end
@@ -1,56 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "arel/nodes/binary"
4
+ require "arel/nodes/function"
4
5
 
5
6
  module Arel
6
7
  module Nodes
7
- class Overlap < Arel::Nodes::Binary
8
- end
9
-
10
- class Contains < Arel::Nodes::Binary
11
- end
12
-
13
- class ContainsHStore < Arel::Nodes::Binary
14
- end
15
-
16
- class ContainsArray < Arel::Nodes::Binary
17
- end
18
-
19
- class ContainedInArray < Arel::Nodes::Binary
20
- end
21
-
22
- class RowToJson < Arel::Nodes::Function
23
- def initialize(*args)
24
- super
25
- unless @expressions.is_a?(Array)
26
- @expressions = Arel.sql(@expressions) unless @expressions.is_a?(Arel::Nodes::SqlLiteral)
27
- @expressions = [@expressions]
8
+ [
9
+ "Overlap",
10
+ "Contains",
11
+ "ContainsHStore",
12
+ "ContainsArray",
13
+ "ContainedInArray"
14
+ ].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
15
+
16
+ [
17
+ "RowToJson",
18
+ "JsonBuildObject",
19
+ "JsonbBuildObject",
20
+ "ToJson",
21
+ "ToJsonb",
22
+ "Array",
23
+ "ArrayAgg"
24
+ ].each do |function_node_name|
25
+ func_klass = Class.new(::Arel::Nodes::Function) do
26
+ def initialize(*args)
27
+ super
28
+ return if @expressions.is_a?(::Array)
29
+
30
+ @expressions = @expressions.is_a?(::Arel::Nodes::Node) ? [@expressions] : [::Arel.sql(@expressions)]
28
31
  end
29
32
  end
30
- end
31
33
 
32
- class JsonBuildObject < Arel::Nodes::Function
33
- def initialize(*args)
34
- super
35
- @expressions = Array(@expressions)
36
- end
37
- end
38
-
39
- class JsonbBuildObject < JsonBuildObject
34
+ const_set(function_node_name, func_klass)
40
35
  end
41
36
 
42
37
  module Inet
43
- class ContainsEquals < Arel::Nodes::Binary
44
- end
45
-
46
- class ContainedWithin < Arel::Nodes::Binary
47
- end
48
-
49
- class ContainedWithinEquals < Arel::Nodes::Binary
50
- end
51
-
52
- class ContainsOrContainedWithin < Arel::Nodes::Binary
53
- end
38
+ [
39
+ "Contains",
40
+ "ContainsEquals",
41
+ "ContainedWithin",
42
+ "ContainedWithinEquals",
43
+ "ContainsOrContainedWithin"
44
+ ].each { |binary_node_name| const_set(binary_node_name, Class.new(::Arel::Nodes::Binary)) }
54
45
  end
55
46
  end
56
47
  end
@@ -21,12 +21,15 @@ module Arel
21
21
  def contains(other)
22
22
  Nodes::Contains.new self, Nodes.build_quoted(other, self)
23
23
  end
24
- alias inet_contains contains
25
24
 
26
25
  def contained_in_array(other)
27
26
  Nodes::ContainedInArray.new self, Nodes.build_quoted(other, self)
28
27
  end
29
28
 
29
+ def inet_contains(other)
30
+ Nodes::Inet::Contains.new self, Nodes.build_quoted(other, self)
31
+ end
32
+
30
33
  def inet_contains_or_is_contained_within(other)
31
34
  Nodes::Inet::ContainsOrContainedWithin.new self, Nodes.build_quoted(other, self)
32
35
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "arel/nodes/sql_literal"
4
+
5
+ # CTE alias fix for Rails 6.1
6
+ module Arel
7
+ module Nodes
8
+ module SqlLiteralDecorator
9
+ def name
10
+ self
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Arel::Nodes::SqlLiteral.prepend(Arel::Nodes::SqlLiteralDecorator)
@@ -23,7 +23,7 @@ module ActiveRecordExtended
23
23
  elsif left_column.try(:array)
24
24
  visit_Arel_Nodes_ContainsArray(object, collector)
25
25
  else
26
- infix_value object, collector, " >> "
26
+ visit_Arel_Nodes_Inet_Contains(object, collector)
27
27
  end
28
28
  end
29
29
 
@@ -59,6 +59,45 @@ module ActiveRecordExtended
59
59
  aggregate "JSONB_BUILD_OBJECT", object, collector
60
60
  end
61
61
 
62
+ def visit_Arel_Nodes_ToJson(object, collector)
63
+ aggregate "TO_JSON", object, collector
64
+ end
65
+
66
+ def visit_Arel_Nodes_ToJsonb(object, collector)
67
+ aggregate "TO_JSONB", object, collector
68
+ end
69
+
70
+ def visit_Arel_Nodes_Array(object, collector)
71
+ aggregate "ARRAY", object, collector
72
+ end
73
+
74
+ def visit_Arel_Nodes_ArrayAgg(object, collector)
75
+ aggregate "ARRAY_AGG", object, collector
76
+ end
77
+
78
+ def visit_Arel_Nodes_AggregateFunctionName(object, collector)
79
+ collector << "#{object.name}("
80
+ collector << "DISTINCT " if object.distinct
81
+ collector = inject_join(object.expressions, collector, ", ")
82
+
83
+ if object.orderings
84
+ collector << " ORDER BY "
85
+ collector = inject_join(object.orderings, collector, ", ")
86
+ end
87
+ collector << ")"
88
+
89
+ if object.alias
90
+ collector << " AS "
91
+ visit object.alias, collector
92
+ else
93
+ collector
94
+ end
95
+ end
96
+
97
+ def visit_Arel_Nodes_Inet_Contains(object, collector)
98
+ infix_value object, collector, " >> "
99
+ end
100
+
62
101
  def visit_Arel_Nodes_Inet_ContainedWithinEquals(object, collector)
63
102
  infix_value object, collector, " <<= "
64
103
  end