arel_toolkit 0.3.0 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +3 -0
  3. data/.github/workflows/develop.yml +90 -0
  4. data/.github/workflows/master.yml +67 -0
  5. data/.gitignore +8 -0
  6. data/.rubocop.yml +13 -5
  7. data/Appraisals +13 -0
  8. data/CHANGELOG.md +94 -5
  9. data/Gemfile +5 -0
  10. data/Gemfile.lock +62 -33
  11. data/Guardfile +4 -0
  12. data/README.md +67 -23
  13. data/Rakefile +11 -1
  14. data/arel_toolkit.gemspec +15 -6
  15. data/benchmark.rb +54 -0
  16. data/ext/pg_result_init/extconf.rb +52 -0
  17. data/ext/pg_result_init/pg_result_init.c +138 -0
  18. data/ext/pg_result_init/pg_result_init.h +6 -0
  19. data/gemfiles/active_record_6.gemfile +7 -0
  20. data/gemfiles/active_record_6.gemfile.lock +210 -0
  21. data/gemfiles/arel_gems.gemfile +10 -0
  22. data/gemfiles/arel_gems.gemfile.lock +284 -0
  23. data/gemfiles/default.gemfile +5 -0
  24. data/gemfiles/default.gemfile.lock +208 -0
  25. data/lib/arel/enhance.rb +17 -0
  26. data/lib/arel/enhance/context_enhancer/arel_table.rb +92 -0
  27. data/lib/arel/enhance/node.rb +232 -0
  28. data/lib/arel/enhance/path.rb +38 -0
  29. data/lib/arel/enhance/path_node.rb +26 -0
  30. data/lib/arel/enhance/query.rb +38 -0
  31. data/lib/arel/enhance/query_methods.rb +23 -0
  32. data/lib/arel/enhance/visitor.rb +97 -0
  33. data/lib/arel/extensions.rb +32 -6
  34. data/lib/arel/extensions/active_model_attribute_with_cast_value.rb +22 -0
  35. data/lib/arel/extensions/active_record_relation_query_attribute.rb +22 -0
  36. data/lib/arel/extensions/active_record_type_caster_connection.rb +7 -0
  37. data/lib/arel/extensions/active_record_type_caster_map.rb +7 -0
  38. data/lib/arel/extensions/array.rb +2 -9
  39. data/lib/arel/extensions/at_time_zone.rb +10 -3
  40. data/lib/arel/extensions/attributes_attribute.rb +47 -0
  41. data/lib/arel/extensions/binary.rb +7 -0
  42. data/lib/arel/extensions/bind_param.rb +15 -0
  43. data/lib/arel/extensions/bit_string.rb +2 -9
  44. data/lib/arel/extensions/case.rb +17 -0
  45. data/lib/arel/extensions/coalesce.rb +17 -3
  46. data/lib/arel/extensions/conflict.rb +9 -0
  47. data/lib/arel/extensions/contains.rb +27 -5
  48. data/lib/arel/extensions/current_catalog.rb +4 -0
  49. data/lib/arel/extensions/current_date.rb +4 -0
  50. data/lib/arel/extensions/current_of_expression.rb +2 -9
  51. data/lib/arel/extensions/current_role.rb +4 -0
  52. data/lib/arel/extensions/current_row.rb +7 -0
  53. data/lib/arel/extensions/current_schema.rb +4 -0
  54. data/lib/arel/extensions/current_user.rb +4 -0
  55. data/lib/arel/extensions/dealocate.rb +31 -0
  56. data/lib/arel/extensions/default_values.rb +4 -0
  57. data/lib/arel/extensions/delete_manager.rb +22 -6
  58. data/lib/arel/extensions/delete_statement.rb +46 -24
  59. data/lib/arel/extensions/dot.rb +11 -0
  60. data/lib/arel/extensions/exists.rb +59 -0
  61. data/lib/arel/extensions/extract_from.rb +3 -10
  62. data/lib/arel/extensions/factorial.rb +10 -2
  63. data/lib/arel/extensions/false.rb +7 -0
  64. data/lib/arel/extensions/function.rb +44 -14
  65. data/lib/arel/extensions/greatest.rb +17 -3
  66. data/lib/arel/extensions/indirection.rb +3 -12
  67. data/lib/arel/extensions/infer.rb +7 -7
  68. data/lib/arel/extensions/infix_operation.rb +17 -0
  69. data/lib/arel/extensions/insert_manager.rb +19 -3
  70. data/lib/arel/extensions/insert_statement.rb +31 -12
  71. data/lib/arel/extensions/into.rb +21 -0
  72. data/lib/arel/extensions/least.rb +17 -3
  73. data/lib/arel/extensions/named_argument.rb +3 -8
  74. data/lib/arel/extensions/named_function.rb +7 -0
  75. data/lib/arel/extensions/node.rb +10 -0
  76. data/lib/arel/extensions/ordering.rb +21 -6
  77. data/lib/arel/extensions/overlaps.rb +9 -0
  78. data/lib/arel/extensions/overlay.rb +9 -0
  79. data/lib/arel/extensions/position.rb +3 -8
  80. data/lib/arel/extensions/prepare.rb +39 -0
  81. data/lib/arel/extensions/range_function.rb +10 -2
  82. data/lib/arel/extensions/row.rb +3 -8
  83. data/lib/arel/extensions/select_core.rb +73 -0
  84. data/lib/arel/extensions/select_manager.rb +22 -6
  85. data/lib/arel/extensions/select_statement.rb +31 -9
  86. data/lib/arel/extensions/session_user.rb +4 -0
  87. data/lib/arel/extensions/set_to_default.rb +4 -0
  88. data/lib/arel/extensions/substring.rb +8 -0
  89. data/lib/arel/extensions/table.rb +43 -10
  90. data/lib/arel/extensions/time_with_precision.rb +6 -0
  91. data/lib/arel/extensions/to_sql.rb +27 -0
  92. data/lib/arel/extensions/top.rb +8 -0
  93. data/lib/arel/extensions/transaction.rb +3 -8
  94. data/lib/arel/extensions/tree_manager.rb +15 -0
  95. data/lib/arel/extensions/trim.rb +8 -0
  96. data/lib/arel/extensions/true.rb +7 -0
  97. data/lib/arel/extensions/type_cast.rb +7 -0
  98. data/lib/arel/extensions/unary.rb +7 -0
  99. data/lib/arel/extensions/unary_operation.rb +16 -0
  100. data/lib/arel/extensions/unknown.rb +4 -0
  101. data/lib/arel/extensions/update_manager.rb +22 -6
  102. data/lib/arel/extensions/update_statement.rb +36 -33
  103. data/lib/arel/extensions/user.rb +4 -0
  104. data/lib/arel/extensions/values_list.rb +15 -0
  105. data/lib/arel/extensions/variable_set.rb +9 -0
  106. data/lib/arel/extensions/variable_show.rb +3 -8
  107. data/lib/arel/middleware.rb +5 -1
  108. data/lib/arel/middleware/active_record_extension.rb +13 -0
  109. data/lib/arel/middleware/cache_accessor.rb +35 -0
  110. data/lib/arel/middleware/chain.rb +108 -33
  111. data/lib/arel/middleware/database_executor.rb +77 -0
  112. data/lib/arel/middleware/no_op_cache.rb +9 -0
  113. data/lib/arel/middleware/postgresql_adapter.rb +41 -5
  114. data/lib/arel/middleware/railtie.rb +15 -1
  115. data/lib/arel/middleware/result.rb +170 -0
  116. data/lib/arel/middleware/to_sql_executor.rb +15 -0
  117. data/lib/arel/middleware/to_sql_middleware.rb +33 -0
  118. data/lib/arel/sql_to_arel.rb +6 -3
  119. data/lib/arel/sql_to_arel/pg_query_visitor.rb +67 -38
  120. data/lib/arel/sql_to_arel/pg_query_visitor/frame_options.rb +1 -1
  121. data/lib/arel/sql_to_arel/result.rb +17 -4
  122. data/lib/arel/transformer.rb +8 -0
  123. data/lib/arel/transformer/prefix_schema_name.rb +183 -0
  124. data/lib/arel/transformer/remove_active_record_info.rb +40 -0
  125. data/lib/arel/transformer/replace_table_with_subquery.rb +31 -0
  126. data/lib/arel_toolkit.rb +15 -2
  127. data/lib/arel_toolkit/version.rb +1 -1
  128. metadata +179 -42
  129. data/.travis.yml +0 -29
  130. data/lib/arel/extensions/generate_series.rb +0 -9
  131. data/lib/arel/extensions/rank.rb +0 -9
  132. data/lib/arel/extensions/unbound_column_reference.rb +0 -5
  133. data/lib/arel/sql_formatter.rb +0 -59
