arel_toolkit 0.2.0 → 0.4.3

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 (150) 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 -2
  7. data/Appraisals +13 -0
  8. data/CHANGELOG.md +132 -6
  9. data/Gemfile +5 -0
  10. data/Gemfile.lock +92 -12
  11. data/Guardfile +23 -12
  12. data/README.md +104 -6
  13. data/Rakefile +18 -0
  14. data/arel_toolkit.gemspec +19 -4
  15. data/benchmark.rb +54 -0
  16. data/bin/console +1 -0
  17. data/ext/pg_result_init/extconf.rb +52 -0
  18. data/ext/pg_result_init/pg_result_init.c +138 -0
  19. data/ext/pg_result_init/pg_result_init.h +6 -0
  20. data/gemfiles/active_record_6.gemfile +7 -0
  21. data/gemfiles/active_record_6.gemfile.lock +210 -0
  22. data/gemfiles/arel_gems.gemfile +10 -0
  23. data/gemfiles/arel_gems.gemfile.lock +284 -0
  24. data/gemfiles/default.gemfile +5 -0
  25. data/gemfiles/default.gemfile.lock +208 -0
  26. data/lib/arel/enhance.rb +17 -0
  27. data/lib/arel/enhance/context_enhancer/arel_table.rb +92 -0
  28. data/lib/arel/enhance/node.rb +232 -0
  29. data/lib/arel/enhance/path.rb +38 -0
  30. data/lib/arel/enhance/path_node.rb +26 -0
  31. data/lib/arel/enhance/query.rb +38 -0
  32. data/lib/arel/enhance/query_methods.rb +23 -0
  33. data/lib/arel/enhance/visitor.rb +97 -0
  34. data/lib/arel/extensions.rb +55 -3
  35. data/lib/arel/extensions/active_model_attribute_with_cast_value.rb +22 -0
  36. data/lib/arel/extensions/active_record_relation_query_attribute.rb +22 -0
  37. data/lib/arel/extensions/active_record_type_caster_connection.rb +7 -0
  38. data/lib/arel/extensions/active_record_type_caster_map.rb +7 -0
  39. data/lib/arel/extensions/array.rb +2 -9
  40. data/lib/arel/extensions/assignment.rb +22 -0
  41. data/lib/arel/extensions/at_time_zone.rb +37 -0
  42. data/lib/arel/extensions/attributes_attribute.rb +47 -0
  43. data/lib/arel/extensions/binary.rb +7 -0
  44. data/lib/arel/extensions/bind_param.rb +15 -0
  45. data/lib/arel/extensions/bit_string.rb +2 -9
  46. data/lib/arel/extensions/case.rb +17 -0
  47. data/lib/arel/extensions/coalesce.rb +17 -3
  48. data/lib/arel/extensions/conflict.rb +9 -0
  49. data/lib/arel/extensions/contained_within_equals.rb +10 -0
  50. data/lib/arel/extensions/contains.rb +27 -5
  51. data/lib/arel/extensions/contains_equals.rb +10 -0
  52. data/lib/arel/extensions/current_catalog.rb +4 -0
  53. data/lib/arel/extensions/current_date.rb +4 -0
  54. data/lib/arel/extensions/current_of_expression.rb +2 -9
  55. data/lib/arel/extensions/current_role.rb +4 -0
  56. data/lib/arel/extensions/current_row.rb +7 -0
  57. data/lib/arel/extensions/current_schema.rb +4 -0
  58. data/lib/arel/extensions/current_user.rb +4 -0
  59. data/lib/arel/extensions/dealocate.rb +31 -0
  60. data/lib/arel/extensions/default_values.rb +4 -0
  61. data/lib/arel/extensions/delete_manager.rb +25 -0
  62. data/lib/arel/extensions/delete_statement.rb +32 -8
  63. data/lib/arel/extensions/distinct_from.rb +3 -16
  64. data/lib/arel/extensions/dot.rb +11 -0
  65. data/lib/arel/extensions/equality.rb +2 -4
  66. data/lib/arel/extensions/exists.rb +59 -0
  67. data/lib/arel/extensions/extract_from.rb +25 -0
  68. data/lib/arel/extensions/factorial.rb +10 -2
  69. data/lib/arel/extensions/false.rb +7 -0
  70. data/lib/arel/extensions/function.rb +44 -14
  71. data/lib/arel/extensions/greatest.rb +17 -3
  72. data/lib/arel/extensions/indirection.rb +3 -12
  73. data/lib/arel/extensions/infer.rb +7 -7
  74. data/lib/arel/extensions/infix_operation.rb +17 -0
  75. data/lib/arel/extensions/insert_manager.rb +21 -0
  76. data/lib/arel/extensions/insert_statement.rb +35 -9
  77. data/lib/arel/extensions/into.rb +21 -0
  78. data/lib/arel/extensions/json_get_field.rb +10 -0
  79. data/lib/arel/extensions/json_get_object.rb +10 -0
  80. data/lib/arel/extensions/json_path_get_field.rb +10 -0
  81. data/lib/arel/extensions/json_path_get_object.rb +10 -0
  82. data/lib/arel/extensions/jsonb_all_key_exists.rb +10 -0
  83. data/lib/arel/extensions/jsonb_any_key_exists.rb +10 -0
  84. data/lib/arel/extensions/jsonb_key_exists.rb +10 -0
  85. data/lib/arel/extensions/least.rb +17 -3
  86. data/lib/arel/extensions/named_argument.rb +24 -0
  87. data/lib/arel/extensions/named_function.rb +7 -0
  88. data/lib/arel/extensions/node.rb +10 -0
  89. data/lib/arel/extensions/not_distinct_from.rb +3 -16
  90. data/lib/arel/extensions/not_equal.rb +2 -4
  91. data/lib/arel/extensions/ordering.rb +21 -6
  92. data/lib/arel/extensions/overlap.rb +1 -1
  93. data/lib/arel/extensions/overlaps.rb +49 -0
  94. data/lib/arel/extensions/overlay.rb +53 -0
  95. data/lib/arel/extensions/position.rb +27 -0
  96. data/lib/arel/extensions/prepare.rb +39 -0
  97. data/lib/arel/extensions/range_function.rb +10 -2
  98. data/lib/arel/extensions/row.rb +3 -8
  99. data/lib/arel/extensions/select_core.rb +73 -0
  100. data/lib/arel/extensions/select_manager.rb +25 -0
  101. data/lib/arel/extensions/select_statement.rb +31 -9
  102. data/lib/arel/extensions/session_user.rb +4 -0
  103. data/lib/arel/extensions/set_to_default.rb +4 -0
  104. data/lib/arel/extensions/substring.rb +46 -0
  105. data/lib/arel/extensions/table.rb +43 -10
  106. data/lib/arel/extensions/time_with_precision.rb +6 -0
  107. data/lib/arel/extensions/to_sql.rb +27 -0
  108. data/lib/arel/extensions/top.rb +8 -0
  109. data/lib/arel/extensions/transaction.rb +45 -0
  110. data/lib/arel/extensions/tree_manager.rb +15 -0
  111. data/lib/arel/extensions/trim.rb +44 -0
  112. data/lib/arel/extensions/true.rb +7 -0
  113. data/lib/arel/extensions/type_cast.rb +11 -0
  114. data/lib/arel/extensions/unary.rb +7 -0
  115. data/lib/arel/extensions/unary_operation.rb +16 -0
  116. data/lib/arel/extensions/unknown.rb +4 -0
  117. data/lib/arel/extensions/update_manager.rb +25 -0
  118. data/lib/arel/extensions/update_statement.rb +31 -6
  119. data/lib/arel/extensions/user.rb +4 -0
  120. data/lib/arel/extensions/values_list.rb +15 -0
  121. data/lib/arel/extensions/variable_set.rb +55 -0
  122. data/lib/arel/extensions/variable_show.rb +26 -0
  123. data/lib/arel/middleware.rb +27 -0
  124. data/lib/arel/middleware/active_record_extension.rb +13 -0
  125. data/lib/arel/middleware/cache_accessor.rb +35 -0
  126. data/lib/arel/middleware/chain.rb +172 -0
  127. data/lib/arel/middleware/database_executor.rb +77 -0
  128. data/lib/arel/middleware/no_op_cache.rb +9 -0
  129. data/lib/arel/middleware/postgresql_adapter.rb +62 -0
  130. data/lib/arel/middleware/railtie.rb +25 -0
  131. data/lib/arel/middleware/result.rb +170 -0
  132. data/lib/arel/middleware/to_sql_executor.rb +15 -0
  133. data/lib/arel/middleware/to_sql_middleware.rb +33 -0
  134. data/lib/arel/sql_to_arel.rb +8 -4
  135. data/lib/arel/sql_to_arel/error.rb +6 -0
  136. data/lib/arel/sql_to_arel/pg_query_visitor.rb +324 -76
  137. data/lib/arel/sql_to_arel/pg_query_visitor/frame_options.rb +112 -0
  138. data/lib/arel/sql_to_arel/result.rb +30 -0
  139. data/lib/arel/transformer.rb +8 -0
  140. data/lib/arel/transformer/prefix_schema_name.rb +183 -0
  141. data/lib/arel/transformer/remove_active_record_info.rb +40 -0
  142. data/lib/arel/transformer/replace_table_with_subquery.rb +31 -0
  143. data/lib/arel_toolkit.rb +16 -1
  144. data/lib/arel_toolkit/version.rb +1 -1
  145. metadata +278 -25
  146. data/.travis.yml +0 -21
  147. data/lib/arel/extensions/generate_series.rb +0 -9
  148. data/lib/arel/extensions/rank.rb +0 -9
  149. data/lib/arel/sql_to_arel/frame_options.rb +0 -110
  150. data/lib/arel/sql_to_arel/unbound_column_reference.rb +0 -5
