arel_toolkit 0.4.2 → 0.4.6

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/coverage.yml +48 -0
  3. data/.github/workflows/test.yml +68 -0
  4. data/.gitignore +3 -1
  5. data/.rubocop.yml +1 -0
  6. data/.ruby-version +1 -1
  7. data/.tool-versions +1 -0
  8. data/Appraisals +4 -0
  9. data/CHANGELOG.md +52 -3
  10. data/Gemfile.lock +129 -81
  11. data/README.md +20 -3
  12. data/arel_toolkit.gemspec +4 -7
  13. data/bin/console +2 -1
  14. data/bin/setup +23 -2
  15. data/docker-compose.yml +11 -0
  16. data/gemfiles/active_record_6.gemfile +7 -0
  17. data/gemfiles/active_record_6.gemfile.lock +212 -0
  18. data/gemfiles/arel_gems.gemfile.lock +11 -10
  19. data/gemfiles/default.gemfile.lock +11 -10
  20. data/lib/arel/enhance/node.rb +18 -12
  21. data/lib/arel/extensions.rb +1 -0
  22. data/lib/arel/extensions/conflict.rb +3 -3
  23. data/lib/arel/extensions/delete_statement.rb +20 -15
  24. data/lib/arel/extensions/function.rb +1 -1
  25. data/lib/arel/extensions/infer.rb +3 -3
  26. data/lib/arel/extensions/insert_statement.rb +4 -4
  27. data/lib/arel/extensions/select_core.rb +21 -7
  28. data/lib/arel/extensions/top.rb +8 -0
  29. data/lib/arel/extensions/transaction.rb +9 -9
  30. data/lib/arel/extensions/update_statement.rb +9 -23
  31. data/lib/arel/middleware/cache_accessor.rb +35 -0
  32. data/lib/arel/middleware/chain.rb +53 -29
  33. data/lib/arel/middleware/database_executor.rb +11 -2
  34. data/lib/arel/middleware/no_op_cache.rb +9 -0
  35. data/lib/arel/sql_to_arel/pg_query_visitor.rb +430 -521
  36. data/lib/arel/sql_to_arel/pg_query_visitor/frame_options.rb +37 -5
  37. data/lib/arel/transformer/prefix_schema_name.rb +5 -3
  38. data/lib/arel_toolkit.rb +1 -0
  39. data/lib/arel_toolkit/version.rb +1 -1
  40. metadata +31 -32
  41. data/.github/workflows/develop.yml +0 -88
  42. data/.github/workflows/master.yml +0 -67
@@ -1,21 +1,15 @@
1
1
  module Arel
2
2
  module Enhance
3
3
  class Node
4
+ attr_reader :local_path
4
5
  attr_reader :object
5
6
  attr_reader :parent
6
- attr_reader :local_path
7
- attr_reader :fields
8
- attr_reader :children
9
7
  attr_reader :root_node
10
- attr_reader :context
11
8
 
12
9
  def initialize(object)
13
10
  @object = object
14
11
  @root_node = self
15
- @fields = []
16
- @children = {}
17
12
  @dirty = false
18
- @context = {}
19
13
  end
20
14
 
21
15
  def inspect
@@ -25,7 +19,7 @@ module Arel
25
19
  def value
26
20
  return unless value?
27
21
 
28
- @fields.first
22
+ fields.first
29
23
  end
30
24
 
31
25
  def each(&block)
@@ -58,7 +52,7 @@ module Arel
58
52
  node.local_path = path_node
59
53
  node.parent = self
60
54
  node.root_node = root_node
61
- @children[path_node.value.to_s] = node
55
+ children[path_node.value.to_s] = node
62
56
  end
63
57
 
64
58
  def to_sql(engine = Table.engine)
@@ -78,19 +72,19 @@ module Arel
78
72
  end
79
73
 
80
74
  def method_missing(name, *args, &block)
81
- child = @children[name.to_s]
75
+ child = children[name.to_s]
82
76
  return super if child.nil?
83
77
 
84
78
  child
85
79
  end
86
80
 
87
81
  def respond_to_missing?(method, include_private = false)
88
- child = @children[method.to_s]
82
+ child = children[method.to_s]
89
83
  child.present? || super
90
84
  end
91
85
 
92
86
  def [](key)