@@ -10,4 +10,10 @@ module Arel
10
10
  end
11
11
  end
12
12
  end
13
+
14
+ module Visitors
15
+ class Dot
16
+ alias visit_Arel_Nodes_TimeWithPrecision terminal
17
+ end
18
+ end
13
19
  end
@@ -0,0 +1,27 @@
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ module Visitors
6
+ class ToSql
7
+ def visit_Arel_Attributes_Attribute(o, collector)
8
+ if o.relation
9
+ join_name = o.relation.table_alias || o.relation.name
10
+ collector << "#{quote_table_name join_name}.#{quote_column_name o.name}"
11
+ else
12
+ visit_Arel_Nodes_UnqualifiedColumn o, collector
13
+ end
14
+ end
15
+
16
+ alias visit_Arel_Attributes_Integer visit_Arel_Attributes_Attribute
17
+ alias visit_Arel_Attributes_Float visit_Arel_Attributes_Attribute
18
+ alias visit_Arel_Attributes_Decimal visit_Arel_Attributes_Attribute
19
+ alias visit_Arel_Attributes_String visit_Arel_Attributes_Attribute
20
+ alias visit_Arel_Attributes_Time visit_Arel_Attributes_Attribute
21
+ alias visit_Arel_Attributes_Boolean visit_Arel_Attributes_Attribute
22
+ end
23
+ end
24
+ end
25
+
26
+ # rubocop:enable Naming/MethodName
27
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arel # :nodoc: all
4
+ module Nodes
5
+ class Top < Unary
6
+ end
7
+ end
8
+ end
@@ -6,14 +6,9 @@
6
6
  module Arel
