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,68 @@
1
+ module Arel
2
+ module Middleware
3
+ class DatabaseExecutor
4
+ attr_reader :middleware
5
+
6
+ attr_accessor :index
7
+ attr_reader :context
8
+ attr_reader :final_block
9
+
10
+ def initialize(middleware)
11
+ @middleware = middleware
12
+ end
13
+
14
+ def run(enhanced_arel, context, final_block)
15
+ @index = 0
16
+ @context = context
17
+ @final_block = final_block
18
+
19
+ result = call(enhanced_arel)
20
+ check_return_type result
21
+ result
22
+ ensure
23
+ @index = 0
24
+ @context = nil
25
+ @final_block = nil
26
+ end
27
+
28
+ def call(next_arel)
29
+ check_argument_type next_arel
30
+
31
+ current_middleware = middleware[index]
32
+
33
+ return execute_sql(next_arel) if current_middleware.nil?
34
+
35
+ self.index += 1
36
+
37
+ case current_middleware.method(:call).arity
38
+ when 2
39
+ current_middleware.call(next_arel, self)
40
+ else
41
+ current_middleware.call(next_arel, self, context.dup)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def execute_sql(next_arel)
48
+ sql, binds = next_arel.to_sql_and_binds
49
+ sql_result = final_block.call(sql, binds)
50
+ check_return_type sql_result
51
+ sql_result
52
+ end
53
+
54
+ def check_argument_type(next_arel)
55
+ return if next_arel.is_a?(Arel::Enhance::Node)
56
+
57
+ raise "Only `Arel::Enhance::Node` is valid for middleware, passed `#{next_arel.class}`"
58
+ end
59
+
60
+ def check_return_type(return_object)
61
+ return if return_object.is_a?(Arel::Middleware::Result)
62
+
63
+ raise 'Object returned from middleware needs to be wrapped in `Arel::Middleware::Result`' \
64
+ " for object `#{return_object}`"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -8,18 +8,54 @@ module Arel
8
8
  end
9
9
 
10
10
  def execute(sql, name = nil)
11
- sql = Arel::Middleware.current_chain.execute(sql)
12
11
  super(sql, name)
13
12
  end
14
13
 
14
+ alias parent_execute execute
15
+
16
+ # rubocop:disable Lint/DuplicateMethods
17
+ def execute(sql, name = nil)
18
+ Arel::Middleware.current_chain.execute(sql) do |processed_sql|
19
+ Arel::Middleware::Result.create(
20
+ data: parent_execute(processed_sql, name),
21
+ from: Arel::Middleware::PGResult,
22
+ to: Arel::Middleware::PGResult,
23
+ )
24
+ end
25
+ end
26
+ # rubocop:enable Lint/DuplicateMethods
27
+
28
+ def query(sql, name = nil)
29
+ Arel::Middleware.current_chain.execute(sql) do |processed_sql|
30
+ # NOTE: we're not calling `super` here, but execute.
31
+ # The `query` super does not return the columns, like the other methods.
32
+ # As we want the result objects to be the same, we call execute instead.
33
+ Arel::Middleware::Result.create(
34
+ data: parent_execute(processed_sql, name),
35
+ from: Arel::Middleware::PGResult,
36
+ to: Arel::Middleware::ArrayResult,
37
+ )
38
+ end
39
+ end
40
+
15
41
  def exec_no_cache(sql, name, binds)
16
- sql = Arel::Middleware.current_chain.execute(sql, binds)
17
- super(sql, name, binds)
42
+ Arel::Middleware.current_chain.execute(sql, binds) do |processed_sql, processed_binds|
43
+ Arel::Middleware::Result.create(
44
+ data: super(processed_sql, name, processed_binds),
45
+ from: Arel::Middleware::PGResult,
46
+ to: Arel::Middleware::PGResult,
47
+ )
48
+ end
18
49
  end
19
50
 
20
51
  def exec_cache(sql, name, binds)
21
- sql = Arel::Middleware.current_chain.execute(sql, binds)
22
- super(sql, name, binds)
52
+ Arel::Middleware.current_chain.execute(sql, binds) do |processed_sql, processed_binds|
53
+ Arel::Middleware::Result.create(
54
+ data: super(processed_sql, name, processed_binds),
55
+ from: Arel::Middleware::PGResult,
56
+ to: Arel::Middleware::PGResult,
57
+ )
58
+ end
23
59
  end
24
60
  end
25
61
  end
@@ -4,17 +4,21 @@ module Arel
4
4
  class Railtie < Rails::Railtie
