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 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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .env
10
+ .DS_Store
11
+ Gemfile.lock
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
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.3
7
+ before_install: gem install bundler -v 2.0.2
data/.vimrc ADDED
@@ -0,0 +1 @@
1
+ set colorcolumn=120
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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")
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Activerecord
4
+ module Cte
5
+ VERSION = "0.1.0"
6
+ end
7
+ 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: []