93
- @children.fetch(key.to_s)
87
+ children.fetch(key.to_s)
94
88
  end
95
89
 
96
90
  def child_at_path(path_items)
@@ -118,6 +112,18 @@ module Arel
118
112
  the_path.compact
119
113
  end
120
114
 
115
+ def children
116
+ @children ||= {}
117
+ end
118
+
119
+ def fields
120
+ @fields ||= []
121
+ end
122
+
123
+ def context
124
+ @context ||= {}
125
+ end
126
+
121
127
  protected
122
128
 
123
129
  attr_writer :local_path
@@ -115,6 +115,7 @@ require 'arel/extensions/active_model_attribute_with_cast_value'
115
115
  require 'arel/extensions/exists'
116
116
  require 'arel/extensions/bind_param'
117
117
  require 'arel/extensions/node'
118
+ require 'arel/extensions/top'
118
119
 
119
120
  module Arel
120
121
  module Extensions
@@ -21,12 +21,12 @@ module Arel
21
21
  visit(o.infer, collector) if o.infer
22
22
 
23
23
  case o.action
24
- when 1
24
+ when :ONCONFLICT_NOTHING
25
25
  collector << 'DO NOTHING'
26
- when 2
26
+ when :ONCONFLICT_UPDATE
27
27
  collector << 'DO UPDATE SET '
28
28
  else
29
- raise "Unknown conflict clause `#{action}`"
29
+ raise "Unknown conflict clause `#{o.action}`"
30
30
  end
31
31
 
32
32
  o.values.any? && (inject_join o.values, collector, ', ')
@@ -9,11 +9,14 @@ module Arel
9
9
  attr_accessor :using
10
10
  attr_accessor :with
11
11
  attr_accessor :returning
12
+ attr_accessor :orders
12
13
 
13
14
  def initialize(relation = nil, wheres = [])
14
15
  super
15
16
 
16
17
  @returning = []
18
+ @orders = []
19
+ @using = []
17
20
  end
18
21
  end
19
22
 
@@ -27,27 +30,29 @@ module Arel
27
30
  def visit_Arel_Nodes_DeleteStatement(o, collector)
28
31
  if o.with
29
32
  collector = visit o.with, collector
30
- collector << SPACE
33
+ collector << ' '
31
34
  end
32
35
 
33
- collector << 'DELETE FROM '
34
- collector = visit o.relation, collector
35
-
36
- if o.using
37
- collector << ' USING '
38
- collector = inject_join o.using, collector, ', '
39
- end
36
+ if Gem.loaded_specs['activerecord'].version >= Gem::Version.new('6.0.0')
37
+ o = prepare_delete_statement(o)
40
38
 
41
- if o.wheres.any?
42
- collector << WHERE
43
- collector = inject_join o.wheres, collector, AND
39
+ if has_join_sources?(o)
40
+ collector << 'DELETE '
41
+ visit o.relation.left, collector
42
+ collector << ' FROM '
43
+ else
44
+ collector << 'DELETE FROM '
45
+ end
46
+ else
47
+ collector << 'DELETE FROM '
44
48
  end
45
49
 
46
- unless o.returning.empty?
47
- collector << ' RETURNING '
48
- collector = inject_join o.returning, collector, ', '
49
- end
50
+ collector = visit o.relation, collector
50
51
 
52
+ collect_nodes_for o.using, collector, ' USING ', ', '
53
+ collect_nodes_for o.wheres, collector, ' WHERE ', ' AND '
54
+ collect_nodes_for o.returning, collector, ' RETURNING ', ', '
55
+ collect_nodes_for o.orders, collector, ' ORDER BY '
51
56
  maybe_visit o.limit, collector
52
57
  end
53
58
  # rubocop:enable Metrics/AbcSize
@@ -46,7 +46,7 @@ module Arel
46
46
  end
47
47
 
48
48
  if o.orders.any?
49
- collector << SPACE unless o.within_group
49
+ collector << ' ' unless o.within_group
50
50
  collector << 'ORDER BY '
51
51
  collector = inject_join o.orders, collector, ', '
52
52
  end
@@ -13,13 +13,13 @@ module Arel
13
13
  module Visitors
14
14
  class ToSql
15
15
  def visit_Arel_Nodes_Infer(o, collector)