5
5
  initializer 'arel.middleware.insert' do
6
6
  ActiveSupport.on_load :active_record do
7
- Arel::Middleware::Railtie.insert_postgresql
7
+ Arel::Middleware::Railtie.insert
8
8
  end
9
9
  end
10
10
  end
11
11
  end
12
12
 
13
13
  class Railtie
14
- def self.insert_postgresql
14
+ def self.insert
15
15
  ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(
16
16
  Arel::Middleware::PostgreSQLAdapter,
17
17
  )
18
+
19
+ ActiveRecord::Base.singleton_class.prepend(
20
+ Arel::Middleware::ActiveRecordExtension,
21
+ )
18
22
  end
19
23
  end
20
24
  end
@@ -0,0 +1,170 @@
1
+ module Arel
2
+ module Middleware
3
+ class Column
4
+ attr_reader :name
5
+ attr_reader :metadata
6
+
7
+ def initialize(name, metadata)
8
+ @name = name
9
+ @metadata = metadata
10
+ end
11
+ end
12
+
13
+ # Class is very similar to ActiveRecord::Result
14
+ # activerecord/lib/active_record/result.rb
15
+ class Result
16
+ attr_reader :original_data
17
+
18
+ def self.create(from:, to:, data:)
19
+ Result.new from, to, data
20
+ end
21
+
22
+ def initialize(from_caster, to_caster, original_data)
23
+ @from_caster = from_caster
24
+ @to_caster = to_caster
25
+ @original_data = original_data
26
+ @modified = false
27
+ end
28
+
29
+ def columns
30
+ @columns ||= column_objects.map(&:name)
31
+ end
32
+
33
+ def column_objects
34
+ @column_objects ||= from_caster.column_objects(original_data)
35
+ end
36
+
37
+ def rows
38
+ @rows ||= from_caster.rows(original_data)
39
+ end
40
+
41
+ def remove_column(column_name)
42
+ column_index = columns.index(column_name)
43
+ raise "Unknown column `#{column_name}`. Existing columns: `#{columns}`" if column_index.nil?
44
+
45
+ @hash_rows = nil
46
+ @columns = nil
47
+ @modified = true
48
+
49
+ column_objects.delete_at(column_index)
50
+ deleted_rows = []
51
+
52
+ rows.map! do |row|
53
+ deleted_rows << row.delete_at(column_index)
54
+ row
55
+ end
56
+
57
+ deleted_rows
58
+ end
59
+
60
+ def hash_rows
61
+ @hash_rows ||=
62
+ begin
63
+ rows.map do |row|
64
+ hash = {}
65
+
66
+ index = 0
67
+ length = columns.length
68
+
69
+ while index < length
70
+ hash[columns[index]] = row[index]
71
+ index += 1
72
+ end
73
+
74
+ hash
75
+ end
76
+ end
77
+ end
78
+
79
+ def to_casted_result
80
+ to_caster.cast_to(self)
81
+ end
82
+
83
+ def modified?
84
+ @modified
85
+ end
86
+
87
+ private
88
+
89
+ attr_reader :to_caster, :from_caster
90
+ end
91
+
92
+ class PGResult
93
+ class << self
94
+ def column_objects(pg_result)
95
+ pg_result.fields.each_with_index.map do |field, index|
96
+ Column.new(
97
+ field,
98
+ tableid: pg_result.ftable(index),
99
+ columnid: pg_result.ftablecol(index),
100
+ format: pg_result.fformat(index),
101
+ typid: pg_result.ftype(index),
102
+ typlen: pg_result.fsize(index),
103
+ atttypmod: pg_result.fmod(index),
104
+ )
105
+ end
106
+ end
107
+
108
+ def rows(data)
109
+ data.values
110
+ end
111
+
112
+ def cast_to(result)
113
+ return result.original_data unless result.modified?
114
+
115
+ pg_columns = result_to_columns(result)
116
+ conn = ActiveRecord::Base.connection.raw_connection
117
+ new_result = PgResultInit.create(conn, result.original_data, pg_columns, result.rows)
118
+ result.original_data.clear
119
+ new_result
120
+ end
121
+
122
+ private
123
+
124
+ def result_to_columns(result)
125
+ result.column_objects.map do |column|
126
+ {
127
+ name: column.name,
128
+ tableid: column.metadata.fetch(:tableid, 0),
129
+ columnid: column.metadata.fetch(:columnid, 0),
130
+ format: column.metadata.fetch(:format, 0),
131
+ typid: column.metadata.fetch(:typid),
132
+ typlen: column.metadata.fetch(:typlen),
133
+ atttypmod: column.metadata.fetch(:atttypmod, -1),
134
+ }
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ class EmptyPGResult < PGResult
141
+ class << self
142
+ def cast_to(_result)
143
+ ActiveRecord::Base.connection.raw_connection.make_empty_pgresult(2)
144
+ end
145
+ end
146
+ end
147
+
148
+ class ArrayResult
149
+ def self.cast_to(result)
150
+ result.rows
151
+ end
152
+ end
153
+
154
+ class StringResult
155
+ class << self
156
+ def column_objects(_string)
157
+ []
158
+ end
159
+
160
+ def rows(_string)
161
+ []
162
+ end
163
+
164
+ def cast_to(result)
165
+ result.original_data
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,15 @@
1
+ module Arel
2
+ module Middleware
3
+ class ToSqlExecutor < DatabaseExecutor
4
+ private
5
+
6
+ def execute_sql(next_arel)
7
+ Arel::Middleware::Result.create(
8
+ data: next_arel.to_sql,
9
+ from: Arel::Middleware::StringResult,
10
+ to: Arel::Middleware::EmptyPGResult,
11
+ )
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ module Arel
2
+ module Middleware
3
+ class ToSqlMiddleware
4
+ attr_reader :sql, :type, :query_class
5
+
6
+ def initialize(type)
7
+ @sql = []
8
+ @type = type
9
+ @query_class = class_from_type
10
+ end
11
+
12
+ def call(next_arel, next_middleware)
13
+ sql << next_arel.to_sql unless next_arel.query(class: query_class).empty?
14
+ next_middleware.call(next_arel)
15
+ end
16
+
17
+ private
18
+
19
+ def class_from_type
20
+ case type
21
+ when :insert
22
+ Arel::Nodes::InsertStatement
23
+ when :select
24
+ Arel::Nodes::SelectStatement
25
+ when :update
26
+ Arel::Nodes::UpdateStatement
27
+ when :delete
28
+ Arel::Nodes::DeleteStatement
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -26,6 +26,9 @@ module Arel
26
26
  @sql = sql
