arel_toolkit 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/develop.yml +86 -0
  3. data/.github/workflows/master.yml +67 -0
  4. data/.gitignore +4 -0
  5. data/CHANGELOG.md +35 -3
  6. data/Gemfile.lock +22 -15
  7. data/Guardfile +4 -0
  8. data/README.md +11 -8
  9. data/Rakefile +11 -1
  10. data/arel_toolkit.gemspec +4 -1
  11. data/ext/pg_result_init/extconf.rb +52 -0
  12. data/ext/pg_result_init/pg_result_init.c +138 -0
  13. data/ext/pg_result_init/pg_result_init.h +6 -0
  14. data/gemfiles/arel_gems.gemfile.lock +7 -0
  15. data/gemfiles/default.gemfile.lock +7 -0
  16. data/lib/arel/enhance.rb +1 -0
  17. data/lib/arel/enhance/context_enhancer/arel_table.rb +18 -1
  18. data/lib/arel/enhance/node.rb +25 -6
  19. data/lib/arel/enhance/query.rb +2 -0
  20. data/lib/arel/enhance/query_methods.rb +23 -0
  21. data/lib/arel/enhance/visitor.rb +4 -2
  22. data/lib/arel/extensions.rb +7 -2
  23. data/lib/arel/extensions/active_model_attribute_with_cast_value.rb +22 -0
  24. data/lib/arel/extensions/active_record_relation_query_attribute.rb +22 -0
  25. data/lib/arel/extensions/active_record_type_caster_connection.rb +7 -0
  26. data/lib/arel/extensions/attributes_attribute.rb +47 -0
  27. data/lib/arel/extensions/bind_param.rb +15 -0
  28. data/lib/arel/extensions/coalesce.rb +17 -3
  29. data/lib/arel/extensions/exists.rb +59 -0
  30. data/lib/arel/extensions/function.rb +2 -1
  31. data/lib/arel/extensions/greatest.rb +17 -3
  32. data/lib/arel/extensions/insert_statement.rb +2 -2
  33. data/lib/arel/extensions/least.rb +17 -3
  34. data/lib/arel/extensions/node.rb +10 -0
  35. data/lib/arel/extensions/range_function.rb +10 -2
  36. data/lib/arel/extensions/select_core.rb +1 -0
  37. data/lib/arel/extensions/tree_manager.rb +5 -0
  38. data/lib/arel/middleware.rb +5 -1
  39. data/lib/arel/middleware/active_record_extension.rb +13 -0
  40. data/lib/arel/middleware/chain.rb +76 -21
  41. data/lib/arel/middleware/database_executor.rb +68 -0
  42. data/lib/arel/middleware/postgresql_adapter.rb +41 -5
  43. data/lib/arel/middleware/railtie.rb +6 -2
  44. data/lib/arel/middleware/result.rb +170 -0
  45. data/lib/arel/middleware/to_sql_executor.rb +15 -0
  46. data/lib/arel/middleware/to_sql_middleware.rb +33 -0
  47. data/lib/arel/sql_to_arel/pg_query_visitor.rb +34 -33
  48. data/lib/arel/sql_to_arel/result.rb +19 -2
  49. data/lib/arel/transformer.rb +2 -1
  50. data/lib/arel/transformer/prefix_schema_name.rb +183 -0
  51. data/lib/arel/transformer/remove_active_record_info.rb +2 -4
  52. data/lib/arel/transformer/replace_table_with_subquery.rb +31 -0
  53. data/lib/arel_toolkit.rb +6 -1
  54. data/lib/arel_toolkit/version.rb +1 -1
  55. metadata +55 -10
  56. data/.travis.yml +0 -34
  57. data/lib/arel/extensions/generate_series.rb +0 -9
  58. data/lib/arel/extensions/rank.rb +0 -9
  59. data/lib/arel/transformer/add_schema_to_table.rb +0 -26
