arel_toolkit 0.3.0 → 0.4.4

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 (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