arel_toolkit 0.3.0 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +3 -0
  3. data/.github/workflows/develop.yml +90 -0
  4. data/.github/workflows/master.yml +67 -0
  5. data/.gitignore +8 -0
  6. data/.rubocop.yml +13 -5
  7. data/Appraisals +13 -0
  8. data/CHANGELOG.md +94 -5
  9. data/Gemfile +5 -0
  10. data/Gemfile.lock +62 -33
  11. data/Guardfile +4 -0
  12. data/README.md +67 -23
  13. data/Rakefile +11 -1
  14. data/arel_toolkit.gemspec +15 -6
  15. data/benchmark.rb +54 -0
  16. data/ext/pg_result_init/extconf.rb +52 -0
  17. data/ext/pg_result_init/pg_result_init.c +138 -0
  18. data/ext/pg_result_init/pg_result_init.h +6 -0
  19. data/gemfiles/active_record_6.gemfile +7 -0
  20. data/gemfiles/active_record_6.gemfile.lock +210 -0
  21. data/gemfiles/arel_gems.gemfile +10 -0
  22. data/gemfiles/arel_gems.gemfile.lock +284 -0
  23. data/gemfiles/default.gemfile +5 -0
  24. data/gemfiles/default.gemfile.lock +208 -0
  25. data/lib/arel/enhance.rb +17 -0
  26. data/lib/arel/enhance/context_enhancer/arel_table.rb +92 -0
  27. data/lib/arel/enhance/node.rb +232 -0
  28. data/lib/arel/enhance/path.rb +38 -0
  29. data/lib/arel/enhance/path_node.rb +26 -0
  30. data/lib/arel/enhance/query.rb +38 -0
  31. data/lib/arel/enhance/query_methods.rb +23 -0
  32. data/lib/arel/enhance/visitor.rb +97 -0
  33. data/lib/arel/extensions.rb +32 -6
  34. data/lib/arel/extensions/active_model_attribute_with_cast_value.rb +22 -0
  35. data/lib/arel/extensions/active_record_relation_query_attribute.rb +22 -0
  36. data/lib/arel/extensions/active_record_type_caster_connection.rb +7 -0
  37. data/lib/arel/extensions/active_record_type_caster_map.rb +7 -0
  38. data/lib/arel/extensions/array.rb +2 -9
  39. data/lib/arel/extensions/at_time_zone.rb +10 -3
  40. data/lib/arel/extensions/attributes_attribute.rb +47 -0
  41. data/lib/arel/extensions/binary.rb +7 -0
  42. data/lib/arel/extensions/bind_param.rb +15 -0
  43. data/lib/arel/extensions/bit_string.rb +2 -9
  44. data/lib/arel/extensions/case.rb +17 -0
  45. data/lib/arel/extensions/coalesce.rb +17 -3
  46. data/lib/arel/extensions/conflict.rb +9 -0
  47. data/lib/arel/extensions/contains.rb +27 -5
  48. data/lib/arel/extensions/current_catalog.rb +4 -0
  49. data/lib/arel/extensions/current_date.rb +4 -0
  50. data/lib/arel/extensions/current_of_expression.rb +2 -9
  51. data/lib/arel/extensions/current_role.rb +4 -0
  52. data/lib/arel/extensions/current_row.rb +7 -0
  53. data/lib/arel/extensions/current_schema.rb +4 -0
  54. data/lib/arel/extensions/current_user.rb +4 -0
  55. data/lib/arel/extensions/dealocate.rb +31 -0
  56. data/lib/arel/extensions/default_values.rb +4 -0
  57. data/lib/arel/extensions/delete_manager.rb +22 -6
  58. data/lib/arel/extensions/delete_statement.rb +46 -24
  59. data/lib/arel/extensions/dot.rb +11 -0
  60. data/lib/arel/extensions/exists.rb +59 -0
  61. data/lib/arel/extensions/extract_from.rb +3 -10
  62. data/lib/arel/extensions/factorial.rb +10 -2
  63. data/lib/arel/extensions/false.rb +7 -0
  64. data/lib/arel/extensions/function.rb +44 -14
  65. data/lib/arel/extensions/greatest.rb +17 -3
  66. data/lib/arel/extensions/indirection.rb +3 -12
  67. data/lib/arel/extensions/infer.rb +7 -7
  68. data/lib/arel/extensions/infix_operation.rb +17 -0
  69. data/lib/arel/extensions/insert_manager.rb +19 -3
  70. data/lib/arel/extensions/insert_statement.rb +31 -12
  71. data/lib/arel/extensions/into.rb +21 -0
  72. data/lib/arel/extensions/least.rb +17 -3
  73. data/lib/arel/extensions/named_argument.rb +3 -8
  74. data/lib/arel/extensions/named_function.rb +7 -0
  75. data/lib/arel/extensions/node.rb +10 -0
  76. data/lib/arel/extensions/ordering.rb +21 -6
  77. data/lib/arel/extensions/overlaps.rb +9 -0
  78. data/lib/arel/extensions/overlay.rb +9 -0
  79. data/lib/arel/extensions/position.rb +3 -8
  80. data/lib/arel/extensions/prepare.rb +39 -0
  81. data/lib/arel/extensions/range_function.rb +10 -2
  82. data/lib/arel/extensions/row.rb +3 -8
  83. data/lib/arel/extensions/select_core.rb +73 -0
  84. data/lib/arel/extensions/select_manager.rb +22 -6
  85. data/lib/arel/extensions/select_statement.rb +31 -9
  86. data/lib/arel/extensions/session_user.rb +4 -0
  87. data/lib/arel/extensions/set_to_default.rb +4 -0
  88. data/lib/arel/extensions/substring.rb +8 -0
  89. data/lib/arel/extensions/table.rb +43 -10
  90. data/lib/arel/extensions/time_with_precision.rb +6 -0
  91. data/lib/arel/extensions/to_sql.rb +27 -0
  92. data/lib/arel/extensions/top.rb +8 -0
  93. data/lib/arel/extensions/transaction.rb +3 -8
  94. data/lib/arel/extensions/tree_manager.rb +15 -0
  95. data/lib/arel/extensions/trim.rb +8 -0
  96. data/lib/arel/extensions/true.rb +7 -0
  97. data/lib/arel/extensions/type_cast.rb +7 -0
  98. data/lib/arel/extensions/unary.rb +7 -0
  99. data/lib/arel/extensions/unary_operation.rb +16 -0
  100. data/lib/arel/extensions/unknown.rb +4 -0
  101. data/lib/arel/extensions/update_manager.rb +22 -6
  102. data/lib/arel/extensions/update_statement.rb +36 -33
  103. data/lib/arel/extensions/user.rb +4 -0
  104. data/lib/arel/extensions/values_list.rb +15 -0
  105. data/lib/arel/extensions/variable_set.rb +9 -0
  106. data/lib/arel/extensions/variable_show.rb +3 -8
  107. data/lib/arel/middleware.rb +5 -1
  108. data/lib/arel/middleware/active_record_extension.rb +13 -0
  109. data/lib/arel/middleware/cache_accessor.rb +35 -0
  110. data/lib/arel/middleware/chain.rb +108 -33
  111. data/lib/arel/middleware/database_executor.rb +77 -0
  112. data/lib/arel/middleware/no_op_cache.rb +9 -0
  113. data/lib/arel/middleware/postgresql_adapter.rb +41 -5
  114. data/lib/arel/middleware/railtie.rb +15 -1
  115. data/lib/arel/middleware/result.rb +170 -0
  116. data/lib/arel/middleware/to_sql_executor.rb +15 -0
  117. data/lib/arel/middleware/to_sql_middleware.rb +33 -0
  118. data/lib/arel/sql_to_arel.rb +6 -3
  119. data/lib/arel/sql_to_arel/pg_query_visitor.rb +67 -38
  120. data/lib/arel/sql_to_arel/pg_query_visitor/frame_options.rb +1 -1
  121. data/lib/arel/sql_to_arel/result.rb +17 -4
  122. data/lib/arel/transformer.rb +8 -0
  123. data/lib/arel/transformer/prefix_schema_name.rb +183 -0
  124. data/lib/arel/transformer/remove_active_record_info.rb +40 -0
  125. data/lib/arel/transformer/replace_table_with_subquery.rb +31 -0
  126. data/lib/arel_toolkit.rb +15 -2
  127. data/lib/arel_toolkit/version.rb +1 -1
  128. metadata +179 -42
  129. data/.travis.yml +0 -29
  130. data/lib/arel/extensions/generate_series.rb +0 -9
  131. data/lib/arel/extensions/rank.rb +0 -9
  132. data/lib/arel/extensions/unbound_column_reference.rb +0 -5
  133. data/lib/arel/sql_formatter.rb +0 -59