@@ -0,0 +1,22 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ module Visitors
6
+ class Dot
7
+ def visit_ActiveModel_Attribute_WithCastValue(o)
8
+ visit_edge o, 'name'
9
+ visit_edge o, 'value_before_type_cast'
10
+ end
11
+ end
12
+
13
+ class ToSql
14
+ def visit_ActiveModel_Attribute_WithCastValue(_o, collector)
15
+ collector
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ # rubocop:enable Naming/MethodName
22
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -0,0 +1,22 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ module Visitors
6
+ class Dot
7
+ def visit_ActiveRecord_Relation_QueryAttribute(o)
8
+ visit_edge o, 'name'
9
+ visit_edge o, 'value_before_type_cast'
10
+ end
11
+ end
12
+
13
+ class ToSql
14
+ def visit_ActiveRecord_Relation_QueryAttribute(_o, collector)
15
+ collector
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ # rubocop:enable Naming/MethodName
22
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -0,0 +1,7 @@
1
+ module Arel
2
+ module Visitors
3
+ class Dot
4
+ alias visit_ActiveRecord_TypeCaster_Connection terminal
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,47 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ module Attributes
6
+ class Attribute
7
+ module AttributeExtension
8
+ # postgres only: https://www.postgresql.org/docs/10/ddl-schemas.html
9
+ attr_accessor :schema_name
10
+ attr_accessor :database
11
+ end
12
+
13
+ prepend AttributeExtension
14
+ end
15
+ end
16
+
17
+ module Visitors
18
+ class ToSql
19
+ module AttributesAttributeExtension
20
+ def visit_Arel_Attributes_Attribute(o, collector)
21
+ collector << "#{quote_table_name(o.database)}." if o.database
22
+ collector << "#{quote_table_name(o.schema_name)}." if o.schema_name
23
+
24
+ super
25
+ end
26
+ end
27
+
28
+ prepend AttributesAttributeExtension
29
+ end
30
+
31
+ class Dot
32
+ module AttributesAttributeExtension
33
+ def visit_Arel_Attributes_Attribute(o)
34
+ super
35
+
36
+ visit_edge o, 'schema_name'
37
+ visit_edge o, 'database'
38
+ end
39
+ end
40
+
41
+ prepend AttributesAttributeExtension
42
+ end
43
+ end
44
+ end
45
+
46
+ # rubocop:enable Naming/MethodName
47
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -0,0 +1,15 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ module Visitors
6
+ class Dot
7
+ def visit_Arel_Nodes_BindParam(o)
8
+ visit_edge o, 'value'
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ # rubocop:enable Naming/MethodName
15
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -1,9 +1,23 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
1
4
  module Arel
2
5
  module Nodes
3
- class Coalesce < Arel::Nodes::NamedFunction
4
- def initialize(args)
5
- super 'COALESCE', args
6
+ # https://www.postgresql.org/docs/10/functions-conditional.html
7
+ class Coalesce < Arel::Nodes::Unary
8
+ end
9
+ end
10
+
11
+ module Visitors
12
+ class ToSql
13
+ def visit_Arel_Nodes_Coalesce(o, collector)
14
+ collector << 'COALESCE('
15
+ collector = inject_join(o.expr, collector, ', ')
16
+ collector << ')'
6
17
  end
7
18
  end
8
19
  end
9
20
  end