data/Guardfile CHANGED
@@ -24,19 +24,30 @@
24
24
  # * zeus: 'zeus rspec' (requires the server to be started separately)
25
25
  # * 'just' rspec: 'rspec'
26
26
 
27
- guard :rspec, cmd: 'bundle exec rspec', failed_mode: :focus do
28
- require 'guard/rspec/dsl'
29
- dsl = Guard::RSpec::Dsl.new(self)
27
+ group :red_green_refactor, halt_on_fail: true do
28
+ guard :rspec, cmd: 'bundle exec rspec', failed_mode: :focus do
29
+ require 'guard/rspec/dsl'
30
+ dsl = Guard::RSpec::Dsl.new(self)
30
31
 
31
- # Feel free to open issues for suggestions and improvements
32
+ # Feel free to open issues for suggestions and improvements
32
33
 
33
- # RSpec files
34
- rspec = dsl.rspec
35
- watch(rspec.spec_helper) { rspec.spec_dir }
36
- watch(rspec.spec_support) { rspec.spec_dir }
37
- watch(rspec.spec_files)
34
+ # RSpec files
35
+ rspec = dsl.rspec
36
+ watch(rspec.spec_helper) { rspec.spec_dir }
37
+ watch(rspec.spec_support) { rspec.spec_dir }
38
+ watch(rspec.spec_files)
38
39
 