7
7
  module Nodes
8
8
  # https://www.postgresql.org/docs/8.3/tutorial-transactions.html
9
- class Transaction < Arel::Nodes::Node
10
- attr_reader :type
11
- attr_reader :options
12
-
13
- def initialize(type, options)
14
- @type = type
15
- @options = options
16
- end
9
+ class Transaction < Arel::Nodes::Binary
10
+ alias type left
11
+ alias options right
17
12
  end
18
13
  end
19
14
 
@@ -0,0 +1,15 @@
1
+ module Arel
2
+ class TreeManager
3
+ # Iterate through AST, nodes will be yielded depth-first
4
+ def each(&block)
5
+ return enum_for(:each) unless block_given?
6
+
7
+ ::Arel::Visitors::DepthFirst.new(block).accept ast
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
14
+ end
15
+ end
@@ -29,6 +29,14 @@ module Arel
29
29
  collector << ')'
30
30
  end
31
31
  end
32
+
33
+ class Dot
34
+ def visit_Arel_Nodes_Trim(o)
35
+ visit_edge o, 'type'
36
+ visit_edge o, 'substring'
37
+ visit_edge o, 'string'
38
+ end
39
+ end
32
40
  end
33
41
  end
34
42
 
@@ -0,0 +1,7 @@
1
+ module Arel
2
+ module Visitors
3
+ class Dot
4
+ alias visit_Arel_Nodes_True terminal
5
+ end
6
+ end
7
+ end
@@ -27,6 +27,13 @@ module Arel
27
27
  collector << o.type_name
28
28
  end
29
29
  end