21
+
22
+ # rubocop:enable Naming/MethodName
23
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -0,0 +1,59 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ module Nodes
6
+ # This is a copy of https://github.com/rails/arel/blob/v9.0.0/lib/arel/nodes/function.rb
7
+ # Only difference is the superclass, because EXISTS is not a function but a subquery expression.
8
+ # Semantic meaning is important when transforming the Arel using the enhanced AST,
9
+ # because EXISTS cannot be processed as a function. For example it does not have a schema
10
+ # like a normal function.
11
+ #
12
+ # To change the superclass we're removing the existing Exists class `Arel::Nodes::Exists`
13
+ # and recreating it extending from `Arel::Nodes::Unary`.
14
+ remove_const(:Exists)
15
+
16
+ # https://www.postgresql.org/docs/10/functions-subquery.html
17
+ class Exists < Arel::Nodes::Unary
18
+ include Arel::Predications
19
+ include Arel::WindowPredications
20
+ include Arel::OrderPredications
21
+ attr_accessor :expressions, :alias, :distinct
22
+
23
+ def initialize(expr, aliaz = nil)
24
+ @expressions = expr
25
+ @alias = aliaz && SqlLiteral.new(aliaz)
26
+ @distinct = false
27
+ end
28
+
29
+ def as(aliaz)
30
+ self.alias = SqlLiteral.new(aliaz)
31
+ self
32
+ end
33
+
34
+ def hash
35
+ [@expressions, @alias, @distinct].hash
36
+ end
37
+
38
+ def eql?(other)
39
+ self.class == other.class &&
40
+ expressions == other.expressions &&
41
+ self.alias == other.alias &&
42
+ distinct == other.distinct
43
+ end
44
+ alias == eql?
45
+ end
46
+ end
47
+
48
+ module Visitors
49
+ class Dot
50
+ def visit_Arel_Nodes_Exists(o)
51
+ visit_edge o, 'expressions'
52
+ visit_edge o, 'alias'
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # rubocop:enable Naming/MethodName
59
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -80,13 +80,14 @@ module Arel
80
80
  visit_edge o, 'filter'
81
81
  visit_edge o, 'within_group'
82
82
  visit_edge o, 'variardic'
83
+ visit_edge o, 'schema_name'
83
84
  end
84
85
 
85
- alias visit_Arel_Nodes_Exists function
86
86
  alias visit_Arel_Nodes_Min function
87
87
  alias visit_Arel_Nodes_Max function
88
88
  alias visit_Arel_Nodes_Avg function
89
89
  alias visit_Arel_Nodes_Sum function
90
+ alias visit_Arel_Nodes_Count function
90
91
  end
91
92
 
92
93
  prepend FunctionExtension
@@ -1,9 +1,23 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
1
4
  module Arel
2
5
  module Nodes
3
- class Greatest < Arel::Nodes::NamedFunction
4
- def initialize(args)
5
- super 'GREATEST', args
6
+ # https://www.postgresql.org/docs/10/functions-conditional.html
7
+ class Greatest < Arel::Nodes::Unary
8
+ end
9
+ end
10
+
11
+ module Visitors
12
+ class ToSql
13
+ def visit_Arel_Nodes_Greatest(o, collector)
14
+ collector << 'GREATEST('
15
+ collector = inject_join(o.expr, collector, ', ')
16
+ collector << ')'
6
17
  end
7
18
  end
8
19
  end
9
20
  end
21
+
22
+ # rubocop:enable Naming/MethodName
23
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -5,7 +5,7 @@ module Arel
5
5
  module Nodes
6
6
  class InsertStatement
7
7
  # https://www.postgresql.org/docs/9.5/sql-insert.html
8
- module InsertStatementExtensions
8
+ module InsertStatementExtension
9
9
  attr_accessor :with
10
10
  attr_accessor :conflict
11
11
  attr_accessor :override
@@ -18,7 +18,7 @@ module Arel
18
18
  end
19
19
  end
20
20
 
21
- prepend(InsertStatementExtensions)
21
+ prepend(InsertStatementExtension)
22
22
  end
23
23
  end
24
24
 
@@ -1,9 +1,23 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
1
4
  module Arel
2
5
  module Nodes
3
- class Least < Arel::Nodes::NamedFunction
4
- def initialize(args)
5
- super 'LEAST', args
6
+ # https://www.postgresql.org/docs/10/functions-conditional.html
7
+ class Least < Arel::Nodes::Unary
8
+ end
9
+ end
10
+
11
+ module Visitors
12
+ class ToSql
13
+ def visit_Arel_Nodes_Least(o, collector)
14
+ collector << 'LEAST('
15
+ collector = inject_join(o.expr, collector, ', ')
16
+ collector << ')'
6
17
  end
7
18
  end
8
19
  end
9
20
  end
21
+
22
+ # rubocop:enable Naming/MethodName
23
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -0,0 +1,10 @@
1
+ module Arel
2
+ module Nodes
3
+ class Node
4
+ def to_sql_and_binds(engine = Arel::Table.engine)
5
+ collector = engine.connection.send(:collector)
6
+ engine.connection.visitor.accept(self, collector).value
7
+ end
8
+ end
9
+ end
10
+ end
@@ -5,15 +5,23 @@ module Arel
5
5
  module Nodes