16
- if o.name
16
+ if o.name.present?
17
17
  collector << 'ON CONSTRAINT '
18
18
  collector << o.left
19
- collector << SPACE
19
+ collector << ' '
20
20
  end
21
21
 
22
- if o.right
22
+ if o.right.present?
23
23
  collector << '('
24
24
  inject_join o.right, collector, ', '
25
25
  collector << ') '
@@ -30,7 +30,7 @@ module Arel
30
30
  def visit_Arel_Nodes_InsertStatement(o, collector)
31
31
  if o.with
32
32
  collector = visit o.with, collector
33
- collector << SPACE
33
+ collector << ' '
34
34
  end
35
35
 
36
36
  collector << 'INSERT INTO '
@@ -42,11 +42,11 @@ module Arel
42
42
  end
43
43
 
44
44
  case o.override
45
- when nil, 0
45
+ when :OVERRIDING_KIND_UNDEFINED, :OVERRIDING_NOT_SET, nil
46
46
  collector << ''
47
- when 1
47
+ when :OVERRIDING_USER_VALUE
48
48
  collector << ' OVERRIDING USER VALUE'
49
- when 2
49
+ when :OVERRIDING_SYSTEM_VALUE
50
50
  collector << ' OVERRIDING SYSTEM VALUE'
51
51
  else
52
52
  raise "Unknown override `#{o.override}`"
@@ -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
@@ -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
@@ -16,21 +16,21 @@ module Arel
16
16
  class ToSql
17
17
  def visit_Arel_Nodes_Transaction(o, collector)
18
18
  case o.type
19
- when 0
19
+ when 1
20
20
  collector << 'BEGIN'
21
- when 2
22
- collector << 'COMMIT'
23
21
  when 3
24
- collector << 'ROLLBACK'
22
+ collector << 'COMMIT'
25
23
  when 4
26
- collector << 'SAVEPOINT '
27
- collector << o.options.join(' ')
24
+ collector << 'ROLLBACK'
28
25
  when 5
29
- collector << 'RELEASE SAVEPOINT '
30
- collector << o.options.join(' ')
26
+ collector << 'SAVEPOINT '
27
+ collector << o.right
31
28
  when 6
29
+ collector << 'RELEASE SAVEPOINT '
30
+ collector << o.right
31
+ when 7
32
32
  collector << 'ROLLBACK TO '
33
- collector << o.options.join(' ')
33
+ collector << o.right
34
34
  else
35
35
  raise "Unknown transaction type `#{o.type}`"
36
36
  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
@@ -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,81 +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
- attr_reader :executing_middleware
7
+ attr_reader :executing_middleware_depth
5
8
  attr_reader :executor
9
+ attr_reader :cache
10
+
11
+ MAX_RECURSION_DEPTH = 10
6
12
 
7
13
  def initialize(
8
14
  internal_middleware = [],
9
15
  internal_context = {},
10
- executor_class = Arel::Middleware::DatabaseExecutor
16
+ executor_class = Arel::Middleware::DatabaseExecutor,
17
+ cache: nil
11
18
  )
12
19
  @internal_middleware = internal_middleware
13
20
  @internal_context = internal_context
14
21
  @executor = executor_class.new(internal_middleware)
15
- @executing_middleware = false
22
+ @executing_middleware_depth = 0
23
+ @cache = cache || NoOpCache
24
+ end
25
+
26
+ def cache_accessor
27
+ @cache_accessor ||= CacheAccessor.new @cache
16
28
  end
17
29
 
18
30
  def execute(sql, binds = [], &execute_sql)
19
31
  return execute_sql.call(sql, binds).to_casted_result if internal_middleware.length.zero?
20
32
 
21
- check_middleware_recursion(sql)
22
-
23
- updated_context = context.merge(original_sql: sql)
24
- enhanced_arel = Arel.enhance(Arel.sql_to_arel(sql, binds: binds))
25
-
26
- result = executor.run(enhanced_arel, updated_context, execute_sql)
33
+ if (cached_sql = cache_accessor.read(sql))
34
+ return execute_sql.call(cached_sql, binds).to_casted_result
35
+ end
27
36
 
28
- result.to_casted_result
37
+ execute_with_middleware(sql, binds, execute_sql).to_casted_result
29
38
  rescue ::PgQuery::ParseError