30
+
31
+ class Dot
32
+ def visit_Arel_Nodes_TypeCast(o)
33
+ visit_edge(o, 'arg')
34
+ visit_edge(o, 'type_name')
35
+ end
36
+ end
30
37
  end
31
38
  end
32
39
 
@@ -0,0 +1,7 @@
1
+ module Arel
2
+ module Visitors
3
+ class Dot
4
+ alias visit_Arel_Nodes_Unary unary
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
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_UnaryOperation(o)
8
+ visit_edge o, 'operator'
9
+ visit_edge o, 'expr'
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ # rubocop:enable Naming/MethodName
16
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -13,6 +13,10 @@ module Arel
13
13
  collector << 'UNKNOWN'
14
14
  end
15
15
  end
16
+
17
+ class Dot
18
+ alias visit_Arel_Nodes_Unknown terminal
19
+ end
16
20
  end
17
21
  end
18
22
 
@@ -1,9 +1,25 @@
1
- Arel::UpdateManager.class_eval do
2
- def ==(other)
3
- @ast == other.ast && @ctx == other.ctx
4
- end
1
+ # rubocop:disable Naming/MethodName
2
+ # rubocop:disable Naming/UncommunicativeMethodParamName
3
+
4
+ module Arel
5
+ class UpdateManager < Arel::TreeManager
6
+ def ==(other)
7
+ other.is_a?(self.class) && @ast == other.ast && @ctx == other.ctx
8
+ end
9
+
10
+ protected
5
11
 
6
- protected
12
+ attr_reader :ctx
13
+ end
7
14
 
8
- attr_reader :ctx
15
+ module Visitors
16
+ class Dot
17
+ def visit_Arel_UpdateManager(o)
18
+ visit_edge o, 'ast'
19
+ end
20
+ end
21
+ end
9
22
  end
23
+
24
+ # rubocop:enable Naming/MethodName
25
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -3,34 +3,38 @@
3
3
 
4
4
  module Arel
5
5
  module Nodes
6
- # https://www.postgresql.org/docs/10/sql-update.html
7
- Arel::Nodes::UpdateStatement.class_eval do
8
- attr_accessor :with
9
- attr_accessor :froms
10
- attr_accessor :returning
6
+ class UpdateStatement
7
+ module UpdateStatementExtension
8
+ # https://www.postgresql.org/docs/10/sql-update.html
9
+ attr_accessor :with
10
+ attr_accessor :froms
11
+ attr_accessor :returning
11
12
 
12
- alias_method :old_initialize, :initialize
13
- def initialize
14
- old_initialize
13
+ def initialize
14
+ super
15
15
 
16
- @froms = []
17
- @returning = []
16
+ @froms = []
17
+ @returning = []
18
+ end
18
19
  end
20
+
21
+ prepend UpdateStatementExtension
19
22
  end
20
23
  end
21
24
 
22
25
  module Visitors
23
26
  class ToSql
24
- # rubocop:disable Metrics/CyclomaticComplexity
25
27
  # rubocop:disable Metrics/AbcSize
26
- # rubocop:disable Metrics/PerceivedComplexity
27
28
  def visit_Arel_Nodes_UpdateStatement(o, collector)
28
29
  if o.with
29
30
  collector = visit o.with, collector
30
- collector << SPACE
31
+ collector << ' '
31
32
  end
32
33
 
33
- wheres = if o.orders.empty? && o.limit.nil?
34
+ wheres = if Gem.loaded_specs['activerecord'].version >= Gem::Version.new('6.0.0')
35
+ o = prepare_update_statement(o)
36
+ o.wheres
37
+ elsif o.orders.empty? && o.limit.nil?
34
38
  o.wheres
35
39
  else
