arel_toolkit 0.4.0 → 0.4.5

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/coverage.yml +48 -0
  3. data/.github/workflows/test.yml +65 -0
  4. data/.gitignore +6 -1
  5. data/Appraisals +4 -0
  6. data/CHANGELOG.md +87 -7
  7. data/Gemfile.lock +50 -39
  8. data/Guardfile +4 -0
  9. data/README.md +25 -11
  10. data/Rakefile +11 -1
  11. data/arel_toolkit.gemspec +10 -5
  12. data/benchmark.rb +54 -0
  13. data/ext/pg_result_init/extconf.rb +52 -0
  14. data/ext/pg_result_init/pg_result_init.c +138 -0
  15. data/ext/pg_result_init/pg_result_init.h +6 -0
  16. data/gemfiles/active_record_6.gemfile +7 -0
  17. data/gemfiles/active_record_6.gemfile.lock +210 -0
  18. data/gemfiles/arel_gems.gemfile.lock +28 -18
  19. data/gemfiles/default.gemfile.lock +30 -20
  20. data/lib/arel/enhance.rb +1 -0
  21. data/lib/arel/enhance/context_enhancer/arel_table.rb +18 -1
  22. data/lib/arel/enhance/node.rb +71 -28
  23. data/lib/arel/enhance/query.rb +2 -0
  24. data/lib/arel/enhance/query_methods.rb +23 -0
  25. data/lib/arel/enhance/visitor.rb +19 -3
  26. data/lib/arel/extensions.rb +8 -2
  27. data/lib/arel/extensions/active_model_attribute_with_cast_value.rb +22 -0
  28. data/lib/arel/extensions/active_record_relation_query_attribute.rb +22 -0
  29. data/lib/arel/extensions/active_record_type_caster_connection.rb +7 -0
  30. data/lib/arel/extensions/attributes_attribute.rb +47 -0
  31. data/lib/arel/extensions/bind_param.rb +15 -0
  32. data/lib/arel/extensions/coalesce.rb +17 -3
  33. data/lib/arel/extensions/delete_statement.rb +20 -15
  34. data/lib/arel/extensions/exists.rb +59 -0
  35. data/lib/arel/extensions/function.rb +3 -2
  36. data/lib/arel/extensions/greatest.rb +17 -3
  37. data/lib/arel/extensions/infer.rb +1 -1
  38. data/lib/arel/extensions/insert_statement.rb +3 -3
  39. data/lib/arel/extensions/least.rb +17 -3
  40. data/lib/arel/extensions/node.rb +10 -0
  41. data/lib/arel/extensions/range_function.rb +10 -2
  42. data/lib/arel/extensions/select_core.rb +22 -7
  43. data/lib/arel/extensions/top.rb +8 -0
  44. data/lib/arel/extensions/tree_manager.rb +5 -0
  45. data/lib/arel/extensions/update_statement.rb +9 -23
  46. data/lib/arel/middleware.rb +5 -1
  47. data/lib/arel/middleware/active_record_extension.rb +13 -0
  48. data/lib/arel/middleware/cache_accessor.rb +35 -0
  49. data/lib/arel/middleware/chain.rb +110 -31
  50. data/lib/arel/middleware/database_executor.rb +77 -0
  51. data/lib/arel/middleware/no_op_cache.rb +9 -0
  52. data/lib/arel/middleware/postgresql_adapter.rb +41 -5
  53. data/lib/arel/middleware/railtie.rb +6 -2
  54. data/lib/arel/middleware/result.rb +170 -0
  55. data/lib/arel/middleware/to_sql_executor.rb +15 -0
  56. data/lib/arel/middleware/to_sql_middleware.rb +33 -0
  57. data/lib/arel/sql_to_arel/pg_query_visitor.rb +34 -33
  58. data/lib/arel/sql_to_arel/result.rb +19 -2
  59. data/lib/arel/transformer.rb +2 -1
  60. data/lib/arel/transformer/prefix_schema_name.rb +183 -0
  61. data/lib/arel/transformer/remove_active_record_info.rb +2 -4
  62. data/lib/arel/transformer/replace_table_with_subquery.rb +31 -0
  63. data/lib/arel_toolkit.rb +7 -1
  64. data/lib/arel_toolkit/version.rb +1 -1
  65. metadata +101 -37
  66. data/.travis.yml +0 -34
  67. data/lib/arel/extensions/generate_series.rb +0 -9
  68. data/lib/arel/extensions/rank.rb +0 -9
  69. data/lib/arel/transformer/add_schema_to_table.rb +0 -26