30
39
  execute_sql.call(sql, binds)
31
40
  ensure
32
- @executing_middleware = false
41
+ @executing_middleware_depth -= 1
33
42
  end
34
43
 
35
44
  def current
36
45
  internal_middleware.dup
37
46
  end
38
47
 
39
- def apply(middleware, &block)
48
+ def apply(middleware, cache: @cache, &block)
40
49
  new_middleware = Array.wrap(middleware)
41
- continue_chain(new_middleware, internal_context, &block)
50
+ continue_chain(new_middleware, internal_context, cache: cache, &block)
42
51
  end
43
52
  alias only apply
44
53
 
45
54
  def none(&block)
46
- continue_chain([], internal_context, &block)
55
+ continue_chain([], internal_context, cache: cache, &block)
47
56
  end
48
57
 
49
- def except(without_middleware, &block)
58
+ def except(without_middleware, cache: @cache, &block)
50
59
  without_middleware = Array.wrap(without_middleware)
51
60
  new_middleware = internal_middleware - without_middleware
52
- continue_chain(new_middleware, internal_context, &block)
61
+ continue_chain(new_middleware, internal_context, cache: cache, &block)
53
62
  end
54
63
 
55
- def insert_before(new_middleware, existing_middleware, &block)
64
+ def insert_before(new_middleware, existing_middleware, cache: @cache, &block)
56
65
  new_middleware = Array.wrap(new_middleware)
57
66
  index = internal_middleware.index(existing_middleware)
58
67
  updated_middleware = internal_middleware.insert(index, *new_middleware)
59
- continue_chain(updated_middleware, internal_context, &block)
68
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
60
69
  end
61
70
 
62
- def prepend(new_middleware, &block)
71
+ def prepend(new_middleware, cache: @cache, &block)
63
72
  new_middleware = Array.wrap(new_middleware)
64
73
  updated_middleware = new_middleware + internal_middleware
65
- continue_chain(updated_middleware, internal_context, &block)
74
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
66
75
  end
67
76
 
68
- def insert_after(new_middleware, existing_middleware, &block)
77
+ def insert_after(new_middleware, existing_middleware, cache: @cache, &block)
69
78
  new_middleware = Array.wrap(new_middleware)
70
79
  index = internal_middleware.index(existing_middleware)
71
80
  updated_middleware = internal_middleware.insert(index + 1, *new_middleware)
72
- continue_chain(updated_middleware, internal_context, &block)
81
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
73
82
  end
74
83
 
75
- def append(new_middleware, &block)
84
+ def append(new_middleware, cache: @cache, &block)
76
85
  new_middleware = Array.wrap(new_middleware)
77
86
  updated_middleware = internal_middleware + new_middleware
78
- continue_chain(updated_middleware, internal_context, &block)
87
+ continue_chain(updated_middleware, internal_context, cache: cache, &block)
79
88
  end
80
89
 
81
90
  def context(new_context = nil, &block)
@@ -85,7 +94,7 @@ module Arel
85
94
 
86
95
  return internal_context if new_context.nil?
87
96
 
88
- continue_chain(internal_middleware, new_context, &block)
97
+ continue_chain(internal_middleware, new_context, cache: @cache, &block)
89
98
  end
90
99
 
91
100
  def to_sql(type, &block)
@@ -109,8 +118,23 @@ module Arel
109
118
 
110
119
  private
111
120
 
112
- def continue_chain(middleware, context, &block)
113
- 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)
114
138
  maybe_execute_block(new_chain, &block)
115
139
  end
116
140
 
@@ -125,7 +149,7 @@ module Arel
125
149
  end
126
150
 
127
151
  def check_middleware_recursion(sql)
128
- if executing_middleware
152
+ if executing_middleware_depth > MAX_RECURSION_DEPTH
129
153
  message = <<~ERROR
130
154
  Middleware is being called from within middleware, aborting execution
131
155
  to prevent endless recursion. You can do the following if you want to execute SQL
@@ -140,7 +164,7 @@ module Arel
140
164
 
141
165
  raise message
142
166
  else
143
- @executing_middleware = true
167
+ @executing_middleware_depth += 1
144
168
  end
145
169
  end
146
170
  end