36
40
  [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
@@ -38,31 +42,30 @@ module Arel
38
42
 
39
43
  collector << 'UPDATE '
40
44
  collector = visit o.relation, collector
41
- unless o.values.empty?
42
- collector << ' SET '
43
- collector = inject_join o.values, collector, ', '
44
- end
45
45
 
46
- unless o.froms.empty?
47
- collector << ' FROM '
48
- collector = inject_join o.froms, collector, ', '
49
- end
50
-
51
- unless wheres.empty?
52
- collector << ' WHERE '
53
- collector = inject_join wheres, collector, ' AND '
54
- end
46
+ collect_nodes_for o.values, collector, ' SET '
47
+ collect_nodes_for o.froms, collector, ' FROM ', ', '
55
48
 
56
- unless o.returning.empty?
57
- collector << ' RETURNING '
58
- collector = inject_join o.returning, collector, ', '
59
- end
49
+ collect_nodes_for wheres, collector, ' WHERE ', ' AND '
50
+ collect_nodes_for o.returning, collector, ' RETURNING ', ', '
60
51
 
61
52
  collector
62
53
  end
63
54
  # rubocop:enable Metrics/AbcSize
64
- # rubocop:enable Metrics/CyclomaticComplexity
65
- # rubocop:enable Metrics/PerceivedComplexity
55
+ end
56
+
57
+ class Dot
58
+ module UpdateStatementExtension
59
+ def visit_Arel_Nodes_UpdateStatement(o)
60
+ super
61
+
62
+ visit_edge o, 'with'
63
+ visit_edge o, 'froms'
64
+ visit_edge o, 'returning'
65
+ end
66
+ end
67
+
68
+ prepend UpdateStatementExtension
66
69
  end
67
70
  end
68
71
  end
@@ -13,6 +13,10 @@ module Arel
13
13
  collector << 'user'
14
14
  end
15
15
  end
16
+
17
+ class Dot
18
+ alias visit_Arel_Nodes_User terminal
19
+ end
16
20
  end
17
21
  end
18
22
 
@@ -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_ValuesList(o)
8
+ visit_edge o, 'rows'
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ # rubocop:enable Naming/MethodName
15
+ # rubocop:enable Naming/UncommunicativeMethodParamName
@@ -39,6 +39,15 @@ module Arel
39
39
  end
40
40
  end
41
41
  end
42
+
43
+ class Dot
44
+ def visit_Arel_Nodes_VariableSet(o)
45
+ visit_edge o, 'type'
46
+ visit_edge o, 'args'
47
+ visit_edge o, 'name'
48
+ visit_edge o, 'local'
49
+ end
50
+ end
42
51
  end
43
52
  end
44
53
 
@@ -4,12 +4,7 @@
4
4
  module Arel
5
5
  module Nodes
6
6
  # https://www.postgresql.org/docs/9.1/sql-show.html
7
- class VariableShow < Arel::Nodes::Node
8
- attr_reader :name
9
-
10
- def initialize(name)
11
- @name = name
12
- end
7
+ class VariableShow < Arel::Nodes::Unary
13
8
  end
14
9
  end
15
10
 
@@ -17,10 +12,10 @@ module Arel
17
12
  class ToSql
18
13
  def visit_Arel_Nodes_VariableShow(o, collector)
19
14
  collector << 'SHOW '
20
- collector << if o.name == 'timezone'
15
+ collector << if o.expr == 'timezone'
21
16
  'TIME ZONE'
22
17
  else
23
- o.name
18
+ o.expr
24
19
  end
25
20
  end
26
21
  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
@@ -0,0 +1,35 @@
1
+ module Arel
2
+ module Middleware
3
+ class CacheAccessor
4
+ attr_reader :cache
5
+
6
+ def initialize(cache)
7
+ @cache = cache
8
+ end
9
+
10
+ def read(original_sql)
11
+ cache.read cache_key(original_sql)
12
+ end
13
+
14
+ def write(transformed_sql:, transformed_binds:, original_sql:, original_binds:)
15
+ # To play it safe, the order of binds was changed and therefore we won't reuse the query
16
+ return if transformed_binds != original_binds
17
+
18
+ cache.write(cache_key(original_sql), transformed_sql)
19
+ end
20
+
21
+ def cache_key_for_sql(sql)
22
+ Digest::SHA256.hexdigest(sql)
23
+ end
24
+
25
+ def cache_key(sql)
26
+ # An important aspect of this cache key method is that it includes hashes of all active
27
+ # middlewares. If multiple Arel middleware chains that are using the same cache backend,
28
+ # this cache key mechanism will prevent cache entries leak in the wrong chain.
29
+
30
+ active_middleware_cache_key = Arel.middleware.current.map(&:hash).join('&') || 0
31
+ active_middleware_cache_key + '|' + cache_key_for_sql(sql)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,60 +1,90 @@
1
+ require_relative './no_op_cache'
2
+ require_relative './cache_accessor'
3
+
1
4
  module Arel
2
5
  module Middleware
3
6
  class Chain
4
- def initialize(internal_middleware = [], internal_context = {})
7
+ attr_reader :executing_middleware_depth
8
+ attr_reader :executor
9
+ attr_reader :cache
10
+
11
+ MAX_RECURSION_DEPTH = 10
12
+
13
+ def initialize(
14
+ internal_middleware = [],
15
+ internal_context = {},
16
+ executor_class = Arel::Middleware::DatabaseExecutor,
17
+ cache: nil
18
+ )
5
19
  @internal_middleware = internal_middleware
6
20
  @internal_context = internal_context
21
+ @executor = executor_class.new(internal_middleware)
22
+ @executing_middleware_depth = 0
23
+ @cache = cache || NoOpCache
7
24
  end
8
25
 
9
- def execute(sql, binds = [])
10
- return sql if internal_middleware.length.zero?
26
+ def cache_accessor
27
+ @cache_accessor ||= CacheAccessor.new @cache
28
+ end
11
29
 
12
- result = Arel.sql_to_arel(sql, binds: binds)
13
- updated_context = context.merge(original_sql: sql)
30
+ def execute(sql, binds = [], &execute_sql)
31
+ return execute_sql.call(sql, binds).to_casted_result if internal_middleware.length.zero?
14
32
 
15
- internal_middleware.each do |middleware_item|
16
- result = result.map do |arel|
17
- middleware_item.call(arel, updated_context)
18
- end
33
+ if (cached_sql = cache_accessor.read(sql))
34
+ return execute_sql.call(cached_sql, binds).to_casted_result
19
35
  end
20
36
 
21
- result.to_sql
37
+ execute_with_middleware(sql, binds, execute_sql).to_casted_result
38
+ rescue ::PgQuery::ParseError
39
+ execute_sql.call(sql, binds)
40
+ ensure
41
+ @executing_middleware_depth -= 1
22
42
  end
23
43
 
24
44
  def current
25
45
  internal_middleware.dup
26
46
  end
27
47
 
28
- def apply(middleware, &block)
29
- continue_chain(middleware, internal_context, &block)
48
+ def apply(middleware, cache: @cache, &block)
49
+ new_middleware = Array.wrap(middleware)
50
+ continue_chain(new_middleware, internal_context, cache: cache, &block)
30
51
  end
52
+ alias only apply
31
53
 
32
- def only(middleware, &block)
33
- continue_chain(middleware, internal_context, &block)
54
+ def none(&block)
55
+ continue_chain([], internal_context, cache: cache, &block)
34
56
  end
35
57
 
36
- def none(&block)
37
- continue_chain([], internal_context, &block)
58
+ def except(without_middleware, cache: @cache, &block)
59
+ without_middleware = Array.wrap(without_middleware)
60
+ new_middleware = internal_middleware - without_middleware
61
+ continue_chain(new_middleware, internal_context, cache: cache, &block)
38
62
  end
39
63
 
40
- def except(without_middleware, &block)
41
- new_middleware = internal_middleware.reject do |middleware|
42
- middleware == without_middleware
43
- end
64
+ def insert_before(new_middleware, existing_middleware, cache: @cache, &block)
65
+ new_middleware = Array.wrap(new_middleware)
66
+ index = internal_middleware.index(existing_middleware)
67
+ updated_middleware = internal_middleware.insert(index, *new_middleware)
68
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
69
+ end
44
70
 
45
- continue_chain(new_middleware, internal_context, &block)
71
+ def prepend(new_middleware, cache: @cache, &block)
72
+ new_middleware = Array.wrap(new_middleware)
73
+ updated_middleware = new_middleware + internal_middleware
74
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
46
75
  end
47
76
 
48
- def insert_before(new_middleware, existing_middleware, &block)
77
+ def insert_after(new_middleware, existing_middleware, cache: @cache, &block)
78
+ new_middleware = Array.wrap(new_middleware)
49
79
  index = internal_middleware.index(existing_middleware)
50
- updated_middleware = internal_middleware.insert(index, new_middleware)
51
- continue_chain(updated_middleware, internal_context, &block)
80
+ updated_middleware = internal_middleware.insert(index + 1, *new_middleware)
81
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
52
82
  end
53
83
 
54
- def insert_after(new_middleware, existing_middleware, &block)
55
- index = internal_middleware.index(existing_middleware)
56
- updated_middleware = internal_middleware.insert(index + 1, new_middleware)
57
- continue_chain(updated_middleware, internal_context, &block)
84
+ def append(new_middleware, cache: @cache, &block)
85
+ new_middleware = Array.wrap(new_middleware)
86
+ updated_middleware = internal_middleware + new_middleware
87
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
58
88
  end
59
89
 
60
90
  def context(new_context = nil, &block)
@@ -64,7 +94,21 @@ module Arel
64
94
 
65
95
  return internal_context if new_context.nil?
66
96
 
67
- continue_chain(internal_middleware, new_context, &block)
97
+ continue_chain(internal_middleware, new_context, cache: @cache, &block)
98
+ end
99
+
100
+ def to_sql(type, &block)
101
+ middleware = Arel::Middleware::ToSqlMiddleware.new(type)
102
+
103
+ new_chain = Arel::Middleware::Chain.new(
104
+ internal_middleware + [middleware],
105
+ internal_context,
106
+ Arel::Middleware::ToSqlExecutor,
107
+ )
108
+
109
+ maybe_execute_block(new_chain, &block)
110
+
111
+ middleware.sql
68
112
  end
69
113
 
70
114
  protected
@@ -74,8 +118,23 @@ module Arel
74
118
 
75
119
  private
76
120
 
77
- def continue_chain(middleware, context, &block)
78
- new_chain = Arel::Middleware::Chain.new(middleware, context)
121
+ def execute_with_middleware(sql, binds, execute_sql)
122
+ check_middleware_recursion(sql)
123
+
124
+ updated_context = context.merge(
125
+ original_sql: sql,
126
+ original_binds: binds,
127
+ cache_accessor: cache_accessor,
128
+ )
129
+
130
+ arel = Arel.sql_to_arel(sql, binds: binds)
131
+ enhanced_arel = Arel.enhance(arel)
132
+
133
+ executor.run(enhanced_arel, updated_context, execute_sql)
134
+ end
135
+
136
+ def continue_chain(middleware, context, cache:, &block)
137
+ new_chain = Arel::Middleware::Chain.new(middleware, context, cache: cache)
79
138
  maybe_execute_block(new_chain, &block)
80
139
  end
81
140
 
@@ -89,8 +148,24 @@ module Arel
89
148
  Arel::Middleware.current_chain = previous_chain
90
149
  end
91
150
 
92
- def current_chain
93
- Arel::Middleware.current_chain
151
+ def check_middleware_recursion(sql)
152
+ if executing_middleware_depth > MAX_RECURSION_DEPTH
153
+ message = <<~ERROR
154
+ Middleware is being called from within middleware, aborting execution
155
+ to prevent endless recursion. You can do the following if you want to execute SQL
156
+ inside middleware:
157
+
158
+ - Set middleware context before entering the middleware
159
+ - Use `Arel.middleware.none { ... }` to temporarily disable middleware
160
+
161
+ SQL that triggered the error:
162
+ #{sql}
163
+ ERROR
164
+
165
+ raise message
166
+ else
167
+ @executing_middleware_depth += 1
168
+ end
94
169
  end
95
170
  end
96
171
  end