data/Guardfile CHANGED
@@ -47,3 +47,7 @@ group :red_green_refactor, halt_on_fail: true do
47
47
  watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
48
48
  end
49
49
  end
50
+
51
+ guard 'rake', task: 'compile' do
52
+ watch(/^ext/)
53
+ end
data/README.md CHANGED
@@ -2,7 +2,7 @@
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)
@@ -27,7 +27,7 @@ Or install it yourself as:
27
27
 
28
28
  $ gem install arel_toolkit
29
29
 
30
- ## sql_to_arel
30
+ ## Sql to Arel
31
31
 
32
32
  Convert your (PostgreSQL) SQL into an Arel AST.
33
33
 
@@ -40,56 +40,96 @@ Convert your (PostgreSQL) SQL into an Arel AST.
40
40
  => "SELECT \"id\" FROM \"users\""
41
41
  ```
42
42
 
43
- ## Extensions
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
+ ```
44
76
 
45
- Adds missing Arel nodes and extends the existing visitors, [lib/arel/extensions](https://github.com/mvgijssel/arel_toolkit/tree/master/lib/arel/extensions) for a full list.
46
77
 
47
78
  ## Middleware
48
79
 
49
- The middleware sits between ActiveRecord and the database, which allows you to mutate or log queries before they hit the database. Multiple middleware are supported by passing the results from a finished middleware to the next. User defined context will be passed, which can contains things like the `current_user_id`.
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.
50
81
 
51
- ### Example
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.
52
83
 
53
- Create an initializer in Rails which loads the Arel::Middleware **after** ActiveRecord:
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:
54
85
 
55
86
  ```ruby
56
- ActiveSupport.on_load :active_record do
57
- Arel::Middleware::Railtie.insert_postgresql
58
- end
87
+ Arel::Middleware::Railtie.insert
59
88
  ```
60
89
 
61
- Create some middleware (can be any Ruby object which responds to `call`):
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.
62
95
 
63
96
  ```ruby
64
97
  class ReorderMiddleware
65
- def self.call(arel, _context)
66
- arel.order(Post.arel_table[:id].asc)
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)
67
107
  end
68
108
  end
69
109
 
70
110
  class LoggingMiddleware
71
- def self.call(arel, context)
111
+ def self.call(arel, next_middleware, context)
72
112
  puts "User executing query: `#{context[:current_user_id]}`"
73
113
  puts "Original SQL: `#{context[:original_sql]}`"
74
114
  puts "Modified SQL: `#{arel.to_sql}`"
75
- arel
115
+
116
+ next_middleware.call(arel)
76
117
  end
77
118
  end
78
119
  ```
79
120
 
80
- Run a query with middleware applied
121
+ Now that we've defined our middelwares, it's time to see them in action:
81
122
 
82
123
  ```ruby
83
124
  [1] > Arel.middleware.apply([ReorderMiddleware, LoggingMiddleware]).context(current_user_id: 1) { Post.all.load }
84
125
  User executing query: `1`
85
- Original SQL: `SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC`
86
- Modified SQL: `SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC, "posts"."id" ASC`
87
- Post Load (4.1ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC, "posts"."id" ASC
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
88
129
  => []
89
130
  ```
90
131
 
91
- There are more methods available to help with ordering and modifying of the current applied middleware:
92
-
132
+ This gem ships with a couple of middelware methods that allow you to fine-tune what and when to apply middelware.
93
133
  - `Arel.middleware.apply([SomeMiddleware]) { ... }`
94
134
  - `Arel.middleware.only([OnlyMe]) { ... }`
95
135
  - `Arel.middleware.none { ... }`
@@ -97,6 +137,10 @@ There are more methods available to help with ordering and modifying of the curr
97
137
  - `Arel.middleware.insert_before(RunBefore, ThisMiddleware) { ... }`
98
138
  - `Arel.middleware.insert_after(RunAfter, ThisMiddleware) { ... }`
99
139
 
140
+ ## Extensions
141
+
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).
143
+
100
144
  ## Development
101
145
 
102
146
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -105,7 +149,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
105
149
 
106
150
  ## Contributing
107
151
 
108
- 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.
109
153
 
110
154
  ## License
111
155
 
@@ -113,4 +157,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
113
157
 
114
158
  ## Code of Conduct
115
159
 
116
- 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
@@ -10,5 +10,15 @@ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
10
10
  config.user = 'mvgijssel'
11
11
  config.project = 'arel_toolkit'
12
12
  config.future_release = "v#{ArelToolkit::VERSION}"
13
- config.pulls = false
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'
14
24
  end
data/arel_toolkit.gemspec CHANGED
@@ -23,28 +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 'activerecord', '~> 5.2.0'
28
+ spec.add_dependency 'activerecord'
29
29
  spec.add_dependency 'pg', '~> 1.1.4'
30
- spec.add_dependency 'pg_query', '~> 1.1.0'
30
+ spec.add_dependency 'pg_query', '~> 1.3'
31
31
 
32
32
  spec.add_development_dependency 'bundler', '~> 2.0'
33
33
  spec.add_development_dependency 'dpl', '~> 1.10.11'
34
- spec.add_development_dependency 'github_changelog_generator', '~> 1.14.3'
35
- spec.add_development_dependency 'rake', '~> 10.0'
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'
36
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'
37
40
  spec.add_development_dependency 'database_cleaner', '~> 1.7.0'
38
41
  spec.add_development_dependency 'simplecov', '~> 0.16.1'
39
42
  spec.add_development_dependency 'simplecov-console', '~> 0.4.2'
40
43
 
41
- spec.add_development_dependency 'rubocop', '~> 0.69'
44
+ # When updating also update .codeclimate.yml:5
45
+ spec.add_development_dependency 'rubocop', '= 0.71.0'
42
46
  spec.add_development_dependency 'guard', '~> 2.15'
43
47
  spec.add_development_dependency 'guard-rspec', '~> 4.7'
44
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'
45
53
 
46
54
  spec.add_development_dependency 'pry'
47
55
  spec.add_development_dependency 'pry-nav'
56
+ spec.add_development_dependency 'pry-doc'
48
57
  spec.add_development_dependency 'pry-rescue'
49
58
  spec.add_development_dependency 'pry-stack_explorer'
50
59
  spec.add_development_dependency 'pry-alias'
data/benchmark.rb ADDED
@@ -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
@@ -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
+ }