27
27
 
28
28
  Result.new visit(object, :top)
29
+ rescue ::PgQuery::ParseError => e
30
+ new_error = ::PgQuery::ParseError.new(e.message, __FILE__, __LINE__, -1)
31
+ raise new_error, e.message, e.backtrace
29
32
  rescue ::StandardError => e
30
33
  raise e.class, e.message, e.backtrace if e.is_a?(Arel::SqlToArel::Error)
31
34
 
@@ -192,7 +195,7 @@ module Arel
192
195
  end
193
196
 
194
197
  def visit_Alias(aliasname:)
195
- aliasname
198
+ Arel.sql visit_String(nil, str: aliasname)
196
199
  end
197
200
 
198
201
  def visit_BitString(str:)
@@ -278,22 +281,20 @@ module Arel
278
281
  end
279
282
 
280
283
  def visit_ColumnRef(fields:)
281
- case fields.length
282
- when 1
283
- visited_field = visit(fields[0], :operator)
284
- Arel::Nodes::UnqualifiedColumn.new Arel::Attribute.new(nil, visited_field)
284
+ fields = fields.reverse
285
+ column = visit(fields[0], :operator)
286
+ table = visit(fields[1], :operator) if fields[1]
287
+ schema_name = visit(fields[2], :operator) if fields[2]
288
+ database = visit(fields[3], :operator) if fields[3]
285
289
 
286
- when 2
287
- table_reference, column_reference = fields
288
- table_reference = visit(table_reference, :operator)
289
- table = Arel::Table.new(table_reference)
290
+ table = Arel::Table.new(table) if table
291
+ attribute = Arel::Attribute.new(table, column)
292
+ attribute.schema_name = schema_name
293
+ attribute.database = database
290
294
 
291
- column_reference = visit(column_reference, :operator)
292
- table[column_reference]
295
+ return attribute if table
293
296
 
294
- else
295
- raise 'Can\'t deal with column refs that have more than 2 fields'
296
- end
297
+ Arel::Nodes::UnqualifiedColumn.new Arel::Attribute.new(nil, column)
297
298
  end
298
299
 
299
300
  def visit_CommonTableExpr(ctename:, ctequery:)
@@ -364,15 +365,9 @@ module Arel
364
365
  when ['sum']
365
366
  Arel::Nodes::Sum.new args
366
367
 