6
6
  # Postgres: https://www.postgresql.org/docs/9.4/sql-select.html
7
7
  class RangeFunction < Arel::Nodes::Unary
8
+ attr_reader :is_rowsfrom
9
+
10
+ def initialize(*args, is_rowsfrom:, **kwargs)
11
+ @is_rowsfrom = is_rowsfrom
12
+ super(*args, **kwargs)
13
+ end
8
14
  end
9
15
  end
10
16
 
11
17
  module Visitors
12
18
  class ToSql
13
19
  def visit_Arel_Nodes_RangeFunction(o, collector)
14
- collector << 'ROWS FROM ('
20
+ collector << 'ROWS FROM (' if o.is_rowsfrom
15
21
  visit o.expr, collector
16
- collector << ')'
22
+ collector << ')' if o.is_rowsfrom
23
+
24
+ collector
17
25
  end
18
26
  end
19
27
  end
@@ -45,6 +45,7 @@ module Arel
45
45
  super
46
46
 
47
47
  visit_edge o, 'into'
48
+ visit_edge o, 'top'
48
49
  end
49
50
  end
50
51
 
@@ -6,5 +6,10 @@ module Arel
6
6
 
7
7
  ::Arel::Visitors::DepthFirst.new(block).accept ast
8
8
  end
9
+
10
+ def to_sql_and_binds(engine = Arel::Table.engine)
11
+ collector = engine.connection.send(:collector)
12
+ engine.connection.visitor.accept(@ast, collector).value
13
+ end
9
14
  end
10
15
  end
@@ -1,6 +1,10 @@
1
- require 'active_record'
1
+ require_relative './middleware/active_record_extension'
2
2
  require_relative './middleware/railtie'
3
3
  require_relative './middleware/chain'
4
+ require_relative './middleware/database_executor'
5
+ require_relative './middleware/to_sql_executor'
6
+ require_relative './middleware/to_sql_middleware'
7
+ require_relative './middleware/result'
4
8
  require_relative './middleware/postgresql_adapter'
5
9
 
6
10
  module Arel
@@ -0,0 +1,13 @@
1
+ module Arel
2
+ module Middleware
3
+ module ActiveRecordExtension
4
+ def load_schema!
5
+ # Prevent Rails from memoizing an empty response when using `Arel.middleware.to_sql`.
6
+ # Re-applying the middleware will use the database executor to fetch the actual data.
7
+ Arel.middleware.apply(Arel.middleware.current) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,24 +1,35 @@
1
1
  module Arel
2
2
  module Middleware
3
3
  class Chain
4
- def initialize(internal_middleware = [], internal_context = {})
4
+ attr_reader :executing_middleware
5
+ attr_reader :executor
6
+
7
+ def initialize(
8
+ internal_middleware = [],
9
+ internal_context = {},
10
+ executor_class = Arel::Middleware::DatabaseExecutor
11
+ )
5
12
  @internal_middleware = internal_middleware
6
13
  @internal_context = internal_context
14
+ @executor = executor_class.new(internal_middleware)
15
+ @executing_middleware = false
7
16
  end
8
17
 
9
- def execute(sql, binds = [])
10
- return sql if internal_middleware.length.zero?
18
+ def execute(sql, binds = [], &execute_sql)
19
+ return execute_sql.call(sql, binds).to_casted_result if internal_middleware.length.zero?
20
+
21
+ check_middleware_recursion(sql)
11
22
 
12
- result = Arel.sql_to_arel(sql, binds: binds)
13
23
  updated_context = context.merge(original_sql: sql)
24
+ enhanced_arel = Arel.enhance(Arel.sql_to_arel(sql, binds: binds))
14
25
 
15
- internal_middleware.each do |middleware_item|
16
- result = result.map do |arel|
17
- middleware_item.call(arel, updated_context.dup)
18
- end
19
- end
26
+ result = executor.run(enhanced_arel, updated_context, execute_sql)
20
27
 
