activerecord-cte 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +39 -0
- data/.travis.yml +7 -0
- data/.vimrc +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dockerfile +19 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +223 -0
- data/Rakefile +18 -0
- data/activerecord-cte.gemspec +40 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/test +26 -0
- data/docker-compose.yml +37 -0
- data/lib/activerecord/cte/core_ext.rb +76 -0
- data/lib/activerecord/cte/version.rb +7 -0
- data/lib/activerecord/cte.rb +15 -0
- metadata +162 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 46f1c2767a3ffe9895a6eb29b0bf7a5fe75d68777333369308f27d51bfa87d70
|
4
|
+
data.tar.gz: 178f6e9757bf085dda1f322223f975deac23fdcda33076f900542cd0ba92c7db
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b73d03b97762fba8e6dc48168268b3fa4a24a47a1d44b740c9c9f4c335ed07fd1ac482f1b9569fe1caf9cc988dc560934c29cfe8d22804e2a9e8445738a9d07
|
7
|
+
data.tar.gz: 60f944ad531fcf8cd109de11c2b453833a1d43d61117072ce4bdba80c8c5bc477cd3b33b52de55d2baab119d60469ecfc19e47e4d9727ed5788466e0ac74f950
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
TargetRubyVersion: 2.6
|
6
|
+
|
7
|
+
Layout/LineLength:
|
8
|
+
AllowHeredoc: true
|
9
|
+
AllowURI: true
|
10
|
+
IgnoreCopDirectives: true
|
11
|
+
Max: 120
|
12
|
+
Exclude:
|
13
|
+
- "test/**/*"
|
14
|
+
|
15
|
+
Metrics/AbcSize:
|
16
|
+
Exclude:
|
17
|
+
- "test/**/*"
|
18
|
+
|
19
|
+
Metrics/CyclomaticComplexity:
|
20
|
+
Max: 7
|
21
|
+
|
22
|
+
Style/ClassAndModuleChildren:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Style/Documentation:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Style/HashEachMethods:
|
29
|
+
Enabled: true
|
30
|
+
|
31
|
+
Style/HashTransformKeys:
|
32
|
+
Enabled: true
|
33
|
+
|
34
|
+
Style/HashTransformValues:
|
35
|
+
Enabled: true
|
36
|
+
|
37
|
+
Style/StringLiterals:
|
38
|
+
EnforcedStyle: double_quotes
|
39
|
+
|
data/.travis.yml
ADDED
data/.vimrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
set colorcolumn=120
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at vladocingel@gmail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Dockerfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
FROM ruby:2.6
|
2
|
+
|
3
|
+
ENV APP_HOME /activerecord_cte
|
4
|
+
RUN mkdir $APP_HOME
|
5
|
+
WORKDIR $APP_HOME
|
6
|
+
|
7
|
+
ENV RAILS_ENV test
|
8
|
+
ENV INSTALL_MYSQL_GEM true
|
9
|
+
ENV INSTALL_PG_GEM true
|
10
|
+
|
11
|
+
# Cache the bundle install
|
12
|
+
COPY Gemfile* $APP_HOME/
|
13
|
+
COPY lib/activerecord/cte/version.rb $APP_HOME/lib/activerecord/cte/version.rb
|
14
|
+
COPY *.gemspec $APP_HOME/
|
15
|
+
RUN gem install bundler
|
16
|
+
RUN bundle install
|
17
|
+
|
18
|
+
ADD . $APP_HOME
|
19
|
+
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in activerecord-cte.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
ACTIVE_RECORD_VERSION = ENV.fetch("ACTIVE_RECORD_VERSION") { "6.0.2.1" }
|
9
|
+
|
10
|
+
gem "activerecord", ACTIVE_RECORD_VERSION
|
11
|
+
|
12
|
+
gem "mysql2" if ENV["INSTALL_MYSQL_GEM"]
|
13
|
+
gem "pg" if ENV["INSTALL_PG_GEM"]
|
14
|
+
|
15
|
+
gem "sqlite3", "1.4.2"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Vlado Cingel
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
# ActiveRecord::Cte
|
2
|
+
|
3
|
+
Adds Common Table Expression support to ActiveRecord (Rails).
|
4
|
+
|
5
|
+
It adds `.with` query method and makes it super easy to build and chain complex CTE queries. Let's explain it using simple example.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
Post.with(
|
9
|
+
posts_with_comments: Post.where("comments_count > ?", 0),
|
10
|
+
posts_with_tags: Post.where("tags_count > ?", 0)
|
11
|
+
)
|
12
|
+
```
|
13
|
+
|
14
|
+
Will return `ActiveRecord::Relation` and will generate SQL like this.
|
15
|
+
|
16
|
+
```SQL
|
17
|
+
WITH posts_with_comments AS (
|
18
|
+
SELECT * FROM posts WHERE (comments_count > 0)
|
19
|
+
), posts_with_tags AS (
|
20
|
+
SELECT * FROM posts WHERE (tags_count > 0)
|
21
|
+
)
|
22
|
+
SELECT * FROM posts
|
23
|
+
```
|
24
|
+
|
25
|
+
Without this gem you would need to use `Arel` directly.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
post_with_comments_table = Arel::Table.new(:posts_with_comments)
|
29
|
+
post_with_comments_expression = Post.arel_table.where(posts_with_comments_table[:comments_count].gt(0))
|
30
|
+
post_with_tags_table = Arel::Table.new(:posts_with_tags)
|
31
|
+
post_with_tags_expression = Post.arel_table.where(posts_with_tags_table[:tags_count].gt(0))
|
32
|
+
|
33
|
+
Post.all.arel.with([
|
34
|
+
Arel::Node::As.new(posts_with_comments_table, posts_with_comments_expression),
|
35
|
+
Arel::Node::As.new(posts_with_tags_table, posts_with_tags_expression)
|
36
|
+
])
|
37
|
+
```
|
38
|
+
|
39
|
+
Instead of Arel you could also pass raw SQL string but either way you will NOT get `ActiveRecord::Relation` and
|
40
|
+
you will not be able to chain them further, cache them easily, call `count` and other aggregates on them, ...
|
41
|
+
|
42
|
+
## Installation
|
43
|
+
|
44
|
+
Add this line to your application's Gemfile:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
gem "activerecord-cte"
|
48
|
+
```
|
49
|
+
|
50
|
+
And then execute:
|
51
|
+
|
52
|
+
$ bundle
|
53
|
+
|
54
|
+
Or install it yourself as:
|
55
|
+
|
56
|
+
$ gem install activerecord-cte
|
57
|
+
|
58
|
+
## Usage
|
59
|
+
|
60
|
+
### Hash arguments
|
61
|
+
|
62
|
+
Easiest way to build the `WITH` query is to pass the `Hash` where keys are used as names of the tables and values are used to
|
63
|
+
generate the SQL. You can pass `ActiveRecord::Relation`, `String` or `Arel::Nodes::As` node.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Post.with(
|
67
|
+
posts_with_comments: Post.where("comments_count > ?", 0),
|
68
|
+
posts_with_tags: "SELECT * FROM posts WHERE tags_count > 0"
|
69
|
+
)
|
70
|
+
# WITH posts_with_comments AS (
|
71
|
+
# SELECT * FROM posts WHERE (comments_count > 0)
|
72
|
+
# ), posts_with_tags AS (
|
73
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
74
|
+
# )
|
75
|
+
# SELECT * FROM posts
|
76
|
+
```
|
77
|
+
|
78
|
+
### SQL string
|
79
|
+
|
80
|
+
You can also pass complete CTE as a single SQL string
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
Post.with("posts_with_tags AS (SELECT * FROM posts WHERE tags_count > 0)")
|
84
|
+
# WITH posts_with_tags AS (
|
85
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
86
|
+
# )
|
87
|
+
# SELECT * FROM posts
|
88
|
+
```
|
89
|
+
|
90
|
+
### Arel Nodes
|
91
|
+
|
92
|
+
If you already have `Arel::Node::As` node you can just pass it as is
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
posts_table = Arel::Table.new(:posts)
|
96
|
+
cte_table = Arel::Table.new(:posts_with_tags)
|
97
|
+
cte_select = posts_table.project(Arel.star).where(posts_table[:tags_count].gt(100))
|
98
|
+
as = Arel::Nodes::As.new(cte_table, cte_select)
|
99
|
+
|
100
|
+
Post.with(as)
|
101
|
+
# WITH posts_with_tags AS (
|
102
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
103
|
+
# )
|
104
|
+
# SELECT * FROM posts
|
105
|
+
```
|
106
|
+
|
107
|
+
You can also pass array of Arel Nodes
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
posts_table = Arel::Table.new(:posts)
|
111
|
+
|
112
|
+
with_tags_table = Arel::Table.new(:posts_with_tags)
|
113
|
+
with_tags_select = posts_table.project(Arel.star).where(posts_table[:tags_count].gt(100))
|
114
|
+
as_posts_with_tags = Arel::Nodes::As.new(with_tags_table, with_tags_select)
|
115
|
+
|
116
|
+
with_comments_table = Arel::Table.new(:posts_with_comments)
|
117
|
+
with_comments_select = posts_table.project(Arel.star).where(posts_table[:comments_count].gt(100))
|
118
|
+
as_posts_with_comments = Arel::Nodes::As.new(with_comments_table, with_comments_select)
|
119
|
+
|
120
|
+
Post.with([as_posts_with_tags, as_posts_with_comments])
|
121
|
+
# WITH posts_with_comments AS (
|
122
|
+
# SELECT * FROM posts WHERE (comments_count > 0)
|
123
|
+
# ), posts_with_tags AS (
|
124
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
125
|
+
# )
|
126
|
+
# SELECT * FROM posts
|
127
|
+
```
|
128
|
+
|
129
|
+
### Taking it further
|
130
|
+
|
131
|
+
As you probably noticed from the examples above `.with` is only a half of the equation. Once we have CTE results we also need to do the select on them somehow.
|
132
|
+
|
133
|
+
You can write custom `FROM` that will alias your CTE table to the table ActiveRecord expects by default (`Post -> posts`) for example.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
Post
|
137
|
+
.with(posts_with_tags: "SELECT * FROM posts WHERE tags_count > 0")
|
138
|
+
.from("posts_with_tags AS posts")
|
139
|
+
# WITH posts_with_tags AS (
|
140
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
141
|
+
# )
|
142
|
+
# SELECT * FROM posts_with_tags AS posts
|
143
|
+
|
144
|
+
Post
|
145
|
+
.with(posts_with_tags: "SELECT * FROM posts WHERE tags_count > 0")
|
146
|
+
.from("posts_with_tags AS posts")
|
147
|
+
.count
|
148
|
+
|
149
|
+
# WITH posts_with_tags AS (
|
150
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
151
|
+
# )
|
152
|
+
# SELECT COUNT(*) FROM posts_with_tags AS posts
|
153
|
+
```
|
154
|
+
|
155
|
+
Another option would be to use join
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Post
|
159
|
+
.with(posts_with_tags: "SELECT * FROM posts WHERE tags_count > 0")
|
160
|
+
.joins("JOIN posts_with_tags ON posts_with_tags.id = posts.id")
|
161
|
+
# WITH posts_with_tags AS (
|
162
|
+
# SELECT * FROM posts WHERE (tags_count > 0)
|
163
|
+
# )
|
164
|
+
# SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id
|
165
|
+
```
|
166
|
+
|
167
|
+
There are other options also but that heavily depends on your use case and is out of scope of this README :)
|
168
|
+
|
169
|
+
### Recursive CTE
|
170
|
+
|
171
|
+
Recursive queries are also supported `Post.with(:recursive, popular_posts: "... union to get popular posts ...")`.
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
posts = Arel::Table.new(:posts)
|
175
|
+
top_posts = Arel::Table.new(:top_posts)
|
176
|
+
|
177
|
+
anchor_term = posts.project(posts[:id]).where(posts[:comments_count].gt(1))
|
178
|
+
recursive_term = posts.project(posts[:id]).join(top_posts).on(posts[:id].eq(top_posts[:id]))
|
179
|
+
|
180
|
+
Post.with(:recursive, top_posts: anchor_term.union(recursive_term)).from("top_posts AS posts")
|
181
|
+
# WITH RECURSIVE "popular_posts" AS (
|
182
|
+
# SELECT "posts"."id" FROM "posts" WHERE "posts"."comments_count" > 0 UNION SELECT "posts"."id" FROM "posts" INNER JOIN "popular_posts" ON "posts"."id" = "popular_posts"."id" ) SELECT "posts".* FROM popular_posts AS posts
|
183
|
+
```
|
184
|
+
|
185
|
+
## Issues
|
186
|
+
|
187
|
+
Please note that `update_all` and `delete_all` methods are not implemented and will not work as expected. I tried to implement them and was succesfull
|
188
|
+
but the "monkey patching" level was so high that I decided not to keep the implementation.
|
189
|
+
|
190
|
+
If my [Pull Request](https://github.com/rails/rails/pull/37944) gets merged adding them to Rails direcly will be easy and since I did not need them yet
|
191
|
+
I decided to wait a bit :)
|
192
|
+
|
193
|
+
## Development
|
194
|
+
|
195
|
+
### Setup
|
196
|
+
|
197
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
198
|
+
|
199
|
+
### Running tests
|
200
|
+
|
201
|
+
Run `rake test` to run the tests using SQLite adapter and latest version on Rails.
|
202
|
+
|
203
|
+
To run tests using all supportted database adapters and ActiveRecord versions run `rake test:matrix`. This will build Docker image with all dependencies and run alll tests in it. See `bin/test` for more info.
|
204
|
+
|
205
|
+
### Console
|
206
|
+
|
207
|
+
You can run `bin/console` for an interactive prompt that will allow you to experiment.
|
208
|
+
|
209
|
+
### Other
|
210
|
+
|
211
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
212
|
+
|
213
|
+
## Contributing
|
214
|
+
|
215
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/activerecord-cte. 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.
|
216
|
+
|
217
|
+
## License
|
218
|
+
|
219
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
220
|
+
|
221
|
+
## Code of Conduct
|
222
|
+
|
223
|
+
Everyone interacting in the Activerecord::Cte project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/activerecord-cte/blob/master/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
task default: :test
|
13
|
+
|
14
|
+
namespace :test do
|
15
|
+
task :matrix do
|
16
|
+
system("docker-compose build && docker-compose run lib bin/test")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "activerecord/cte/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "activerecord-cte"
|
9
|
+
spec.version = Activerecord::Cte::VERSION
|
10
|
+
spec.authors = ["Vlado Cingel"]
|
11
|
+
spec.email = ["vladocingel@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = "Write a short summary, because RubyGems requires one."
|
14
|
+
spec.description = " Write a longer description or delete this line."
|
15
|
+
spec.homepage = "https://github.com/vlado/activerecord-cte"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
19
|
+
|
20
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
21
|
+
spec.metadata["source_code_uri"] = "https://github.com/vlado/activerecord-cte"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency "activerecord"
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
36
|
+
spec.add_development_dependency "rake", "~> 13.0.1"
|
37
|
+
spec.add_development_dependency "rubocop", "~> 0.80.1"
|
38
|
+
spec.add_development_dependency "rubocop-performance", "~> 1.5.2"
|
39
|
+
spec.add_development_dependency "sqlite3"
|
40
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "activerecord/cte"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/bin/test
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
ADAPTERS = %w[
|
5
|
+
mysql
|
6
|
+
postgresql
|
7
|
+
sqlite3
|
8
|
+
]
|
9
|
+
AR_VERSIONS = %w[
|
10
|
+
6.0.2.1
|
11
|
+
5.2.4.1
|
12
|
+
]
|
13
|
+
|
14
|
+
ORIGINAL_AR_VERSION = `bundle show activerecord`.split("-").last.strip
|
15
|
+
|
16
|
+
AR_VERSIONS.each do |ar_version|
|
17
|
+
puts "----> Running tests with ActiveRecord #{ar_version}"
|
18
|
+
system("ACTIVE_RECORD_VERSION=#{ar_version} bundle update activerecord") unless ar_version == ORIGINAL_AR_VERSION
|
19
|
+
ADAPTERS.each do |adapter|
|
20
|
+
puts "----> Running tests with #{adapter} adapter"
|
21
|
+
system("DATABASE_ADAPTER=#{adapter} ACTIVE_RECORD_VERSION=#{ar_version} bundle exec rake")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
puts "----> Reverting back to original ActiveRecord version (#{ORIGINAL_AR_VERSION})"
|
26
|
+
system("ACTIVE_RECORD_VERSION=#{ORIGINAL_AR_VERSION} bundle update activerecord")
|
data/docker-compose.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
version: '3.7'
|
2
|
+
|
3
|
+
services:
|
4
|
+
lib:
|
5
|
+
build: .
|
6
|
+
links:
|
7
|
+
- mysql
|
8
|
+
- postgres
|
9
|
+
volumes:
|
10
|
+
- ".:/activerecord_cte"
|
11
|
+
|
12
|
+
mysql:
|
13
|
+
image: mysql:8.0
|
14
|
+
command: mysqld --default-authentication-plugin=mysql_native_password --skip-mysqlx
|
15
|
+
restart: always
|
16
|
+
environment:
|
17
|
+
MYSQL_DATABASE: activerecord_cte_test
|
18
|
+
MYSQL_USER: user
|
19
|
+
MYSQL_PASSWORD: secret
|
20
|
+
MYSQL_ROOT_PASSWORD: secret
|
21
|
+
ports:
|
22
|
+
- 3306:3306
|
23
|
+
expose:
|
24
|
+
- 3306
|
25
|
+
|
26
|
+
postgres:
|
27
|
+
image: postgres:12
|
28
|
+
restart: always
|
29
|
+
environment:
|
30
|
+
POSTGRES_DB: activerecord_cte_test
|
31
|
+
POSTGRES_USER: user
|
32
|
+
POSTGRES_PASSWORD: secret
|
33
|
+
ports:
|
34
|
+
- 5432:5432
|
35
|
+
expose:
|
36
|
+
- 5432
|
37
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Querying
|
5
|
+
delegate :with, to: :all
|
6
|
+
end
|
7
|
+
|
8
|
+
class Relation
|
9
|
+
def with(opts, *rest)
|
10
|
+
spawn.with!(opts, *rest)
|
11
|
+
end
|
12
|
+
|
13
|
+
def with!(opts, *rest)
|
14
|
+
self.with_values += [opts] + rest
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_values
|
19
|
+
@values[:with] || []
|
20
|
+
end
|
21
|
+
|
22
|
+
def with_values=(values)
|
23
|
+
raise ImmutableRelation if @loaded
|
24
|
+
|
25
|
+
@values[:with] = values
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def build_arel(aliases)
|
31
|
+
arel = super(aliases)
|
32
|
+
build_with(arel) if @values[:with]
|
33
|
+
arel
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_with(arel) # rubocop:disable Metrics/MethodLength
|
37
|
+
return if with_values.empty?
|
38
|
+
|
39
|
+
recursive = with_values.delete(:recursive)
|
40
|
+
with_statements = with_values.map do |with_value|
|
41
|
+
case with_value
|
42
|
+
when String then Arel::Nodes::SqlLiteral.new(with_value)
|
43
|
+
when Arel::Nodes::As then with_value
|
44
|
+
when Hash then build_with_value_from_hash(with_value)
|
45
|
+
when Array then build_with_value_from_array(with_value)
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Unsupported argument type: #{with_value} #{with_value.class}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
recursive ? arel.with(:recursive, with_statements) : arel.with(with_statements)
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_with_value_from_array(array)
|
55
|
+
unless array.map(&:class).uniq == [Arel::Nodes::As]
|
56
|
+
raise ArgumentError, "Unsupported argument type: #{array} #{array.class}"
|
57
|
+
end
|
58
|
+
|
59
|
+
array
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_with_value_from_hash(hash) # rubocop:disable Metrics/MethodLength
|
63
|
+
hash.map do |name, value|
|
64
|
+
table = Arel::Table.new(name)
|
65
|
+
expression = case value
|
66
|
+
when String then Arel::Nodes::SqlLiteral.new("(#{value})")
|
67
|
+
when ActiveRecord::Relation then value.arel
|
68
|
+
when Arel::SelectManager, Arel::Nodes::Union then value
|
69
|
+
else
|
70
|
+
raise ArgumentError, "Unsupported argument type: #{value} #{value.class}"
|
71
|
+
end
|
72
|
+
Arel::Nodes::As.new(table, expression)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support"
|
4
|
+
require "activerecord/cte/version"
|
5
|
+
|
6
|
+
module Activerecord
|
7
|
+
module Cte
|
8
|
+
class Error < StandardError; end
|
9
|
+
# Your code goes here...
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveSupport.on_load(:active_record) do
|
14
|
+
require "activerecord/cte/core_ext"
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-cte
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vlado Cingel
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 13.0.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 13.0.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.80.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.80.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-performance
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.5.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.5.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: " Write a longer description or delete this line."
|
112
|
+
email:
|
113
|
+
- vladocingel@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rubocop.yml"
|
120
|
+
- ".travis.yml"
|
121
|
+
- ".vimrc"
|
122
|
+
- CODE_OF_CONDUCT.md
|
123
|
+
- Dockerfile
|
124
|
+
- Gemfile
|
125
|
+
- LICENSE.txt
|
126
|
+
- README.md
|
127
|
+
- Rakefile
|
128
|
+
- activerecord-cte.gemspec
|
129
|
+
- bin/console
|
130
|
+
- bin/setup
|
131
|
+
- bin/test
|
132
|
+
- docker-compose.yml
|
133
|
+
- lib/activerecord/cte.rb
|
134
|
+
- lib/activerecord/cte/core_ext.rb
|
135
|
+
- lib/activerecord/cte/version.rb
|
136
|
+
homepage: https://github.com/vlado/activerecord-cte
|
137
|
+
licenses:
|
138
|
+
- MIT
|
139
|
+
metadata:
|
140
|
+
allowed_push_host: https://rubygems.org
|
141
|
+
homepage_uri: https://github.com/vlado/activerecord-cte
|
142
|
+
source_code_uri: https://github.com/vlado/activerecord-cte
|
143
|
+
post_install_message:
|
144
|
+
rdoc_options: []
|
145
|
+
require_paths:
|
146
|
+
- lib
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
requirements: []
|
158
|
+
rubygems_version: 3.0.3
|
159
|
+
signing_key:
|
160
|
+
specification_version: 4
|
161
|
+
summary: Write a short summary, because RubyGems requires one.
|
162
|
+
test_files: []
|