367
- when ['rank']
368
- Arel::Nodes::Rank.new args
369
-
370
368
  when ['count']
371
369
  Arel::Nodes::Count.new args
372
370
 
373
- when ['generate_series']
374
- Arel::Nodes::GenerateSeries.new args
375
-
376
371
  when ['max']
377
372
  Arel::Nodes::Max.new args
378
373
 
@@ -429,10 +424,6 @@ module Arel
429
424
  else
430
425
  case function_names.length
431
426
  when 2
432
- if function_names.first == PG_CATALOG
433
- boom "Missing postgres function `#{function_names.last}`"
434
- end
435
-
436
427
  func = Arel::Nodes::NamedFunction.new(function_names.last, args)
437
428
  func.schema_name = function_names.first
438
429
  func
@@ -602,15 +593,19 @@ module Arel
602
593
  conflict
603
594
  end
604
595
 
605
- def visit_ParamRef(number:)
596
+ def visit_ParamRef(number: nil)
606
597
  value = (binds[number - 1] unless binds.empty?)
607
598
 
608
599
  Arel::Nodes::BindParam.new(value)
609
600
  end
610
601
 
611
- def visit_RangeFunction(is_rowsfrom:, functions:, lateral: false, ordinality: false)
612
- boom 'https://github.com/mvgijssel/arel_toolkit/issues/36' unless is_rowsfrom == true
613
-
602
+ def visit_RangeFunction(
603
+ is_rowsfrom: nil,
604
+ functions:,
605
+ lateral: false,
606
+ ordinality: false,
607
+ aliaz: nil
608
+ )
614
609
  functions = functions.map do |function_array|
615
610
  function, empty_value = function_array
616
611
  boom 'https://github.com/mvgijssel/arel_toolkit/issues/37' unless empty_value.nil?
@@ -618,15 +613,16 @@ module Arel
618
613
  visit(function)
619
614
  end
620
615
 
621
- node = Arel::Nodes::RangeFunction.new functions
616
+ node = Arel::Nodes::RangeFunction.new functions, is_rowsfrom: is_rowsfrom
622
617
  node = lateral ? Arel::Nodes::Lateral.new(node) : node
623
- ordinality ? Arel::Nodes::WithOrdinality.new(node) : node
618
+ node = ordinality ? Arel::Nodes::WithOrdinality.new(node) : node
619
+ aliaz.nil? ? node : Arel::Nodes::As.new(node, visit(aliaz))
624
620
  end
625
621
 
626
622
  def visit_RangeSubselect(aliaz:, subquery:, lateral: false)
627
623
  aliaz = visit(aliaz)
628
624
  subquery = visit(subquery)
629
- node = Arel::Nodes::TableAlias.new(Arel::Nodes::Grouping.new(subquery), aliaz)
625
+ node = Arel::Nodes::As.new(Arel::Nodes::Grouping.new(subquery), aliaz)
630
626
  lateral ? Arel::Nodes::Lateral.new(node) : node
631
627
  end
632
628
 
@@ -650,7 +646,8 @@ module Arel
650
646
  val = visit(val)
651
647
 
652
648
  if name
653
- Arel::Nodes::As.new(val, Arel.sql(name))
649
+ aliaz = visit_Alias(aliasname: name)
650
+ Arel::Nodes::As.new(val, aliaz)
654
651
  else
655
652
  val
656
653
  end
@@ -722,6 +719,7 @@ module Arel
722
719
  select_core.havings = [visit(having_clause)] if having_clause
723
720
  select_core.windows = visit(window_clause) if window_clause
724
721
  select_core.into = visit(into_clause) if into_clause
722
+ select_core.top = ::Arel::Nodes::Top.new visit(limit_count) if limit_count
725
723
 
726
724
  if distinct_clause == [nil]
727
725
  select_core.set_quantifier = Arel::Nodes::Distinct.new
@@ -950,8 +948,11 @@ module Arel
950
948
  start_offset: nil,
951
949
  end_offset: nil
952
950
  )
953
- instance = name.nil? ? Arel::Nodes::Window.new : Arel::Nodes::NamedWindow.new(name)
951
+ if name.present? && partition_clause.empty? && order_clause.empty?
952
+ return Arel::Nodes::SqlLiteral.new(name)
953
+ end
954
954
 
955
+ instance = name.nil? ? Arel::Nodes::Window.new : Arel::Nodes::NamedWindow.new(name)
955
956
  instance.tap do |window|
956
957
  window.orders = visit order_clause
957
958
  window.partitions = visit partition_clause