39
- # Ruby files
40
- ruby = dsl.ruby
41
- dsl.watch_spec_files_for(ruby.lib_files)
40
+ # Ruby files
41
+ ruby = dsl.ruby
42
+ dsl.watch_spec_files_for(ruby.lib_files)
43
+ end
44
+
45
+ guard :rubocop do
46
+ watch(/.+\.rb$/)
47
+ watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
48
+ end
49
+ end
50
+
51
+ guard 'rake', task: 'compile' do
52
+ watch(/^ext/)
42
53
  end
data/README.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- - [![Build Status](https://travis-ci.com/mvgijssel/arel_toolkit.svg?branch=master)](https://travis-ci.com/mvgijssel/arel_toolkit)
5
+ - [![](https://github.com/mvgijssel/arel_toolkit/workflows/CI%20-%20master/badge.svg)](https://github.com/mvgijssel/arel_toolkit/actions)
6
6
  - [![Maintainability](https://api.codeclimate.com/v1/badges/3ef13d1649a00a98562d/maintainability)](https://codeclimate.com/github/mvgijssel/arel_toolkit/maintainability)
7
7
  - [![Test Coverage](https://api.codeclimate.com/v1/badges/3ef13d1649a00a98562d/test_coverage)](https://codeclimate.com/github/mvgijssel/arel_toolkit/test_coverage)
8
8
  - [![Gem Version](https://badge.fury.io/rb/arel_toolkit.svg)](https://badge.fury.io/rb/arel_toolkit)
9
9
  - [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  - ![](http://ruby-gem-downloads-badge.herokuapp.com/arel_toolkit?type=total)
11
11
  - ![](http://ruby-gem-downloads-badge.herokuapp.com/arel_toolkit?label=downloads-current-version)
12
+ - [Coverage report](https://mvgijssel.github.io/arel_toolkit/)
12
13
 
13
14
  ## Installation
14
15
 
@@ -26,9 +27,9 @@ Or install it yourself as:
26
27
 
27
28
  $ gem install arel_toolkit
28
29
 
29
- ## sql_to_arel
30
+ ## Sql to Arel
30
31
 
31
- Convert your (PostgreSQL) SQL into Arel.
32
+ Convert your (PostgreSQL) SQL into an Arel AST.
32
33
 
33
34
  ```ruby
34
35
  [1] > sql = 'SELECT id FROM users'
@@ -39,9 +40,106 @@ Convert your (PostgreSQL) SQL into Arel.
39
40
  => "SELECT \"id\" FROM \"users\""
40
41
  ```
41
42
 
43
+ ## Enhanced Arel AST
44
+
45
+ `Arel.enhance(arel)` adds additional information and helper methods to the existing Arel AST. This allows for mutating the AST, adding contextual information to the AST and querying for nodes. Some examples:
46
+
47
+ ##### Query for Arel nodes with certain properties
48
+ ```ruby
49
+ arel = Post.select(:id, :public).where(id: 1).arel
50
+ enhanced_arel = Arel.enhance(arel)
51
+ enhanced_arel.query(class: Arel::Table).each { ... }
52
+ ```
53
+
54
+ ##### Query for Arel nodes with an enhanced context
55
+ An `Arel::Table` is used in multiple different places inside the AST, and those locations will give the `Arel::Table` a different meaning. Used within a projection (_column_reference_) like `SELECT posts.id` has a different meaning than within a from `SELECT * FROM posts` (_range_variable_). The following example results in `Arel::Table` nodes where the object is used in the context of referencing a column:
56
+
57
+ ```ruby
58
+ enhanced_arel.query(class: Arel::Table, context: { column_reference: true }).each { ... }
59
+ ```
60
+
61
+ ##### Get an Arel node at a certain path
62
+ ```ruby
63
+ enhanced_arel.child_at_path(['ast', 'cores', 0, 'projections', 1]).object
64
+ => #<struct Arel::Attributes::Attribute>
65
+ ```
66
+
67
+ ##### Replace or remove nodes without modifying the original arel
68
+ `remove` and `replace` allow for modifications to the Arel AST. The changes are aplied to a new copy of the AST, making sure the original AST is not touched. The `replace` method accepts any Arel node or a precomputed enhanced node for improved performance.
69
+
70
+ ```ruby
71
+ enhanced_arel.child_at_path(['ast', 'cores', 0, 'projections', 1]).replace(Post.arel_table[:content])
72
+ enhanced_arel.child_at_path(['ast', 'cores', 0, 'projections', 0]).remove
73
+ enhanced_arel.to_sql
74
+ => SELECT "posts"."content" FROM "posts" WHERE "posts"."id" = $1
75
+ ```
76
+
77
+
78
+ ## Middleware
79
+
80
+ Creating Arel from SQL and enhancing Arel is just the beginning, where this gem really shines is the ability to modify Arel ASTs using middleware.
81
+
82
+ Middleware sits between ActiveRecord and the database, it allows you to alter the Arel (the SQL query) before it's send to the database. Multiple middlewares are supported by passing the results from a finished middleware to the next. Next to the arel object, a context object is used that acts as a intermediate storage between middlewares.
83
+
84
+ The middleware works out of the box in combination with Rails. If using ActiveRecord standalone you need to run the following **after** setting up the database connection:
85
+
86
+ ```ruby
87
+ Arel::Middleware::Railtie.insert
88
+ ```
89
+
90
+ ### Example
91
+
92
+ Create middleware which can be any Ruby object as long as it responds to `call`. Middleware accepts 2 or 3 arguments, context is optional. Calling `.call` on `next_middleware` invokes the next middleware in the chain, returning the response from the database.
93
+
94
+ In this example, we're creating a middleware that will reorder any query. Next to reordering, we're adding an additional middleware that prints out the result of the reorder middleware.
95
+
96
+ ```ruby
97
+ class ReorderMiddleware
98
+ def self.call(arel, next_middleware)
99
+ enhanced_arel = Arel.enhance(arel)
100
+ enhanced_arel.query(class: Arel::Nodes::SelectStatement).each do |node|
101
+ arel_table = node.child_at_path(['cores', 0, 'source', 'left']).object
102
+ node['orders'].replace([arel_table[:id].asc])
103
+ end
104
+
105
+ new_arel = arel.order(Post.arel_table[:id].asc)
106
+ next_middleware.call(new_arel)
107
+ end
108
+ end
109
+
110
+ class LoggingMiddleware
111
+ def self.call(arel, next_middleware, context)
112
+ puts "User executing query: `#{context[:current_user_id]}`"
113
+ puts "Original SQL: `#{context[:original_sql]}`"
114
+ puts "Modified SQL: `#{arel.to_sql}`"
115
+
116
+ next_middleware.call(arel)
117
+ end
118
+ end
119
+ ```
120
+
121
+ Now that we've defined our middelwares, it's time to see them in action:
122
+
123
+ ```ruby
124
+ [1] > Arel.middleware.apply([ReorderMiddleware, LoggingMiddleware]).context(current_user_id: 1) { Post.all.load }
125
+ User executing query: `1`
126
+ Original SQL: `SELECT "posts".* FROM "posts"`
127
+ Modified SQL: `SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC`
128
+ Post Load (4.1ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC
129
+ => []
130
+ ```
131
+
132
+ This gem ships with a couple of middelware methods that allow you to fine-tune what and when to apply middelware.
133
+ - `Arel.middleware.apply([SomeMiddleware]) { ... }`
134
+ - `Arel.middleware.only([OnlyMe]) { ... }`
135
+ - `Arel.middleware.none { ... }`
136
+ - `Arel.middleware.except(RemoveMe) { ... }`
137
+ - `Arel.middleware.insert_before(RunBefore, ThisMiddleware) { ... }`
138
+ - `Arel.middleware.insert_after(RunAfter, ThisMiddleware) { ... }`
139
+
42
140
  ## Extensions
43
141
 
44
- Adds some missing Arel nodes and extends the existing visitors.
142
+ This gem aims to have full support for PostgreSQL's SQL. In order to do so, it needs to add missing Arel nodes and extends the existing visitors. A full list of extensions on Arel can be found here: [lib/arel/extensions](https://github.com/mvgijssel/arel_toolkit/tree/master/lib/arel/extensions).
45
143
 
46
144
  ## Development
47
145
 
@@ -51,7 +149,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
51
149
 
52
150
  ## Contributing
53
151
 
54
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/arel_toolkit. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
152
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mvgijssel/arel_toolkit. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
55
153
 
56
154
  ## License
57
155
 
@@ -59,4 +157,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
59
157
 
60
158
  ## Code of Conduct
61
159
 
62
- Everyone interacting in the ArelToolkit project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/arel_toolkit/blob/master/CODE_OF_CONDUCT.md).
160
+ Everyone interacting in the ArelToolkit project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/mvgijssel/arel_toolkit/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,6 +1,24 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'github_changelog_generator/task'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
6
7
  task default: :spec
8
+
9
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
10
+ config.user = 'mvgijssel'
11
+ config.project = 'arel_toolkit'
12
+ config.future_release = "v#{ArelToolkit::VERSION}"
13
+ config.add_pr_wo_labels = false
14
+ config.enhancement_labels = %w[enhancement dependencies]
15
+ config.exclude_tags = ['v0.1.0']
16
+ end
17
+
18
+ require 'rake/extensiontask'
19
+
20
+ task build: :compile
21
+
22
+ Rake::ExtensionTask.new('pg_result_init') do |ext|
23
+ ext.lib_dir = 'lib/arel_toolkit'
24
+ end
@@ -23,22 +23,37 @@ Gem::Specification.new do |spec|
23
23
  spec.bindir = 'exe'
24
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ['lib']
26
+ spec.extensions = ['ext/pg_result_init/extconf.rb']
26
27
 
27
- spec.add_dependency 'arel', '~> 9.0.0'
28
- spec.add_dependency 'pg_query', '~> 1.1.0'
28
+ spec.add_dependency 'activerecord'
29
+ spec.add_dependency 'pg', '~> 1.1.4'
30
+ spec.add_dependency 'pg_query', '~> 1.2.0'
29
31
 
30
32
  spec.add_development_dependency 'bundler', '~> 2.0'
31
- spec.add_development_dependency 'rake', '~> 10.0'
33
+ spec.add_development_dependency 'dpl', '~> 1.10.11'
34
+ spec.add_development_dependency 'github_changelog_generator', '~> 1.15'
35
+ spec.add_development_dependency 'rake', '~> 13.0'
36
+ spec.add_development_dependency 'rake-compiler', '~> 1.0'
32
37
  spec.add_development_dependency 'rspec', '~> 3.8'
38
+ spec.add_development_dependency 'approvals', '~> 0.0.24'
39
+ spec.add_development_dependency 'appraisal', '~> 2.2.0'
40
+ spec.add_development_dependency 'database_cleaner', '~> 1.7.0'
33
41
  spec.add_development_dependency 'simplecov', '~> 0.16.1'
34
42
  spec.add_development_dependency 'simplecov-console', '~> 0.4.2'
35
43
 
36
- spec.add_development_dependency 'rubocop', '~> 0.69'
44
+ # When updating also update .codeclimate.yml:5
45
+ spec.add_development_dependency 'rubocop', '= 0.71.0'
37
46
  spec.add_development_dependency 'guard', '~> 2.15'
38
47
  spec.add_development_dependency 'guard-rspec', '~> 4.7'
48
+ spec.add_development_dependency 'guard-rubocop', '~> 1.3.0'
49
+ spec.add_development_dependency 'guard-rake', '~> 1.0.0'
50
+
51
+ spec.add_development_dependency 'stackprof', '~> 0.2'
52
+ spec.add_development_dependency 'memory_profiler', '~> 0.9'
39
53
 
40
54
  spec.add_development_dependency 'pry'
41
55
  spec.add_development_dependency 'pry-nav'
56
+ spec.add_development_dependency 'pry-doc'
42
57
  spec.add_development_dependency 'pry-rescue'
43
58
  spec.add_development_dependency 'pry-stack_explorer'
44
59
  spec.add_development_dependency 'pry-alias'
@@ -0,0 +1,54 @@
1
+ GC.disable
2
+
3
+ require 'arel'
4
+ require 'active_record'
5
+ require 'memory_profiler'
6
+ require 'json'
7
+ require 'stackprof'
8
+
9
+ require './lib/arel/enhance'
10
+ require './lib/arel/extensions'
11
+ require './lib/arel/sql_to_arel'
12
+
13
+ ActiveRecord::Base.establish_connection(
14
+ adapter: 'postgresql',
15
+ host: 'localhost',
16
+ databse: 'arel_toolkit_test',
17
+ username: 'postgres',
18
+ )
19
+
20
+ # rubocop:disable Metrics/LineLength
21
+ SQL = %{
22
+ SELECT COUNT("hacktivity_items"."id") FROM (SELECT "hacktivity_items".* FROM (SELECT "hacktivity_items".* FROM (SELECT "hacktivity_items".*, (NULLIF(current_setting('secure.requester_id'), ''))::integer AS "_requester_id" FROM (SELECT "outer_hacktivity_items".*, EXISTS (SELECT 1 FROM "public"."reports" INNER JOIN "public"."teams" ON "teams"."id" = "reports"."team_id" WHERE "teams"."state" = 5 AND "reports"."disclosed_at" IS NOT NULL AND "reports"."public_view" = 'full' AND "reports"."id" = "outer_hacktivity_items"."report_id") AS "is_someone_visiting_a_public_report", EXISTS (SELECT 1 FROM "public"."reports" INNER JOIN "public"."teams" ON "teams"."id" = "reports"."team_id" INNER JOIN "public"."whitelisted_reporters" ON "whitelisted_reporters"."team_id" = "teams"."id" WHERE "teams" ."state" = 4 AND "teams"."allows_private_disclosure" = 't'::bool AND (("whitelisted_reporters"."user_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "whitelisted_reporters"."user_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "reports"."disclosed_at" IS NOT NULL AND "reports"."public_view" = 'full' AND "reports"."id" = "outer_hacktivity_items"."report_id") AS "is_someone_visiting_a_report_privately_disclosed_to_them", EXISTS (SELECT 1 FROM "public"."reports" WHERE "reports"."hacker_published" = 't'::bool AND "reports"."disclosed_at" IS NOT NULL AND "reports"."id" = "outer_hacktivity_items"."report_id") AS "is_hacker_published", EXISTS (SELECT 1 FROM "public"."teams" INNER JOIN "public"."team_members" ON "team_members"."team_id" = "teams"."id" WHERE (("team_members"."user_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "team_members"."user_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "team_members"."user_id" IS NOT NULL AND "teams"."id" = "outer_hacktivity_items"."team_id") AS "is_team_team_member", EXISTS (SELECT 1 FROM "public"."teams" WHERE "teams"."state" = 5 AND "teams"."id" = "outer_hacktivity_items"."team_id") AS "is_team_public", EXISTS (SELECT 1 FROM "public"."teams" INNER JOIN "public"."whitelisted_reporters" ON "whitelisted_reporters"."team_id" = "teams"."id" INNER JOIN "public"."users" ON "users"."id" = "whitelisted_reporters"."user_id" WHERE (("users"."id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "users"."id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "users"."id" IS NOT NULL AND "teams"."id" = "outer_hacktivity_items"."team_id") AS "is_team_whitelisted_reporter", EXISTS (SELECT 1 FROM "public"."teams" WHERE "teams"."state" = 4 AND "teams"."id" = "outer_hacktivity_items"."team_id") AS "is_team_private", EXISTS (SELECT 1 FROM "public"."teams" WHERE "teams"."offers_bounties" = 't'::bool AND "teams"."hide_bounty_amounts" = 'f'::bool AND "teams"."id" = "outer_hacktivity_items"."team_id") AS "is_team_shows_bounty_amounts" FROM "public"."hacktivity_items" "outer_hacktivity_items") AS "hacktivity_items" WHERE "hacktivity_items"."is_team_public" = 't'::bool OR ("hacktivity_items"."is_team_private" = 't'::bool AND ("hacktivity_items"."is_team_team_member" = 't'::bool OR "hacktivity_items"."is_team_whitelisted_reporter" = 't'::bool)) OR "hacktivity_items"."is_hacker_published" = 't'::bool) hacktivity_items) AS "hacktivity_items" LEFT OUTER JOIN (SELECT "reports".*, "new_j0"."disclosed_at" AS "__new_filterable_disclosed_at" FROM (SELECT "reports".*, (NULLIF(current_setting('secure.requester_id'), ''))::integer AS "_requester_id" FROM (SELECT "outer_reports".*, EXISTS (SELECT 1 FROM "public"."teams" INNER JOIN "public"."team_members" ON "team_members"."team_id" = "teams"."id" INNER JOIN "public"."users" ON "users"."id" = "team_members"."user_id" WHERE (("users"."id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "users"."id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "users"."id" IS NOT NULL AND "teams"."id" = "outer_reports"."team_id") AS "is_team_member", (("outer_reports"."reporter_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "outer_reports"."reporter_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "outer_reports"."reporter_id" IS NOT NULL AS "is_reporter", EXISTS (SELECT 1 FROM "public"."reports_users" INNER JOIN "public"."users" ON "users"."id" = "reports_users"."user_id" WHERE (("reports_users"."user_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "reports_users"."user_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "reports_users"."user_id" IS NOT NULL AND "reports_users"."report_id" = "outer_reports"."id") AS "is_external_user", EXISTS (SELECT 1 FROM "public"."report_retests" INNER JOIN "public"."report_retest_users" ON "report_retest_users"."report_retest_id" = "report_retests"."id" WHERE (EXISTS (SELECT "invitations".* FROM "public"."invitations" WHERE ("accepted_at" IS NOT NULL OR "expires_at" >= now() OR "expires_at" IS NULL) AND "invitations"."cancelled_at" IS NULL AND "invitations"."rejected_at" IS NULL AND "invitations"."id" = "report_retest_users"."invitation_id") OR (NOT (EXISTS (SELECT "invitations".* FROM "public"."invitations" WHERE "invitations"."id" = "report_retest_users"."invitation_id")))) AND (("report_retest_users"."user_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "report_retest_users"."user_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "report_retest_users"."user_id" IS NOT NULL AND "report_retests"."report_id" = "outer_reports"."id") AS "is_retester", EXISTS (SELECT 1 FROM "public"."teams" LEFT OUTER JOIN "public"."users" ON "users"."anc_triager" WHERE "outer_reports"."pre_submission_review_state" IS NOT NULL AND "teams"."state" IN (4, 5) AND "teams"."anc_enabled" = 't'::bool AND (("users"."id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "users"."id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "users"."id" IS NOT NULL AND "teams"."id" = "outer_reports"."team_id") AS "is_anc_triager", EXISTS (SELECT 1 FROM "public"."teams" INNER JOIN "public"."team_members" ON "team_members"."team_id" = "teams"."id" INNER JOIN "public"."users" ON "users"."id" = "team_members"."user_id" WHERE (("users"."id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "users"."id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "users"."hackerone_triager" = 't'::bool AND "users"."id" IS NOT NULL AND "teams"."id" = "outer_reports"."team_id") AS "is_hackerone_triager", EXISTS (SELECT 1 FROM "public"."report_collaborators" WHERE (("report_collaborators"."user_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "report_collaborators"."user_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "report_collaborators"."user_id" IS NOT NULL AND "report_collaborators"."report_id" = "outer_reports"."id") AS "is_report_collaborator", EXISTS (SELECT 1 FROM "public"."teams" WHERE "teams"."state" = 5 AND "outer_reports"."disclosed_at" IS NOT NULL AND "outer_reports"."public_view" = 'full' AND "teams"."id" = "outer_reports"."team_id") AS "is_someone_visiting_a_public_report", EXISTS (SELECT 1 FROM "public"."teams" WHERE "teams"."state" = 5 AND "outer_reports"."disclosed_at" IS NOT NULL AND "outer_reports"."public_view" = 'no-content' AND "teams"."id" = "outer_reports"."team_id") AS "is_someone_visiting_a_public_limited_report", EXISTS (SELECT 1 FROM "public"."teams" INNER JOIN "public"."whitelisted_reporters" ON "whitelisted_reporters"."team_id" = "teams"."id" WHERE "teams"."state" = 4 AND "teams"."allows_private_disclosure" = 't'::bool AND (("whitelisted_reporters"."user_id" IS NULL AND NULLIF(current_setting('secure.requester_id'), '') IS NULL) OR "whitelisted_reporters"."user_id" = (NULLIF(current_setting('secure.requester_id'), ''))::integer) AND "outer_reports"."disclosed_at" IS NOT NULL AND "outer_reports"."public_view" = 'full' AND "teams"."id" = "outer_reports"."team_id") AS "is_someone_visiting_a_report_privately_disclosed_to_them", "outer_reports"."hacker_published" = 't'::bool AND "outer_reports"."disclosed_at" IS NOT NULL AS "is_hacker_published" FROM "public"."reports" "outer_reports") AS "reports" WHERE "reports"."is_reporter" = 't'::bool OR "reports"."is_team_member" = 't'::bool OR "reports"."is_external_user" = 't'::bool OR "reports"."is_anc_triager" = 't'::bool OR "reports"."is_report_collaborator" = 't'::bool OR "reports"."is_someone_visiting_a_public_report" = 't'::bool OR "reports"."is_someone_visiting_a_public_limited_report" = 't'::bool OR "reports"."is_someone_visiting_a_report_privately_disclosed_to_them" = 't'::bool OR "reports"."is_hacker_published" = 't'::bool OR "reports"."is_retester" = 't'::bool) reports LEFT OUTER JOIN "public"."reports" "new_j0" ON "reports"."id" = "new_j0"."id" AND ("reports"."is_reporter" = 't'::bool OR "reports"."is_team_member" = 't'::bool OR "reports"."is_external_user" = 't'::bool OR "reports"."is_anc_triager" = 't'::bool OR "reports"."is_report_collaborator" = 't'::bool OR "reports"."is_someone_visiting_a_public_report" = 't'::bool OR "reports"."is_someone_visiting_a_public_limited_report" = 't'::bool OR "reports"."is_someone_visiting_a_report_privately_disclosed_to_them" = 't'::bool OR "reports"."is_hacker_published" = 't'::bool)) AS "reports" ON "reports"."id" = "hacktivity_items"."report_id" WHERE "reports"."__new_filterable_disclosed_at" IS NOT NULL) AS "hacktivity_items" LEFT OUTER JOIN "hacktivity_scores" ON "hacktivity_scores"."hacktivity_item_id" = "hacktivity_items"."id"
23
+ }.freeze
24
+ # rubocop:enable Metrics/LineLength
25
+
26
+ def do_it
27
+ arel = Arel.sql_to_arel SQL
28
+ enhanced_arel = Arel.enhance(arel)
29
+
30
+ enhanced_arel.query(class: Arel::Attributes::Attribute).each do |node|
31
+ node.replace node
32
+ end
33
+
34
+ enhanced_arel
35
+ end
36
+
37
+ # Warm up
38
+ do_it
39
+
40
+ profile = StackProf.run(mode: :wall, raw: true) { do_it }
41
+ File.write('stackprof.json', JSON.generate(profile))
42
+
43
+ report = MemoryProfiler.report { do_it }
44
+ puts report.pretty_print
45
+
46
+ n = 100
47
+
48
+ Benchmark.bm do |benchmark|
49
+ benchmark.report do
50
+ n.times do
51
+ do_it
52
+ end
53
+ end
54
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'arel_toolkit'
5
+ require_relative '../spec/support/active_record'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -0,0 +1,52 @@
1
+ require 'mkmf'
2
+ require 'pg'
3
+
4
+ CONFIG['debugflags'] = '-ggdb3'
5
+ CONFIG['optflags'] = '-O0'
6
+
7
+ # https://github.com/jeremyevans/sequel_pg/blob/master/ext/sequel_pg/extconf.rb
8
+
9
+ pg_include_dir = ENV['POSTGRES_INCLUDE'] ||
10
+ (begin
11
+ IO.popen('pg_config --includedir').readline.chomp
12
+ rescue StandardError
13
+ nil
14
+ end)
15
+ pg_lib_dir = ENV['POSTGRES_LIB'] ||
16
+ (begin
17
+ IO.popen('pg_config --libdir').readline.chomp
18
+ rescue StandardError
19
+ nil
20
+ end)
21
+
22
+ dir_config(
23
+ 'pg',
24
+ pg_include_dir,
25
+ pg_lib_dir,
26
+ )
27
+
28
+ pg_ext = Gem.loaded_specs.fetch('pg')
29
+ pg_ext_inlude_dir = File.join(pg_ext.full_gem_path, 'ext')
30
+ pg_ext_lib_dir = pg_ext.extension_dir
31
+
32
+ dir_config(
33
+ 'pg_ext',
34
+ pg_ext_inlude_dir,
35
+ pg_ext_lib_dir,
36
+ )
37
+
38
+ if (
39
+ have_library('pq') ||
40
+ have_library('libpq') ||
41
+ have_library('ms/libpq')
42
+ ) &&
43
+ have_header('libpq-fe.h') &&
44
+ have_header('pg.h') &&
45
+ have_func('PQcopyResult') &&
46
+ have_func('PQsetResultAttrs') &&
47
+ have_func('PQsetvalue')
48
+
49
+ create_makefile('arel_toolkit/pg_result_init')
50
+ else
51
+ abort 'Could not find PostgreSQL build environment (libraries & headers): Makefile not created'
52
+ end
@@ -0,0 +1,138 @@
1
+ #include <libpq-fe.h>
2
+ #include <pg.h>
3
+ #include "pg_result_init.h"
4
+
5
+ VALUE rb_mPgResultInit;
6
+
7
+ static VALUE
8
+ pg_result_init_create(VALUE self, VALUE rb_pgconn, VALUE rb_result, VALUE rb_columns, VALUE rb_rows) {
9
+ Check_Type(rb_columns, T_ARRAY);
10
+ Check_Type(rb_rows, T_ARRAY);
11
+
12
+ if (!rb_obj_is_kind_of(rb_result, rb_cPGresult)) {
13
+ rb_raise(
14
+ rb_eTypeError,
15
+ "wrong argument type %s (expected kind of PG::Result)",
16
+ rb_obj_classname(rb_result)
17
+ );
18
+ }
19
+
20
+ if (!rb_obj_is_kind_of(rb_pgconn, rb_cPGconn)) {
21
+ rb_raise(
22
+ rb_eTypeError,
23
+ "wrong argument type %s (expected kind of PG::Connection)",
24
+ rb_obj_classname(rb_pgconn)
25
+ );
26
+ }
27
+
28
+ PGresult *result = pgresult_get(rb_result);
29
+ PGresult *result_copy = PQcopyResult(result, PG_COPYRES_EVENTS | PG_COPYRES_NOTICEHOOKS);
30
+
31
+ int num_columns = RARRAY_LEN(rb_columns);
32
+ PGresAttDesc *attDescs = malloc(num_columns * sizeof(PGresAttDesc));
33
+
34
+ if (attDescs == NULL)
35
+ rb_raise(rb_eRuntimeError, "Cannot allocate memory");
36
+
37
+ int index;
38
+ VALUE rb_column;
39
+ VALUE rb_column_name;
40
+ VALUE rb_table_id;
41
+ VALUE rb_column_id;
42
+ VALUE rb_format;
43
+ VALUE rb_typ_id;
44
+ VALUE rb_typ_len;
45
+ VALUE rb_att_typemod;
46
+
47
+ char *column_name;
48
+
49
+ for(index = 0; index < num_columns; index++) {
50
+ rb_column = rb_ary_entry(rb_columns, index);
51
+ Check_Type(rb_column, T_HASH);
52
+
53
+ rb_column_name = rb_funcall(rb_column, rb_intern("fetch"), 1, ID2SYM(rb_intern("name")));
54
+
55
+ // 0 means unknown tableid
56
+ rb_table_id = rb_funcall(rb_column, rb_intern("fetch"), 2, ID2SYM(rb_intern("tableid")), INT2NUM(0));
57
+
58
+ // 0 means unknown columnid
59
+ rb_column_id = rb_funcall(rb_column, rb_intern("fetch"), 2, ID2SYM(rb_intern("columnid")), INT2NUM(0));
60
+
61
+ // 0 is text, 1 is binary https://www.postgresql.org/docs/10/libpq-exec.html
62
+ rb_format = rb_funcall(rb_column, rb_intern("fetch"), 2, ID2SYM(rb_intern("format")), INT2NUM(0));
63
+
64
+ rb_typ_id = rb_funcall(rb_column, rb_intern("fetch"), 1, ID2SYM(rb_intern("typid")));
65
+ rb_typ_len = rb_funcall(rb_column, rb_intern("fetch"), 1, ID2SYM(rb_intern("typlen")));
66
+
67
+ // -1 means that there is no type modifier
68
+ rb_att_typemod = rb_funcall(rb_column, rb_intern("fetch"), 2, ID2SYM(rb_intern("atttypmod")), INT2NUM(-1));
69
+
70
+ // Using StringValueCStr, if column contains null bytes it should raise Argument error
71
+ // postgres does not handle null bytes.
72
+ column_name = StringValueCStr(rb_column_name);
73
+
74
+ // postgres/src/interfaces/libpq/libpq-fe.h:235
75
+ attDescs[index].name = column_name;
76
+ attDescs[index].tableid = NUM2INT(rb_table_id);
77
+ attDescs[index].columnid = NUM2INT(rb_column_id);
78
+ attDescs[index].format = NUM2INT(rb_format);
79
+ attDescs[index].typid = NUM2INT(rb_typ_id);
80
+ attDescs[index].typlen = NUM2INT(rb_typ_len);
81
+ attDescs[index].atttypmod = NUM2INT(rb_att_typemod);
82
+ }
83
+
84
+ int success;
85
+
86
+ success = PQsetResultAttrs(result_copy, num_columns, attDescs);
87
+
88
+ if (success == 0)
89
+ rb_raise(rb_eRuntimeError, "PQsetResultAttrs failed: %d", success);
90
+
91
+ free(attDescs);
92
+
93
+ int num_rows = RARRAY_LEN(rb_rows);
94
+ int row_index;
95
+ int column_index;
96
+ VALUE rb_row;
97
+ VALUE rb_value;
98
+ char *value;
99
+ int value_len;
100
+
101
+ for(row_index = 0; row_index < num_rows; row_index++) {
102
+ for(column_index = 0; column_index < num_columns; column_index++) {
103
+ rb_row = rb_ary_entry(rb_rows, row_index);
104
+ rb_value = rb_ary_entry(rb_row, column_index);
105
+
106
+ if (NIL_P(rb_value)) {
107
+ success = PQsetvalue(result_copy, row_index, column_index, NULL, -1);
108
+
109
+ if (success == 0)
110
+ rb_raise(rb_eRuntimeError, "PQsetvalue failed: %d", success);
111
+ }
112
+ else {
113
+ rb_value = rb_funcall(rb_value, rb_intern("to_s"), 0);
114
+
115
+ // Using StringValueCStr, if column contains null bytes it should raise Argument error
116
+ // postgres does not handle null bytes.
117
+ value = StringValueCStr(rb_value);
118
+ value_len = RSTRING_LEN(rb_value);
119
+
120
+ success = PQsetvalue(result_copy, row_index, column_index, value, value_len);
121
+
122
+ if (success == 0)
123
+ rb_raise(rb_eRuntimeError, "PQsetvalue failed: %d", success);
124
+ }
125
+ }
126
+ }
127
+
128
+ VALUE rb_pgresult = pg_new_result(result_copy, rb_pgconn);
129
+ return rb_pgresult;
130
+ }
131
+
132
+ void
133
+ Init_pg_result_init(void)
134
+ {
135
+ rb_mPgResultInit = rb_define_module("PgResultInit");
136
+
137
+ rb_define_module_function(rb_mPgResultInit, "create", pg_result_init_create, 4);
138
+ }