@@ -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
@@ -6,6 +6,22 @@ module Arel
6
6
  module Nodes
7
7
  class SelectCore < Arel::Nodes::Node
8
8
  attr_accessor :into
9
+ attr_accessor :top
10
+
11
+ private
12
+
13
+ def hash
14
+ [
15
+ @source, @set_quantifier, @projections, @optimizer_hints,
16
+ @wheres, @groups, @havings, @windows, @comment, @top, @into
17
+ ].hash
18
+ end
19
+
20
+ def eql?(other)
21
+ super &&
22
+ top == other.top &&
23
+ into == other.into
24
+ end
9
25
  end
10
26
  end
11
27
 
@@ -14,11 +30,9 @@ module Arel
14
30
  def visit_Arel_Nodes_SelectCore(o, collector)
15
31
  collector << 'SELECT'
16
32
 
17
- collector = maybe_visit o.top, collector
18
-
19
33
  collector = maybe_visit o.set_quantifier, collector
20
34
 
21
- collect_nodes_for o.projections, collector, SPACE
35
+ collect_nodes_for o.projections, collector, ' '
22
36
 
23
37
  maybe_visit o.into, collector
24
38
 
@@ -27,13 +41,13 @@ module Arel
27
41
  collector = visit o.source, collector
28
42
  end
29
43
 
30
- collect_nodes_for o.wheres, collector, WHERE, AND
31
- collect_nodes_for o.groups, collector, GROUP_BY
44
+ collect_nodes_for o.wheres, collector, ' WHERE ', ' AND '
45
+ collect_nodes_for o.groups, collector, ' GROUP BY '
32
46
  unless o.havings.empty?
33
47
  collector << ' HAVING '
34
- inject_join o.havings, collector, AND
48
+ inject_join o.havings, collector, ' AND '
35
49
  end
36
- collect_nodes_for o.windows, collector, WINDOW
50
+ collect_nodes_for o.windows, collector, ' WINDOW '
37
51
 
38
52
  collector
39
53
  end
@@ -45,6 +59,7 @@ module Arel
45
59
  super
46
60
 
47
61
  visit_edge o, 'into'
62
+ visit_edge o, 'top'
48
63
  end
49
64
  end
50
65
 
@@ -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,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
@@ -24,16 +24,17 @@ module Arel
24
24
 
25
25
  module Visitors
26
26
  class ToSql
27
- # rubocop:disable Metrics/CyclomaticComplexity
28
27
  # rubocop:disable Metrics/AbcSize
29
- # rubocop:disable Metrics/PerceivedComplexity
30
28
  def visit_Arel_Nodes_UpdateStatement(o, collector)
31
29
  if o.with
32
30
  collector = visit o.with, collector
33
- collector << SPACE
31
+ collector << ' '
34
32
  end
35
33
 
36
- 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?
37
38
  o.wheres
38
39
  else
39
40
  [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
@@ -41,31 +42,16 @@ module Arel
41
42
 
42
43
  collector << 'UPDATE '
43
44
  collector = visit o.relation, collector
44
- unless o.values.empty?
45
- collector << ' SET '
46
- collector = inject_join o.values, collector, ', '
47
- end
48
-
49
- unless o.froms.empty?
50
- collector << ' FROM '
51
- collector = inject_join o.froms, collector, ', '
52
- end
53
45
 
54
- unless wheres.empty?
55
- collector << ' WHERE '
56
- collector = inject_join wheres, collector, ' AND '
57
- end
46
+ collect_nodes_for o.values, collector, ' SET '
47
+ collect_nodes_for o.froms, collector, ' FROM ', ', '
58
48
 
59
- unless o.returning.empty?
60
- collector << ' RETURNING '
61
- collector = inject_join o.returning, collector, ', '
62
- end
49
+ collect_nodes_for wheres, collector, ' WHERE ', ' AND '
50
+ collect_nodes_for o.returning, collector, ' RETURNING ', ', '
63
51
 
64
52
  collector
65
53
  end
66
54
  # rubocop:enable Metrics/AbcSize
67
- # rubocop:enable Metrics/CyclomaticComplexity
68
- # rubocop:enable Metrics/PerceivedComplexity
69
55
  end
70
56
 
71
57
  class Dot
@@ -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.dup)
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
 
@@ -88,6 +147,26 @@ module Arel
88
147
  ensure
89
148
  Arel::Middleware.current_chain = previous_chain
90
149
  end
150
+
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
169
+ end
91
170
  end
92
171
  end
93
172
  end