21
- result.to_sql
28
+ result.to_casted_result
29
+ rescue ::PgQuery::ParseError
30
+ execute_sql.call(sql, binds)
31
+ ensure
32
+ @executing_middleware = false
22
33
  end
23
34
 
24
35
  def current
@@ -26,34 +37,44 @@ module Arel
26
37
  end
27
38
 
28
39
  def apply(middleware, &block)
29
- continue_chain(middleware, internal_context, &block)
30
- end
31
-
32
- def only(middleware, &block)
33
- continue_chain(middleware, internal_context, &block)
40
+ new_middleware = Array.wrap(middleware)
41
+ continue_chain(new_middleware, internal_context, &block)
34
42
  end
43
+ alias only apply
35
44
 
36
45
  def none(&block)
37
46
  continue_chain([], internal_context, &block)
38
47
  end
39
48
 
40
49
  def except(without_middleware, &block)
41
- new_middleware = internal_middleware.reject do |middleware|
42
- middleware == without_middleware
43
- end
44
-
50
+ without_middleware = Array.wrap(without_middleware)
51
+ new_middleware = internal_middleware - without_middleware
45
52
  continue_chain(new_middleware, internal_context, &block)
46
53
  end
47
54
 
48
55
  def insert_before(new_middleware, existing_middleware, &block)
56
+ new_middleware = Array.wrap(new_middleware)
49
57
  index = internal_middleware.index(existing_middleware)
50
- updated_middleware = internal_middleware.insert(index, new_middleware)
58
+ updated_middleware = internal_middleware.insert(index, *new_middleware)
59
+ continue_chain(updated_middleware, internal_context, &block)
60
+ end
61
+
62
+ def prepend(new_middleware, &block)
63
+ new_middleware = Array.wrap(new_middleware)
64
+ updated_middleware = new_middleware + internal_middleware
51
65
  continue_chain(updated_middleware, internal_context, &block)
52
66
  end
53
67
 
54
68
  def insert_after(new_middleware, existing_middleware, &block)
69
+ new_middleware = Array.wrap(new_middleware)
55
70
  index = internal_middleware.index(existing_middleware)
56
- updated_middleware = internal_middleware.insert(index + 1, new_middleware)
71
+ updated_middleware = internal_middleware.insert(index + 1, *new_middleware)
72
+ continue_chain(updated_middleware, internal_context, &block)
73
+ end
74
+
75
+ def append(new_middleware, &block)
76
+ new_middleware = Array.wrap(new_middleware)
77
+ updated_middleware = internal_middleware + new_middleware
57
78
  continue_chain(updated_middleware, internal_context, &block)
58
79
  end
59
80
 
@@ -67,6 +88,20 @@ module Arel
67
88
  continue_chain(internal_middleware, new_context, &block)
68
89
  end
69
90
 
91
+ def to_sql(type, &block)
92
+ middleware = Arel::Middleware::ToSqlMiddleware.new(type)
93
+
94
+ new_chain = Arel::Middleware::Chain.new(
95
+ internal_middleware + [middleware],
96
+ internal_context,
97
+ Arel::Middleware::ToSqlExecutor,
98
+ )
99
+
100
+ maybe_execute_block(new_chain, &block)
101
+
102
+ middleware.sql
103
+ end
104
+
70
105
  protected
71
106
 
72
107
  attr_reader :internal_middleware
@@ -88,6 +123,26 @@ module Arel
88
123
  ensure
89
124
  Arel::Middleware.current_chain = previous_chain
90
125
  end
126
+
127
+ def check_middleware_recursion(sql)
128
+ if executing_middleware
129
+ message = <<~ERROR
130
+ Middleware is being called from within middleware, aborting execution
131
+ to prevent endless recursion. You can do the following if you want to execute SQL
132
+ inside middleware:
133
+
134
+ - Set middleware context before entering the middleware
135
+ - Use `Arel.middleware.none { ... }` to temporarily disable middleware
136
+
137
+ SQL that triggered the error:
138
+ #{sql}
139
+ ERROR
140
+
141
+ raise message
142
+ else
143
+ @executing_middleware = true
144
+ end
145
+ end
91
146
  end
92
147
  end
93
148
  end