├── bin ├── setup └── console ├── .gitignore ├── Gemfile ├── lib └── active_record │ ├── union_relation │ └── version.rb │ └── union_relation.rb ├── gemfiles ├── mysql-ar7 │ ├── Gemfile │ └── Gemfile.lock ├── mysql-ar8 │ ├── Gemfile │ └── Gemfile.lock ├── postgresql-ar7 │ ├── Gemfile │ └── Gemfile.lock ├── postgresql-ar8 │ ├── Gemfile │ └── Gemfile.lock ├── sqlite-ar7 │ ├── Gemfile │ └── Gemfile.lock └── sqlite-ar8 │ ├── Gemfile │ └── Gemfile.lock ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ └── main.yml ├── Rakefile ├── LICENSE ├── active_record-union_relation.gemspec ├── CHANGELOG.md ├── test ├── test_helper.rb └── active_record │ └── union_relation_test.rb ├── CODE_OF_CONDUCT.md ├── Gemfile.lock └── README.md /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'active_record/union_relation' 5 | 6 | require 'irb' 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "mysql2" 8 | gem "pg" 9 | gem "sqlite3", "~> 2.5" 10 | -------------------------------------------------------------------------------- /lib/active_record/union_relation/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | class UnionRelation 5 | VERSION = "0.4.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /gemfiles/mysql-ar7/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../.." 6 | 7 | gem "mysql2" 8 | gem "activerecord", "~> 7.0" 9 | -------------------------------------------------------------------------------- /gemfiles/mysql-ar8/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../.." 6 | 7 | gem "mysql2" 8 | gem "activerecord", "~> 8.0" 9 | -------------------------------------------------------------------------------- /gemfiles/postgresql-ar7/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../.." 6 | 7 | gem "pg" 8 | gem "activerecord", "~> 7.0" 9 | -------------------------------------------------------------------------------- /gemfiles/postgresql-ar8/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../.." 6 | 7 | gem "pg" 8 | gem "activerecord", "~> 8.0" 9 | -------------------------------------------------------------------------------- /gemfiles/sqlite-ar7/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../.." 6 | 7 | gem "sqlite3", "~> 1.0" 8 | gem "activerecord", "~> 7.0" 9 | -------------------------------------------------------------------------------- /gemfiles/sqlite-ar8/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../.." 6 | 7 | gem "sqlite3", "~> 2.0" 8 | gem "activerecord", "~> 8.0" 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directories: 5 | - "/" 6 | - "/gemfiles/mysql-ar7" 7 | - "/gemfiles/mysql-ar8" 8 | - "/gemfiles/postgresql-ar7" 9 | - "/gemfiles/postgresql-ar8" 10 | - "/gemfiles/sqlite-ar7" 11 | - "/gemfiles/sqlite-ar8" 12 | schedule: 13 | interval: "daily" 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | require "syntax_tree/rake_tasks" 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << "test" 9 | t.libs << "lib" 10 | t.test_files = FileList["test/**/*_test.rb"] 11 | end 12 | 13 | task default: :test 14 | 15 | configure = ->(task) do 16 | task.source_files = 17 | FileList[%w[Gemfile Rakefile *.gemspec lib/**/*.rb test/**/*.rb]] 18 | end 19 | 20 | SyntaxTree::Rake::CheckTask.new(&configure) 21 | SyntaxTree::Rake::WriteTask.new(&configure) 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --merge "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present Kevin Newton 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. 22 | -------------------------------------------------------------------------------- /active_record-union_relation.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/active_record/union_relation/version" 4 | 5 | version = ActiveRecord::UnionRelation::VERSION 6 | repository = "https://github.com/kddnewton/active_record-union_relation" 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = "active_record-union_relation" 10 | spec.version = version 11 | spec.authors = ["Kevin Newton"] 12 | spec.email = ["kddnewton@gmail.com"] 13 | 14 | spec.summary = "Create ActiveRecord relations from UNIONs" 15 | spec.homepage = repository 16 | spec.license = "MIT" 17 | 18 | spec.metadata = { 19 | "bug_tracker_uri" => "#{repository}/issues", 20 | "changelog_uri" => "#{repository}/blob/v#{version}/CHANGELOG.md", 21 | "source_code_uri" => repository, 22 | "rubygems_mfa_required" => "true" 23 | } 24 | 25 | spec.files = %w[ 26 | CHANGELOG.md 27 | CODE_OF_CONDUCT.md 28 | LICENSE 29 | README.md 30 | active_record-union_relation.gemspec 31 | lib/active_record/union_relation.rb 32 | lib/active_record/union_relation/version.rb 33 | ] 34 | 35 | spec.require_paths = ["lib"] 36 | 37 | spec.add_dependency "activerecord", ">= 6" 38 | 39 | spec.add_development_dependency "minitest" 40 | spec.add_development_dependency "rails" 41 | spec.add_development_dependency "rake" 42 | spec.add_development_dependency "syntax_tree" 43 | end 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.4.0] - 2025-01-04 10 | 11 | ### Added 12 | 13 | - Support for Rails 8. 14 | 15 | ## [0.3.1] - 2024-09-19 16 | 17 | ### Changed 18 | 19 | - Fix up relations with a single source model. 20 | 21 | ## [0.3.0] - 2024-06-13 22 | 23 | ### Added 24 | 25 | - Support relations that are using models that have descendants through STI. 26 | 27 | ## [0.2.1] - 2024-05-29 28 | 29 | ### Changed 30 | 31 | - Limit files in packaged gem. 32 | 33 | ## [0.2.0] - 2024-02-09 34 | 35 | ### Added 36 | 37 | - Support for sqlite. 38 | - Support for scoped column names. 39 | 40 | ## [0.1.1] - 2021-11-17 41 | 42 | ### Changed 43 | 44 | - Require MFA for releasing. 45 | 46 | ## [0.1.0] - 2020-10-08 47 | 48 | ### Added 49 | 50 | - 🎉 Initial release. 🎉 51 | 52 | [unreleased]: https://github.com/kddnewton/active_record-union_relation/compare/v0.4.0...HEAD 53 | [0.4.0]: https://github.com/kddnewton/active_record-union_relation/compare/v0.3.1...v0.4.0 54 | [0.3.1]: https://github.com/kddnewton/active_record-union_relation/compare/v0.3.0...v0.3.1 55 | [0.3.0]: https://github.com/kddnewton/active_record-union_relation/compare/v0.2.1...v0.3.0 56 | [0.2.1]: https://github.com/kddnewton/active_record-union_relation/compare/v0.2.0...v0.2.1 57 | [0.2.0]: https://github.com/kddnewton/active_record-union_relation/compare/v0.1.1...v0.2.0 58 | [0.1.1]: https://github.com/kddnewton/active_record-union_relation/compare/v0.1.0...v0.1.1 59 | [0.1.0]: https://github.com/kddnewton/active_record-union_relation/compare/a71bb8...v0.1.0 60 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "active_record/union_relation" 5 | require "minitest/autorun" 6 | require "rails" 7 | 8 | ENV["DATABASE_URL"] ||= "sqlite3::memory:" 9 | ActiveRecord::Tasks::DatabaseTasks.create_current 10 | 11 | ActiveRecord::Base.establish_connection 12 | ActiveRecord::Base.logger = Logger.new(STDOUT) 13 | 14 | ActiveRecord::Schema.define do 15 | create_table :tags, force: true do |t| 16 | t.string :name 17 | end 18 | 19 | create_table :posts, force: true do |t| 20 | t.boolean :published 21 | t.string :title 22 | end 23 | 24 | create_table :comments, force: true do |t| 25 | t.references :post 26 | t.text :body 27 | end 28 | 29 | create_table :links, force: true do |t| 30 | t.string :type 31 | t.string :url 32 | end 33 | end 34 | 35 | class Tag < ActiveRecord::Base 36 | end 37 | 38 | class Post < ActiveRecord::Base 39 | has_many :comments 40 | accepts_nested_attributes_for :comments 41 | end 42 | 43 | class Comment < ActiveRecord::Base 44 | belongs_to :post 45 | end 46 | 47 | class Link < ActiveRecord::Base 48 | end 49 | 50 | class ImageLink < Link 51 | end 52 | 53 | class VideoLink < Link 54 | end 55 | 56 | class AudioLink < Link 57 | end 58 | 59 | ActiveRecord::Base.transaction do 60 | Tag.create!([{ name: "some" }, { name: "tags" }, { name: "foo" }]) 61 | 62 | Post.create!( 63 | [ 64 | { published: false, title: "foo not published" }, 65 | { 66 | published: true, 67 | title: "foo published", 68 | comments_attributes: [ 69 | { body: "This is a comment" }, 70 | { body: "This is another comment" }, 71 | { body: "This is a comment with foo in it" } 72 | ] 73 | } 74 | ] 75 | ) 76 | 77 | Link.create!( 78 | [ 79 | { type: "ImageLink", url: "http://example.com/some-image" }, 80 | { type: "VideoLink", url: "http://example.com/some-video" }, 81 | { type: "AudioLink", url: "http://example.com/some-audio1" }, 82 | { type: "AudioLink", url: "http://example.com/some-audio2" } 83 | ] 84 | ) 85 | end 86 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | lint: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - run: sudo apt-get -yqq install libpq-dev libsqlite3-dev 13 | - uses: actions/checkout@master 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: '3.3' 17 | bundler-cache: true 18 | - name: Lint 19 | run: bundle exec rake stree:check 20 | mysql: 21 | name: MySQL 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | gemfile: 26 | - gemfiles/mysql-ar7/Gemfile 27 | - gemfiles/mysql-ar8/Gemfile 28 | env: 29 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 30 | DATABASE_URL: mysql2://root:password@127.0.0.1:3306/test 31 | RAILS_ENV: test 32 | services: 33 | mysql: 34 | image: mysql:5.7 35 | env: 36 | MYSQL_DATABASE: test 37 | MYSQL_USERNAME: root 38 | MYSQL_PASSWORD: password 39 | MYSQL_ROOT_PASSWORD: password 40 | MYSQL_HOST: 127.0.0.1 41 | MYSQL_PORT: 3306 42 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 43 | ports: 44 | - 3306:3306 45 | options: >- 46 | --health-cmd="mysqladmin ping" 47 | --health-interval=10s 48 | --health-timeout=5s 49 | --health-retries=3 50 | steps: 51 | - uses: actions/checkout@master 52 | - uses: ruby/setup-ruby@v1 53 | with: 54 | ruby-version: '3.3' 55 | bundler-cache: true 56 | - name: Test 57 | run: bundle exec rake test 58 | postgresql: 59 | name: PostgreSQL 60 | runs-on: ubuntu-latest 61 | strategy: 62 | matrix: 63 | gemfile: 64 | - gemfiles/postgresql-ar7/Gemfile 65 | - gemfiles/postgresql-ar8/Gemfile 66 | env: 67 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 68 | DATABASE_URL: postgres://postgres:@localhost:5432/postgres 69 | RAILS_ENV: test 70 | services: 71 | postgres: 72 | image: postgres:11.5 73 | ports: 74 | - 5432:5432 75 | options: >- 76 | --health-cmd pg_isready 77 | --health-interval 10s 78 | --health-timeout 5s 79 | --health-retries 5 80 | steps: 81 | - run: sudo apt-get -yqq install libpq-dev 82 | - uses: actions/checkout@master 83 | - uses: ruby/setup-ruby@v1 84 | with: 85 | ruby-version: '3.3' 86 | bundler-cache: true 87 | - name: Test 88 | run: bundle exec rake test 89 | sqlite: 90 | name: SQLite 91 | runs-on: ubuntu-latest 92 | strategy: 93 | matrix: 94 | gemfile: 95 | - gemfiles/sqlite-ar7/Gemfile 96 | - gemfiles/sqlite-ar8/Gemfile 97 | env: 98 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 99 | DATABASE_URL: "sqlite3::memory:" 100 | RAILS_ENV: test 101 | steps: 102 | - run: sudo apt-get -yqq install libsqlite3-dev 103 | - uses: actions/checkout@master 104 | - uses: ruby/setup-ruby@v1 105 | with: 106 | ruby-version: '3.3' 107 | bundler-cache: true 108 | - name: Test 109 | run: bundle exec rake test 110 | -------------------------------------------------------------------------------- /test/active_record/union_relation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActiveRecord 6 | class UnionRelationTest < Minitest::Test 7 | def test_version 8 | refute_nil UnionRelation::VERSION 9 | end 10 | 11 | def test_empty_union 12 | assert_raises UnionRelation::NoConfiguredSubqueriesError do 13 | ActiveRecord.union(:id, :post_id, :matched) {} 14 | end 15 | end 16 | 17 | def test_bad_config_union 18 | assert_raises UnionRelation::MismatchedColumnsError do 19 | ActiveRecord.union(:id) { |union| union.add Post.all, :id, :title } 20 | end 21 | end 22 | 23 | def test_good_union 24 | term = "foo" 25 | relation = 26 | ActiveRecord.union(:id, :post_id, :matched) do |union| 27 | posts = Post.where(published: true).where("title LIKE ?", "%#{term}%") 28 | comments = Comment.where("body LIKE ?", "%#{term}%") 29 | tags = Tag.where("name LIKE ?", "%#{term}%") 30 | 31 | union.add posts, :id, nil, :title 32 | union.add comments, :id, :post_id, :body 33 | union.add tags, :id, nil, :name 34 | end 35 | 36 | unioned = relation.order(matched: :asc).group_by(&:class) 37 | assert_equal 3, unioned.length 38 | 39 | assert_kind_of Post, unioned[Comment][0].post 40 | end 41 | 42 | # When using joined queries it's often required to append the table/scope name 43 | # before the column name. This is to disambiguate the column name. 44 | # ActiveRecord attributes should not contain the scope/table part of this 45 | # name. 46 | def test_scoped_column_union 47 | relation = 48 | ActiveRecord.union(:id, :post_id, :body, :title) do |union| 49 | comments = Comment.joins(:post).where(posts: { published: true }) 50 | posts = Post.none 51 | 52 | union.add comments, 53 | "comments.id", 54 | "comments.post_id", 55 | "comments.body", 56 | :title 57 | 58 | union.add posts, nil, "posts.id", nil, :title 59 | end 60 | 61 | items = relation.order(title: :asc) 62 | assert_kind_of Post, items.first.post 63 | end 64 | 65 | def test_single_table_inheritance 66 | relation = 67 | ActiveRecord.union(:id, :text) do |union| 68 | union.add Tag.all, :id, :name 69 | union.add Link.all, :id, :url 70 | end 71 | 72 | unioned = relation.where("text LIKE ?", "%some%").group_by(&:class) 73 | assert_equal 4, unioned.length 74 | 75 | unioned.delete(Tag) 76 | assert unioned.keys.all? { |key| key < Link } 77 | end 78 | 79 | def test_one_model 80 | relation = 81 | ActiveRecord.union(:id, :body, :post_id) do |union| 82 | union.add Comment.all, :id, :body, :post_id 83 | end 84 | 85 | items = relation.order(body: :asc) 86 | assert_kind_of Post, items.first.post 87 | end 88 | 89 | def test_one_sti_model 90 | relation = 91 | ActiveRecord.union(:id, :url) { |union| union.add Link.all, :id, :url } 92 | 93 | unioned = relation.group_by(&:class) 94 | unioned.delete(Tag) 95 | 96 | assert unioned.keys.all? { |key| key < Link } 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 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 kddnewton@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 [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /gemfiles/mysql-ar8/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (8.0.1) 11 | actionpack (= 8.0.1) 12 | activesupport (= 8.0.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (8.0.1) 17 | actionpack (= 8.0.1) 18 | activejob (= 8.0.1) 19 | activerecord (= 8.0.1) 20 | activestorage (= 8.0.1) 21 | activesupport (= 8.0.1) 22 | mail (>= 2.8.0) 23 | actionmailer (8.0.1) 24 | actionpack (= 8.0.1) 25 | actionview (= 8.0.1) 26 | activejob (= 8.0.1) 27 | activesupport (= 8.0.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (8.0.1) 31 | actionview (= 8.0.1) 32 | activesupport (= 8.0.1) 33 | nokogiri (>= 1.8.5) 34 | rack (>= 2.2.4) 35 | rack-session (>= 1.0.1) 36 | rack-test (>= 0.6.3) 37 | rails-dom-testing (~> 2.2) 38 | rails-html-sanitizer (~> 1.6) 39 | useragent (~> 0.16) 40 | actiontext (8.0.1) 41 | actionpack (= 8.0.1) 42 | activerecord (= 8.0.1) 43 | activestorage (= 8.0.1) 44 | activesupport (= 8.0.1) 45 | globalid (>= 0.6.0) 46 | nokogiri (>= 1.8.5) 47 | actionview (8.0.1) 48 | activesupport (= 8.0.1) 49 | builder (~> 3.1) 50 | erubi (~> 1.11) 51 | rails-dom-testing (~> 2.2) 52 | rails-html-sanitizer (~> 1.6) 53 | activejob (8.0.1) 54 | activesupport (= 8.0.1) 55 | globalid (>= 0.3.6) 56 | activemodel (8.0.1) 57 | activesupport (= 8.0.1) 58 | activerecord (8.0.1) 59 | activemodel (= 8.0.1) 60 | activesupport (= 8.0.1) 61 | timeout (>= 0.4.0) 62 | activestorage (8.0.1) 63 | actionpack (= 8.0.1) 64 | activejob (= 8.0.1) 65 | activerecord (= 8.0.1) 66 | activesupport (= 8.0.1) 67 | marcel (~> 1.0) 68 | activesupport (8.0.1) 69 | base64 70 | benchmark (>= 0.3) 71 | bigdecimal 72 | concurrent-ruby (~> 1.0, >= 1.3.1) 73 | connection_pool (>= 2.2.5) 74 | drb 75 | i18n (>= 1.6, < 2) 76 | logger (>= 1.4.2) 77 | minitest (>= 5.1) 78 | securerandom (>= 0.3) 79 | tzinfo (~> 2.0, >= 2.0.5) 80 | uri (>= 0.13.1) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | mysql2 (0.5.6) 112 | net-imap (0.5.5) 113 | date 114 | net-protocol 115 | net-pop (0.1.2) 116 | net-protocol 117 | net-protocol (0.2.2) 118 | timeout 119 | net-smtp (0.5.0) 120 | net-protocol 121 | nio4r (2.7.4) 122 | nokogiri (1.18.1-arm64-darwin) 123 | racc (~> 1.4) 124 | nokogiri (1.18.1-x86_64-linux-gnu) 125 | racc (~> 1.4) 126 | prettier_print (1.2.1) 127 | psych (5.2.2) 128 | date 129 | stringio 130 | racc (1.8.1) 131 | rack (3.1.8) 132 | rack-session (2.1.0) 133 | base64 (>= 0.1.0) 134 | rack (>= 3.0.0) 135 | rack-test (2.2.0) 136 | rack (>= 1.3) 137 | rackup (2.2.1) 138 | rack (>= 3) 139 | rails (8.0.1) 140 | actioncable (= 8.0.1) 141 | actionmailbox (= 8.0.1) 142 | actionmailer (= 8.0.1) 143 | actionpack (= 8.0.1) 144 | actiontext (= 8.0.1) 145 | actionview (= 8.0.1) 146 | activejob (= 8.0.1) 147 | activemodel (= 8.0.1) 148 | activerecord (= 8.0.1) 149 | activestorage (= 8.0.1) 150 | activesupport (= 8.0.1) 151 | bundler (>= 1.15.0) 152 | railties (= 8.0.1) 153 | rails-dom-testing (2.2.0) 154 | activesupport (>= 5.0.0) 155 | minitest 156 | nokogiri (>= 1.6) 157 | rails-html-sanitizer (1.6.2) 158 | loofah (~> 2.21) 159 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 160 | railties (8.0.1) 161 | actionpack (= 8.0.1) 162 | activesupport (= 8.0.1) 163 | irb (~> 1.13) 164 | rackup (>= 1.0.0) 165 | rake (>= 12.2) 166 | thor (~> 1.0, >= 1.2.2) 167 | zeitwerk (~> 2.6) 168 | rake (13.2.1) 169 | rdoc (6.10.0) 170 | psych (>= 4.0.0) 171 | reline (0.6.0) 172 | io-console (~> 0.5) 173 | securerandom (0.4.1) 174 | stringio (3.1.2) 175 | syntax_tree (6.2.0) 176 | prettier_print (>= 1.2.0) 177 | thor (1.3.2) 178 | timeout (0.4.3) 179 | tzinfo (2.0.6) 180 | concurrent-ruby (~> 1.0) 181 | uri (1.0.2) 182 | useragent (0.16.11) 183 | websocket-driver (0.7.7) 184 | base64 185 | websocket-extensions (>= 0.1.0) 186 | websocket-extensions (0.1.5) 187 | zeitwerk (2.7.1) 188 | 189 | PLATFORMS 190 | arm64-darwin-22 191 | arm64-darwin-23 192 | x86_64-linux 193 | 194 | DEPENDENCIES 195 | active_record-union_relation! 196 | activerecord (~> 8.0) 197 | minitest 198 | mysql2 199 | rails 200 | rake 201 | syntax_tree 202 | 203 | BUNDLED WITH 204 | 2.5.16 205 | -------------------------------------------------------------------------------- /gemfiles/postgresql-ar7/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (7.2.2.1) 11 | actionpack (= 7.2.2.1) 12 | activesupport (= 7.2.2.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (7.2.2.1) 17 | actionpack (= 7.2.2.1) 18 | activejob (= 7.2.2.1) 19 | activerecord (= 7.2.2.1) 20 | activestorage (= 7.2.2.1) 21 | activesupport (= 7.2.2.1) 22 | mail (>= 2.8.0) 23 | actionmailer (7.2.2.1) 24 | actionpack (= 7.2.2.1) 25 | actionview (= 7.2.2.1) 26 | activejob (= 7.2.2.1) 27 | activesupport (= 7.2.2.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (7.2.2.1) 31 | actionview (= 7.2.2.1) 32 | activesupport (= 7.2.2.1) 33 | nokogiri (>= 1.8.5) 34 | racc 35 | rack (>= 2.2.4, < 3.2) 36 | rack-session (>= 1.0.1) 37 | rack-test (>= 0.6.3) 38 | rails-dom-testing (~> 2.2) 39 | rails-html-sanitizer (~> 1.6) 40 | useragent (~> 0.16) 41 | actiontext (7.2.2.1) 42 | actionpack (= 7.2.2.1) 43 | activerecord (= 7.2.2.1) 44 | activestorage (= 7.2.2.1) 45 | activesupport (= 7.2.2.1) 46 | globalid (>= 0.6.0) 47 | nokogiri (>= 1.8.5) 48 | actionview (7.2.2.1) 49 | activesupport (= 7.2.2.1) 50 | builder (~> 3.1) 51 | erubi (~> 1.11) 52 | rails-dom-testing (~> 2.2) 53 | rails-html-sanitizer (~> 1.6) 54 | activejob (7.2.2.1) 55 | activesupport (= 7.2.2.1) 56 | globalid (>= 0.3.6) 57 | activemodel (7.2.2.1) 58 | activesupport (= 7.2.2.1) 59 | activerecord (7.2.2.1) 60 | activemodel (= 7.2.2.1) 61 | activesupport (= 7.2.2.1) 62 | timeout (>= 0.4.0) 63 | activestorage (7.2.2.1) 64 | actionpack (= 7.2.2.1) 65 | activejob (= 7.2.2.1) 66 | activerecord (= 7.2.2.1) 67 | activesupport (= 7.2.2.1) 68 | marcel (~> 1.0) 69 | activesupport (7.2.2.1) 70 | base64 71 | benchmark (>= 0.3) 72 | bigdecimal 73 | concurrent-ruby (~> 1.0, >= 1.3.1) 74 | connection_pool (>= 2.2.5) 75 | drb 76 | i18n (>= 1.6, < 2) 77 | logger (>= 1.4.2) 78 | minitest (>= 5.1) 79 | securerandom (>= 0.3) 80 | tzinfo (~> 2.0, >= 2.0.5) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | net-imap (0.5.5) 112 | date 113 | net-protocol 114 | net-pop (0.1.2) 115 | net-protocol 116 | net-protocol (0.2.2) 117 | timeout 118 | net-smtp (0.5.0) 119 | net-protocol 120 | nio4r (2.7.4) 121 | nokogiri (1.18.1-arm64-darwin) 122 | racc (~> 1.4) 123 | nokogiri (1.18.1-x86_64-linux-gnu) 124 | racc (~> 1.4) 125 | pg (1.5.9) 126 | prettier_print (1.2.1) 127 | psych (5.2.2) 128 | date 129 | stringio 130 | racc (1.8.1) 131 | rack (3.1.8) 132 | rack-session (2.1.0) 133 | base64 (>= 0.1.0) 134 | rack (>= 3.0.0) 135 | rack-test (2.2.0) 136 | rack (>= 1.3) 137 | rackup (2.2.1) 138 | rack (>= 3) 139 | rails (7.2.2.1) 140 | actioncable (= 7.2.2.1) 141 | actionmailbox (= 7.2.2.1) 142 | actionmailer (= 7.2.2.1) 143 | actionpack (= 7.2.2.1) 144 | actiontext (= 7.2.2.1) 145 | actionview (= 7.2.2.1) 146 | activejob (= 7.2.2.1) 147 | activemodel (= 7.2.2.1) 148 | activerecord (= 7.2.2.1) 149 | activestorage (= 7.2.2.1) 150 | activesupport (= 7.2.2.1) 151 | bundler (>= 1.15.0) 152 | railties (= 7.2.2.1) 153 | rails-dom-testing (2.2.0) 154 | activesupport (>= 5.0.0) 155 | minitest 156 | nokogiri (>= 1.6) 157 | rails-html-sanitizer (1.6.2) 158 | loofah (~> 2.21) 159 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 160 | railties (7.2.2.1) 161 | actionpack (= 7.2.2.1) 162 | activesupport (= 7.2.2.1) 163 | irb (~> 1.13) 164 | rackup (>= 1.0.0) 165 | rake (>= 12.2) 166 | thor (~> 1.0, >= 1.2.2) 167 | zeitwerk (~> 2.6) 168 | rake (13.2.1) 169 | rdoc (6.10.0) 170 | psych (>= 4.0.0) 171 | reline (0.6.0) 172 | io-console (~> 0.5) 173 | securerandom (0.4.1) 174 | stringio (3.1.2) 175 | syntax_tree (6.2.0) 176 | prettier_print (>= 1.2.0) 177 | thor (1.3.2) 178 | timeout (0.4.3) 179 | tzinfo (2.0.6) 180 | concurrent-ruby (~> 1.0) 181 | useragent (0.16.11) 182 | websocket-driver (0.7.7) 183 | base64 184 | websocket-extensions (>= 0.1.0) 185 | websocket-extensions (0.1.5) 186 | zeitwerk (2.7.1) 187 | 188 | PLATFORMS 189 | arm64-darwin-22 190 | arm64-darwin-23 191 | x86_64-linux 192 | 193 | DEPENDENCIES 194 | active_record-union_relation! 195 | activerecord (~> 7.0) 196 | minitest 197 | pg 198 | rails 199 | rake 200 | syntax_tree 201 | 202 | BUNDLED WITH 203 | 2.5.16 204 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (8.0.1) 11 | actionpack (= 8.0.1) 12 | activesupport (= 8.0.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (8.0.1) 17 | actionpack (= 8.0.1) 18 | activejob (= 8.0.1) 19 | activerecord (= 8.0.1) 20 | activestorage (= 8.0.1) 21 | activesupport (= 8.0.1) 22 | mail (>= 2.8.0) 23 | actionmailer (8.0.1) 24 | actionpack (= 8.0.1) 25 | actionview (= 8.0.1) 26 | activejob (= 8.0.1) 27 | activesupport (= 8.0.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (8.0.1) 31 | actionview (= 8.0.1) 32 | activesupport (= 8.0.1) 33 | nokogiri (>= 1.8.5) 34 | rack (>= 2.2.4) 35 | rack-session (>= 1.0.1) 36 | rack-test (>= 0.6.3) 37 | rails-dom-testing (~> 2.2) 38 | rails-html-sanitizer (~> 1.6) 39 | useragent (~> 0.16) 40 | actiontext (8.0.1) 41 | actionpack (= 8.0.1) 42 | activerecord (= 8.0.1) 43 | activestorage (= 8.0.1) 44 | activesupport (= 8.0.1) 45 | globalid (>= 0.6.0) 46 | nokogiri (>= 1.8.5) 47 | actionview (8.0.1) 48 | activesupport (= 8.0.1) 49 | builder (~> 3.1) 50 | erubi (~> 1.11) 51 | rails-dom-testing (~> 2.2) 52 | rails-html-sanitizer (~> 1.6) 53 | activejob (8.0.1) 54 | activesupport (= 8.0.1) 55 | globalid (>= 0.3.6) 56 | activemodel (8.0.1) 57 | activesupport (= 8.0.1) 58 | activerecord (8.0.1) 59 | activemodel (= 8.0.1) 60 | activesupport (= 8.0.1) 61 | timeout (>= 0.4.0) 62 | activestorage (8.0.1) 63 | actionpack (= 8.0.1) 64 | activejob (= 8.0.1) 65 | activerecord (= 8.0.1) 66 | activesupport (= 8.0.1) 67 | marcel (~> 1.0) 68 | activesupport (8.0.1) 69 | base64 70 | benchmark (>= 0.3) 71 | bigdecimal 72 | concurrent-ruby (~> 1.0, >= 1.3.1) 73 | connection_pool (>= 2.2.5) 74 | drb 75 | i18n (>= 1.6, < 2) 76 | logger (>= 1.4.2) 77 | minitest (>= 5.1) 78 | securerandom (>= 0.3) 79 | tzinfo (~> 2.0, >= 2.0.5) 80 | uri (>= 0.13.1) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | mysql2 (0.5.6) 112 | net-imap (0.5.5) 113 | date 114 | net-protocol 115 | net-pop (0.1.2) 116 | net-protocol 117 | net-protocol (0.2.2) 118 | timeout 119 | net-smtp (0.5.0) 120 | net-protocol 121 | nio4r (2.7.4) 122 | nokogiri (1.18.1-arm64-darwin) 123 | racc (~> 1.4) 124 | nokogiri (1.18.1-x86_64-linux-gnu) 125 | racc (~> 1.4) 126 | pg (1.5.9) 127 | prettier_print (1.2.1) 128 | psych (5.2.2) 129 | date 130 | stringio 131 | racc (1.8.1) 132 | rack (3.1.8) 133 | rack-session (2.1.0) 134 | base64 (>= 0.1.0) 135 | rack (>= 3.0.0) 136 | rack-test (2.2.0) 137 | rack (>= 1.3) 138 | rackup (2.2.1) 139 | rack (>= 3) 140 | rails (8.0.1) 141 | actioncable (= 8.0.1) 142 | actionmailbox (= 8.0.1) 143 | actionmailer (= 8.0.1) 144 | actionpack (= 8.0.1) 145 | actiontext (= 8.0.1) 146 | actionview (= 8.0.1) 147 | activejob (= 8.0.1) 148 | activemodel (= 8.0.1) 149 | activerecord (= 8.0.1) 150 | activestorage (= 8.0.1) 151 | activesupport (= 8.0.1) 152 | bundler (>= 1.15.0) 153 | railties (= 8.0.1) 154 | rails-dom-testing (2.2.0) 155 | activesupport (>= 5.0.0) 156 | minitest 157 | nokogiri (>= 1.6) 158 | rails-html-sanitizer (1.6.2) 159 | loofah (~> 2.21) 160 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 161 | railties (8.0.1) 162 | actionpack (= 8.0.1) 163 | activesupport (= 8.0.1) 164 | irb (~> 1.13) 165 | rackup (>= 1.0.0) 166 | rake (>= 12.2) 167 | thor (~> 1.0, >= 1.2.2) 168 | zeitwerk (~> 2.6) 169 | rake (13.2.1) 170 | rdoc (6.10.0) 171 | psych (>= 4.0.0) 172 | reline (0.6.0) 173 | io-console (~> 0.5) 174 | securerandom (0.4.1) 175 | sqlite3 (2.5.0-arm64-darwin) 176 | sqlite3 (2.5.0-x86_64-linux-gnu) 177 | stringio (3.1.2) 178 | syntax_tree (6.2.0) 179 | prettier_print (>= 1.2.0) 180 | thor (1.3.2) 181 | timeout (0.4.3) 182 | tzinfo (2.0.6) 183 | concurrent-ruby (~> 1.0) 184 | uri (1.0.2) 185 | useragent (0.16.11) 186 | websocket-driver (0.7.7) 187 | base64 188 | websocket-extensions (>= 0.1.0) 189 | websocket-extensions (0.1.5) 190 | zeitwerk (2.7.1) 191 | 192 | PLATFORMS 193 | arm64-darwin-22 194 | arm64-darwin-23 195 | x86_64-linux 196 | 197 | DEPENDENCIES 198 | active_record-union_relation! 199 | minitest 200 | mysql2 201 | pg 202 | rails 203 | rake 204 | sqlite3 (~> 2.5) 205 | syntax_tree 206 | 207 | BUNDLED WITH 208 | 2.5.7 209 | -------------------------------------------------------------------------------- /gemfiles/sqlite-ar7/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (7.2.2.1) 11 | actionpack (= 7.2.2.1) 12 | activesupport (= 7.2.2.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (7.2.2.1) 17 | actionpack (= 7.2.2.1) 18 | activejob (= 7.2.2.1) 19 | activerecord (= 7.2.2.1) 20 | activestorage (= 7.2.2.1) 21 | activesupport (= 7.2.2.1) 22 | mail (>= 2.8.0) 23 | actionmailer (7.2.2.1) 24 | actionpack (= 7.2.2.1) 25 | actionview (= 7.2.2.1) 26 | activejob (= 7.2.2.1) 27 | activesupport (= 7.2.2.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (7.2.2.1) 31 | actionview (= 7.2.2.1) 32 | activesupport (= 7.2.2.1) 33 | nokogiri (>= 1.8.5) 34 | racc 35 | rack (>= 2.2.4, < 3.2) 36 | rack-session (>= 1.0.1) 37 | rack-test (>= 0.6.3) 38 | rails-dom-testing (~> 2.2) 39 | rails-html-sanitizer (~> 1.6) 40 | useragent (~> 0.16) 41 | actiontext (7.2.2.1) 42 | actionpack (= 7.2.2.1) 43 | activerecord (= 7.2.2.1) 44 | activestorage (= 7.2.2.1) 45 | activesupport (= 7.2.2.1) 46 | globalid (>= 0.6.0) 47 | nokogiri (>= 1.8.5) 48 | actionview (7.2.2.1) 49 | activesupport (= 7.2.2.1) 50 | builder (~> 3.1) 51 | erubi (~> 1.11) 52 | rails-dom-testing (~> 2.2) 53 | rails-html-sanitizer (~> 1.6) 54 | activejob (7.2.2.1) 55 | activesupport (= 7.2.2.1) 56 | globalid (>= 0.3.6) 57 | activemodel (7.2.2.1) 58 | activesupport (= 7.2.2.1) 59 | activerecord (7.2.2.1) 60 | activemodel (= 7.2.2.1) 61 | activesupport (= 7.2.2.1) 62 | timeout (>= 0.4.0) 63 | activestorage (7.2.2.1) 64 | actionpack (= 7.2.2.1) 65 | activejob (= 7.2.2.1) 66 | activerecord (= 7.2.2.1) 67 | activesupport (= 7.2.2.1) 68 | marcel (~> 1.0) 69 | activesupport (7.2.2.1) 70 | base64 71 | benchmark (>= 0.3) 72 | bigdecimal 73 | concurrent-ruby (~> 1.0, >= 1.3.1) 74 | connection_pool (>= 2.2.5) 75 | drb 76 | i18n (>= 1.6, < 2) 77 | logger (>= 1.4.2) 78 | minitest (>= 5.1) 79 | securerandom (>= 0.3) 80 | tzinfo (~> 2.0, >= 2.0.5) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | net-imap (0.5.5) 112 | date 113 | net-protocol 114 | net-pop (0.1.2) 115 | net-protocol 116 | net-protocol (0.2.2) 117 | timeout 118 | net-smtp (0.5.0) 119 | net-protocol 120 | nio4r (2.7.4) 121 | nokogiri (1.18.1-arm64-darwin) 122 | racc (~> 1.4) 123 | nokogiri (1.18.1-x86_64-linux-gnu) 124 | racc (~> 1.4) 125 | prettier_print (1.2.1) 126 | psych (5.2.2) 127 | date 128 | stringio 129 | racc (1.8.1) 130 | rack (3.1.8) 131 | rack-session (2.1.0) 132 | base64 (>= 0.1.0) 133 | rack (>= 3.0.0) 134 | rack-test (2.2.0) 135 | rack (>= 1.3) 136 | rackup (2.2.1) 137 | rack (>= 3) 138 | rails (7.2.2.1) 139 | actioncable (= 7.2.2.1) 140 | actionmailbox (= 7.2.2.1) 141 | actionmailer (= 7.2.2.1) 142 | actionpack (= 7.2.2.1) 143 | actiontext (= 7.2.2.1) 144 | actionview (= 7.2.2.1) 145 | activejob (= 7.2.2.1) 146 | activemodel (= 7.2.2.1) 147 | activerecord (= 7.2.2.1) 148 | activestorage (= 7.2.2.1) 149 | activesupport (= 7.2.2.1) 150 | bundler (>= 1.15.0) 151 | railties (= 7.2.2.1) 152 | rails-dom-testing (2.2.0) 153 | activesupport (>= 5.0.0) 154 | minitest 155 | nokogiri (>= 1.6) 156 | rails-html-sanitizer (1.6.2) 157 | loofah (~> 2.21) 158 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 159 | railties (7.2.2.1) 160 | actionpack (= 7.2.2.1) 161 | activesupport (= 7.2.2.1) 162 | irb (~> 1.13) 163 | rackup (>= 1.0.0) 164 | rake (>= 12.2) 165 | thor (~> 1.0, >= 1.2.2) 166 | zeitwerk (~> 2.6) 167 | rake (13.2.1) 168 | rdoc (6.10.0) 169 | psych (>= 4.0.0) 170 | reline (0.6.0) 171 | io-console (~> 0.5) 172 | securerandom (0.4.1) 173 | sqlite3 (1.7.3-arm64-darwin) 174 | sqlite3 (1.7.3-x86_64-linux) 175 | stringio (3.1.2) 176 | syntax_tree (6.2.0) 177 | prettier_print (>= 1.2.0) 178 | thor (1.3.2) 179 | timeout (0.4.3) 180 | tzinfo (2.0.6) 181 | concurrent-ruby (~> 1.0) 182 | useragent (0.16.11) 183 | websocket-driver (0.7.7) 184 | base64 185 | websocket-extensions (>= 0.1.0) 186 | websocket-extensions (0.1.5) 187 | zeitwerk (2.7.1) 188 | 189 | PLATFORMS 190 | arm64-darwin-22 191 | arm64-darwin-23 192 | x86_64-linux 193 | 194 | DEPENDENCIES 195 | active_record-union_relation! 196 | activerecord (~> 7.0) 197 | minitest 198 | rails 199 | rake 200 | sqlite3 (~> 1.0) 201 | syntax_tree 202 | 203 | BUNDLED WITH 204 | 2.5.16 205 | -------------------------------------------------------------------------------- /gemfiles/postgresql-ar8/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (8.0.1) 11 | actionpack (= 8.0.1) 12 | activesupport (= 8.0.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (8.0.1) 17 | actionpack (= 8.0.1) 18 | activejob (= 8.0.1) 19 | activerecord (= 8.0.1) 20 | activestorage (= 8.0.1) 21 | activesupport (= 8.0.1) 22 | mail (>= 2.8.0) 23 | actionmailer (8.0.1) 24 | actionpack (= 8.0.1) 25 | actionview (= 8.0.1) 26 | activejob (= 8.0.1) 27 | activesupport (= 8.0.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (8.0.1) 31 | actionview (= 8.0.1) 32 | activesupport (= 8.0.1) 33 | nokogiri (>= 1.8.5) 34 | rack (>= 2.2.4) 35 | rack-session (>= 1.0.1) 36 | rack-test (>= 0.6.3) 37 | rails-dom-testing (~> 2.2) 38 | rails-html-sanitizer (~> 1.6) 39 | useragent (~> 0.16) 40 | actiontext (8.0.1) 41 | actionpack (= 8.0.1) 42 | activerecord (= 8.0.1) 43 | activestorage (= 8.0.1) 44 | activesupport (= 8.0.1) 45 | globalid (>= 0.6.0) 46 | nokogiri (>= 1.8.5) 47 | actionview (8.0.1) 48 | activesupport (= 8.0.1) 49 | builder (~> 3.1) 50 | erubi (~> 1.11) 51 | rails-dom-testing (~> 2.2) 52 | rails-html-sanitizer (~> 1.6) 53 | activejob (8.0.1) 54 | activesupport (= 8.0.1) 55 | globalid (>= 0.3.6) 56 | activemodel (8.0.1) 57 | activesupport (= 8.0.1) 58 | activerecord (8.0.1) 59 | activemodel (= 8.0.1) 60 | activesupport (= 8.0.1) 61 | timeout (>= 0.4.0) 62 | activestorage (8.0.1) 63 | actionpack (= 8.0.1) 64 | activejob (= 8.0.1) 65 | activerecord (= 8.0.1) 66 | activesupport (= 8.0.1) 67 | marcel (~> 1.0) 68 | activesupport (8.0.1) 69 | base64 70 | benchmark (>= 0.3) 71 | bigdecimal 72 | concurrent-ruby (~> 1.0, >= 1.3.1) 73 | connection_pool (>= 2.2.5) 74 | drb 75 | i18n (>= 1.6, < 2) 76 | logger (>= 1.4.2) 77 | minitest (>= 5.1) 78 | securerandom (>= 0.3) 79 | tzinfo (~> 2.0, >= 2.0.5) 80 | uri (>= 0.13.1) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | net-imap (0.5.5) 112 | date 113 | net-protocol 114 | net-pop (0.1.2) 115 | net-protocol 116 | net-protocol (0.2.2) 117 | timeout 118 | net-smtp (0.5.0) 119 | net-protocol 120 | nio4r (2.7.4) 121 | nokogiri (1.18.1-aarch64-linux-gnu) 122 | racc (~> 1.4) 123 | nokogiri (1.18.1-aarch64-linux-musl) 124 | racc (~> 1.4) 125 | nokogiri (1.18.1-arm-linux-gnu) 126 | racc (~> 1.4) 127 | nokogiri (1.18.1-arm-linux-musl) 128 | racc (~> 1.4) 129 | nokogiri (1.18.1-arm64-darwin) 130 | racc (~> 1.4) 131 | nokogiri (1.18.1-x86_64-darwin) 132 | racc (~> 1.4) 133 | nokogiri (1.18.1-x86_64-linux-gnu) 134 | racc (~> 1.4) 135 | nokogiri (1.18.1-x86_64-linux-musl) 136 | racc (~> 1.4) 137 | pg (1.5.9) 138 | prettier_print (1.2.1) 139 | psych (5.2.2) 140 | date 141 | stringio 142 | racc (1.8.1) 143 | rack (3.1.8) 144 | rack-session (2.1.0) 145 | base64 (>= 0.1.0) 146 | rack (>= 3.0.0) 147 | rack-test (2.2.0) 148 | rack (>= 1.3) 149 | rackup (2.2.1) 150 | rack (>= 3) 151 | rails (8.0.1) 152 | actioncable (= 8.0.1) 153 | actionmailbox (= 8.0.1) 154 | actionmailer (= 8.0.1) 155 | actionpack (= 8.0.1) 156 | actiontext (= 8.0.1) 157 | actionview (= 8.0.1) 158 | activejob (= 8.0.1) 159 | activemodel (= 8.0.1) 160 | activerecord (= 8.0.1) 161 | activestorage (= 8.0.1) 162 | activesupport (= 8.0.1) 163 | bundler (>= 1.15.0) 164 | railties (= 8.0.1) 165 | rails-dom-testing (2.2.0) 166 | activesupport (>= 5.0.0) 167 | minitest 168 | nokogiri (>= 1.6) 169 | rails-html-sanitizer (1.6.2) 170 | loofah (~> 2.21) 171 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 172 | railties (8.0.1) 173 | actionpack (= 8.0.1) 174 | activesupport (= 8.0.1) 175 | irb (~> 1.13) 176 | rackup (>= 1.0.0) 177 | rake (>= 12.2) 178 | thor (~> 1.0, >= 1.2.2) 179 | zeitwerk (~> 2.6) 180 | rake (13.2.1) 181 | rdoc (6.10.0) 182 | psych (>= 4.0.0) 183 | reline (0.6.0) 184 | io-console (~> 0.5) 185 | securerandom (0.4.1) 186 | stringio (3.1.2) 187 | syntax_tree (6.2.0) 188 | prettier_print (>= 1.2.0) 189 | thor (1.3.2) 190 | timeout (0.4.3) 191 | tzinfo (2.0.6) 192 | concurrent-ruby (~> 1.0) 193 | uri (1.0.2) 194 | useragent (0.16.11) 195 | websocket-driver (0.7.7) 196 | base64 197 | websocket-extensions (>= 0.1.0) 198 | websocket-extensions (0.1.5) 199 | zeitwerk (2.7.1) 200 | 201 | PLATFORMS 202 | aarch64-linux-gnu 203 | aarch64-linux-musl 204 | arm-linux-gnu 205 | arm-linux-musl 206 | arm64-darwin 207 | x86_64-darwin 208 | x86_64-linux-gnu 209 | x86_64-linux-musl 210 | 211 | DEPENDENCIES 212 | active_record-union_relation! 213 | activerecord (~> 8.0) 214 | minitest 215 | pg 216 | rails 217 | rake 218 | syntax_tree 219 | 220 | BUNDLED WITH 221 | 2.5.16 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveRecord::UnionRelation 2 | 3 | [![Build Status](https://github.com/kddnewton/active_record-union_relation/workflows/Main/badge.svg)](https://github.com/kddnewton/active_record-union_relation/actions) 4 | [![Gem Version](https://img.shields.io/gem/v/active_record-union_relation.svg)](https://rubygems.org/gems/active_record-union_relation) 5 | 6 | There are times when you want to use SQL's [UNION](https://www.w3schools.com/sql/sql_union.asp) operator to pull rows from multiple relations, but you still want to maintain the query-builder interface of ActiveRecord. This gem allows you to do that with minimal syntax. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem "active_record-union_relation" 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle install 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install active_record-union_relation 23 | 24 | ## Usage 25 | 26 | Let's assume you're writing something like a search function, and you want to be able to return a polymorphic relation containing all of the search results. You could maintain a separate index table with links out to the entities or use a more advanced search engine. Or you could perform a `UNION` that searches each table. 27 | 28 | `UNION` subrelations must all have the same number of columns, so first we define the name of the columns that the `UNION` will select, then we define the sources that will become those columns from each subrelation. It makes more sense looking at an example. 29 | 30 | Let's assume we have the following structure with default table names: 31 | 32 | ```ruby 33 | class Comment < ActiveRecord::Base 34 | belongs_to :post 35 | end 36 | 37 | class Post < ActiveRecord::Base 38 | has_many :comments 39 | end 40 | 41 | class Tag < ActiveRecord::Base 42 | end 43 | ``` 44 | 45 | Now, let's pull all of the records matching a specific term. For `Post`, we'll pull `published: true` records that have a `title` matching the term. For `Comment`, we'll pull the records that have a `body` matching the term. And finally, for `Tag`, we'll pull records that have a `name` matching the term. 46 | 47 | ```ruby 48 | # Let's get a local variable that we'll use to reference within each of our 49 | # subqueries. Presumably this would come from some kind of user input. 50 | term = "foo" 51 | 52 | # First, we call ActiveRecord::union. The arguments are the names of the columns 53 | # that will be aliased from each source relation. It also accepts a block that 54 | # is used to configure the union's subqueries. 55 | relation = 56 | ActiveRecord.union(:id, :post_id, :matched) do |union| 57 | # Okay, now we're going to pull the post records into a subquery. First, 58 | # we'll get a posts variable that contains the relation that we want to pull 59 | # just for this one table. That can include any kind of 60 | # joins/conditions/orders etc. that it needs to. In this case we'll need 61 | # published: true and a matching query. 62 | posts = Post.where(published: true).where("title LIKE ?", "%#{term}%") 63 | 64 | # Next, we'll add that posts relation as a subquery into the union. The 65 | # number of arguments here must directly align with the number of arguments 66 | # given to the overall union. In this case to line everything up, we'll 67 | # select id as the id column, nil as a placeholder since we don't need 68 | # anything for the post_id column, and title as the matched column. 69 | union.add posts, :id, nil, :title 70 | 71 | # Next we'll pull the comments relation that we want into its own variable, 72 | # and then add it into the overall union. We'll line up the id column to id, 73 | # the post_id column to post_id, and the body to matched. Since we're 74 | # explicitly pulling post_id, we'll actually be able to call .post on the 75 | # comment records that get pulled since we alias them back when we 76 | # instantiate the objects. 77 | comments = Comment.where("body LIKE ?", "%#{term}%") 78 | union.add comments, :id, :post_id, :body 79 | 80 | # Finally, we'll pull the tag records that we want and add them into the 81 | # overall union as well. 82 | tags = Tag.where("name LIKE ?", "%#{term}%") 83 | union.add tags, :id, nil, :name 84 | end 85 | 86 | # Now we have a relation object that represents the UNION, and we can perform 87 | # all of the mutations that we would normally perform on a relation. 88 | relation.order(matched: :asc) 89 | 90 | # This results in a polymorphic response that once we load the records has 91 | # everything loaded and aliased properly, as in: 92 | # 93 | # [#, 94 | # #, 95 | # #] 96 | ``` 97 | 98 | The query generated in the example above will look something like: 99 | 100 | ```sql 101 | SELECT discriminator, id, post_id, matched 102 | FROM ( 103 | (SELECT 'Post' AS "discriminator", id AS "id", NULL AS "post_id", title AS "matched" FROM "posts" WHERE "posts"."published" = $1 AND (title LIKE '%foo%')) 104 | UNION 105 | (SELECT 'Comment' AS "discriminator", id AS "id", post_id AS "post_id", body AS "matched" FROM "comments" WHERE (body LIKE '%foo%')) 106 | UNION 107 | (SELECT 'Tag' AS "discriminator", id AS "id", NULL AS "post_id", name AS "matched" FROM "tags" WHERE (name LIKE '%foo%')) 108 | ) AS "union" 109 | ORDER BY "matched" ASC 110 | ``` 111 | 112 | ## Development 113 | 114 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 115 | 116 | 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). 117 | 118 | ## Contributing 119 | 120 | Bug reports and pull requests are welcome on GitHub at https://github.com/kddnewton/active_record-union_relation. 121 | 122 | ## License 123 | 124 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 125 | -------------------------------------------------------------------------------- /gemfiles/mysql-ar7/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (7.2.2.1) 11 | actionpack (= 7.2.2.1) 12 | activesupport (= 7.2.2.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (7.2.2.1) 17 | actionpack (= 7.2.2.1) 18 | activejob (= 7.2.2.1) 19 | activerecord (= 7.2.2.1) 20 | activestorage (= 7.2.2.1) 21 | activesupport (= 7.2.2.1) 22 | mail (>= 2.8.0) 23 | actionmailer (7.2.2.1) 24 | actionpack (= 7.2.2.1) 25 | actionview (= 7.2.2.1) 26 | activejob (= 7.2.2.1) 27 | activesupport (= 7.2.2.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (7.2.2.1) 31 | actionview (= 7.2.2.1) 32 | activesupport (= 7.2.2.1) 33 | nokogiri (>= 1.8.5) 34 | racc 35 | rack (>= 2.2.4, < 3.2) 36 | rack-session (>= 1.0.1) 37 | rack-test (>= 0.6.3) 38 | rails-dom-testing (~> 2.2) 39 | rails-html-sanitizer (~> 1.6) 40 | useragent (~> 0.16) 41 | actiontext (7.2.2.1) 42 | actionpack (= 7.2.2.1) 43 | activerecord (= 7.2.2.1) 44 | activestorage (= 7.2.2.1) 45 | activesupport (= 7.2.2.1) 46 | globalid (>= 0.6.0) 47 | nokogiri (>= 1.8.5) 48 | actionview (7.2.2.1) 49 | activesupport (= 7.2.2.1) 50 | builder (~> 3.1) 51 | erubi (~> 1.11) 52 | rails-dom-testing (~> 2.2) 53 | rails-html-sanitizer (~> 1.6) 54 | activejob (7.2.2.1) 55 | activesupport (= 7.2.2.1) 56 | globalid (>= 0.3.6) 57 | activemodel (7.2.2.1) 58 | activesupport (= 7.2.2.1) 59 | activerecord (7.2.2.1) 60 | activemodel (= 7.2.2.1) 61 | activesupport (= 7.2.2.1) 62 | timeout (>= 0.4.0) 63 | activestorage (7.2.2.1) 64 | actionpack (= 7.2.2.1) 65 | activejob (= 7.2.2.1) 66 | activerecord (= 7.2.2.1) 67 | activesupport (= 7.2.2.1) 68 | marcel (~> 1.0) 69 | activesupport (7.2.2.1) 70 | base64 71 | benchmark (>= 0.3) 72 | bigdecimal 73 | concurrent-ruby (~> 1.0, >= 1.3.1) 74 | connection_pool (>= 2.2.5) 75 | drb 76 | i18n (>= 1.6, < 2) 77 | logger (>= 1.4.2) 78 | minitest (>= 5.1) 79 | securerandom (>= 0.3) 80 | tzinfo (~> 2.0, >= 2.0.5) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | mysql2 (0.5.6) 112 | net-imap (0.5.5) 113 | date 114 | net-protocol 115 | net-pop (0.1.2) 116 | net-protocol 117 | net-protocol (0.2.2) 118 | timeout 119 | net-smtp (0.5.0) 120 | net-protocol 121 | nio4r (2.7.4) 122 | nokogiri (1.18.1-aarch64-linux-gnu) 123 | racc (~> 1.4) 124 | nokogiri (1.18.1-aarch64-linux-musl) 125 | racc (~> 1.4) 126 | nokogiri (1.18.1-arm-linux-gnu) 127 | racc (~> 1.4) 128 | nokogiri (1.18.1-arm-linux-musl) 129 | racc (~> 1.4) 130 | nokogiri (1.18.1-arm64-darwin) 131 | racc (~> 1.4) 132 | nokogiri (1.18.1-x86_64-darwin) 133 | racc (~> 1.4) 134 | nokogiri (1.18.1-x86_64-linux-gnu) 135 | racc (~> 1.4) 136 | nokogiri (1.18.1-x86_64-linux-musl) 137 | racc (~> 1.4) 138 | prettier_print (1.2.1) 139 | psych (5.2.2) 140 | date 141 | stringio 142 | racc (1.8.1) 143 | rack (3.1.8) 144 | rack-session (2.1.0) 145 | base64 (>= 0.1.0) 146 | rack (>= 3.0.0) 147 | rack-test (2.2.0) 148 | rack (>= 1.3) 149 | rackup (2.2.1) 150 | rack (>= 3) 151 | rails (7.2.2.1) 152 | actioncable (= 7.2.2.1) 153 | actionmailbox (= 7.2.2.1) 154 | actionmailer (= 7.2.2.1) 155 | actionpack (= 7.2.2.1) 156 | actiontext (= 7.2.2.1) 157 | actionview (= 7.2.2.1) 158 | activejob (= 7.2.2.1) 159 | activemodel (= 7.2.2.1) 160 | activerecord (= 7.2.2.1) 161 | activestorage (= 7.2.2.1) 162 | activesupport (= 7.2.2.1) 163 | bundler (>= 1.15.0) 164 | railties (= 7.2.2.1) 165 | rails-dom-testing (2.2.0) 166 | activesupport (>= 5.0.0) 167 | minitest 168 | nokogiri (>= 1.6) 169 | rails-html-sanitizer (1.6.2) 170 | loofah (~> 2.21) 171 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 172 | railties (7.2.2.1) 173 | actionpack (= 7.2.2.1) 174 | activesupport (= 7.2.2.1) 175 | irb (~> 1.13) 176 | rackup (>= 1.0.0) 177 | rake (>= 12.2) 178 | thor (~> 1.0, >= 1.2.2) 179 | zeitwerk (~> 2.6) 180 | rake (13.2.1) 181 | rdoc (6.10.0) 182 | psych (>= 4.0.0) 183 | reline (0.6.0) 184 | io-console (~> 0.5) 185 | securerandom (0.4.1) 186 | stringio (3.1.2) 187 | syntax_tree (6.2.0) 188 | prettier_print (>= 1.2.0) 189 | thor (1.3.2) 190 | timeout (0.4.3) 191 | tzinfo (2.0.6) 192 | concurrent-ruby (~> 1.0) 193 | useragent (0.16.11) 194 | websocket-driver (0.7.7) 195 | base64 196 | websocket-extensions (>= 0.1.0) 197 | websocket-extensions (0.1.5) 198 | zeitwerk (2.7.1) 199 | 200 | PLATFORMS 201 | aarch64-linux-gnu 202 | aarch64-linux-musl 203 | arm-linux-gnu 204 | arm-linux-musl 205 | arm64-darwin 206 | x86_64-darwin 207 | x86_64-linux-gnu 208 | x86_64-linux-musl 209 | 210 | DEPENDENCIES 211 | active_record-union_relation! 212 | activerecord (~> 7.0) 213 | minitest 214 | mysql2 215 | rails 216 | rake 217 | syntax_tree 218 | 219 | BUNDLED WITH 220 | 2.5.16 221 | -------------------------------------------------------------------------------- /gemfiles/sqlite-ar8/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | active_record-union_relation (0.4.0) 5 | activerecord (>= 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (8.0.1) 11 | actionpack (= 8.0.1) 12 | activesupport (= 8.0.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | zeitwerk (~> 2.6) 16 | actionmailbox (8.0.1) 17 | actionpack (= 8.0.1) 18 | activejob (= 8.0.1) 19 | activerecord (= 8.0.1) 20 | activestorage (= 8.0.1) 21 | activesupport (= 8.0.1) 22 | mail (>= 2.8.0) 23 | actionmailer (8.0.1) 24 | actionpack (= 8.0.1) 25 | actionview (= 8.0.1) 26 | activejob (= 8.0.1) 27 | activesupport (= 8.0.1) 28 | mail (>= 2.8.0) 29 | rails-dom-testing (~> 2.2) 30 | actionpack (8.0.1) 31 | actionview (= 8.0.1) 32 | activesupport (= 8.0.1) 33 | nokogiri (>= 1.8.5) 34 | rack (>= 2.2.4) 35 | rack-session (>= 1.0.1) 36 | rack-test (>= 0.6.3) 37 | rails-dom-testing (~> 2.2) 38 | rails-html-sanitizer (~> 1.6) 39 | useragent (~> 0.16) 40 | actiontext (8.0.1) 41 | actionpack (= 8.0.1) 42 | activerecord (= 8.0.1) 43 | activestorage (= 8.0.1) 44 | activesupport (= 8.0.1) 45 | globalid (>= 0.6.0) 46 | nokogiri (>= 1.8.5) 47 | actionview (8.0.1) 48 | activesupport (= 8.0.1) 49 | builder (~> 3.1) 50 | erubi (~> 1.11) 51 | rails-dom-testing (~> 2.2) 52 | rails-html-sanitizer (~> 1.6) 53 | activejob (8.0.1) 54 | activesupport (= 8.0.1) 55 | globalid (>= 0.3.6) 56 | activemodel (8.0.1) 57 | activesupport (= 8.0.1) 58 | activerecord (8.0.1) 59 | activemodel (= 8.0.1) 60 | activesupport (= 8.0.1) 61 | timeout (>= 0.4.0) 62 | activestorage (8.0.1) 63 | actionpack (= 8.0.1) 64 | activejob (= 8.0.1) 65 | activerecord (= 8.0.1) 66 | activesupport (= 8.0.1) 67 | marcel (~> 1.0) 68 | activesupport (8.0.1) 69 | base64 70 | benchmark (>= 0.3) 71 | bigdecimal 72 | concurrent-ruby (~> 1.0, >= 1.3.1) 73 | connection_pool (>= 2.2.5) 74 | drb 75 | i18n (>= 1.6, < 2) 76 | logger (>= 1.4.2) 77 | minitest (>= 5.1) 78 | securerandom (>= 0.3) 79 | tzinfo (~> 2.0, >= 2.0.5) 80 | uri (>= 0.13.1) 81 | base64 (0.2.0) 82 | benchmark (0.4.0) 83 | bigdecimal (3.1.9) 84 | builder (3.3.0) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.4.1) 89 | drb (2.2.1) 90 | erubi (1.13.1) 91 | globalid (1.2.1) 92 | activesupport (>= 6.1) 93 | i18n (1.14.6) 94 | concurrent-ruby (~> 1.0) 95 | io-console (0.8.0) 96 | irb (1.14.3) 97 | rdoc (>= 4.0.0) 98 | reline (>= 0.4.2) 99 | logger (1.6.4) 100 | loofah (2.24.0) 101 | crass (~> 1.0.2) 102 | nokogiri (>= 1.12.0) 103 | mail (2.8.1) 104 | mini_mime (>= 0.1.1) 105 | net-imap 106 | net-pop 107 | net-smtp 108 | marcel (1.0.4) 109 | mini_mime (1.1.5) 110 | minitest (5.25.4) 111 | net-imap (0.5.5) 112 | date 113 | net-protocol 114 | net-pop (0.1.2) 115 | net-protocol 116 | net-protocol (0.2.2) 117 | timeout 118 | net-smtp (0.5.0) 119 | net-protocol 120 | nio4r (2.7.4) 121 | nokogiri (1.18.1-aarch64-linux-gnu) 122 | racc (~> 1.4) 123 | nokogiri (1.18.1-aarch64-linux-musl) 124 | racc (~> 1.4) 125 | nokogiri (1.18.1-arm-linux-gnu) 126 | racc (~> 1.4) 127 | nokogiri (1.18.1-arm-linux-musl) 128 | racc (~> 1.4) 129 | nokogiri (1.18.1-arm64-darwin) 130 | racc (~> 1.4) 131 | nokogiri (1.18.1-x86_64-darwin) 132 | racc (~> 1.4) 133 | nokogiri (1.18.1-x86_64-linux-gnu) 134 | racc (~> 1.4) 135 | nokogiri (1.18.1-x86_64-linux-musl) 136 | racc (~> 1.4) 137 | prettier_print (1.2.1) 138 | psych (5.2.2) 139 | date 140 | stringio 141 | racc (1.8.1) 142 | rack (3.1.8) 143 | rack-session (2.1.0) 144 | base64 (>= 0.1.0) 145 | rack (>= 3.0.0) 146 | rack-test (2.2.0) 147 | rack (>= 1.3) 148 | rackup (2.2.1) 149 | rack (>= 3) 150 | rails (8.0.1) 151 | actioncable (= 8.0.1) 152 | actionmailbox (= 8.0.1) 153 | actionmailer (= 8.0.1) 154 | actionpack (= 8.0.1) 155 | actiontext (= 8.0.1) 156 | actionview (= 8.0.1) 157 | activejob (= 8.0.1) 158 | activemodel (= 8.0.1) 159 | activerecord (= 8.0.1) 160 | activestorage (= 8.0.1) 161 | activesupport (= 8.0.1) 162 | bundler (>= 1.15.0) 163 | railties (= 8.0.1) 164 | rails-dom-testing (2.2.0) 165 | activesupport (>= 5.0.0) 166 | minitest 167 | nokogiri (>= 1.6) 168 | rails-html-sanitizer (1.6.2) 169 | loofah (~> 2.21) 170 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) 171 | railties (8.0.1) 172 | actionpack (= 8.0.1) 173 | activesupport (= 8.0.1) 174 | irb (~> 1.13) 175 | rackup (>= 1.0.0) 176 | rake (>= 12.2) 177 | thor (~> 1.0, >= 1.2.2) 178 | zeitwerk (~> 2.6) 179 | rake (13.2.1) 180 | rdoc (6.10.0) 181 | psych (>= 4.0.0) 182 | reline (0.6.0) 183 | io-console (~> 0.5) 184 | securerandom (0.4.1) 185 | sqlite3 (2.5.0-aarch64-linux-gnu) 186 | sqlite3 (2.5.0-aarch64-linux-musl) 187 | sqlite3 (2.5.0-arm-linux-gnu) 188 | sqlite3 (2.5.0-arm-linux-musl) 189 | sqlite3 (2.5.0-arm64-darwin) 190 | sqlite3 (2.5.0-x86_64-darwin) 191 | sqlite3 (2.5.0-x86_64-linux-gnu) 192 | sqlite3 (2.5.0-x86_64-linux-musl) 193 | stringio (3.1.2) 194 | syntax_tree (6.2.0) 195 | prettier_print (>= 1.2.0) 196 | thor (1.3.2) 197 | timeout (0.4.3) 198 | tzinfo (2.0.6) 199 | concurrent-ruby (~> 1.0) 200 | uri (1.0.2) 201 | useragent (0.16.11) 202 | websocket-driver (0.7.7) 203 | base64 204 | websocket-extensions (>= 0.1.0) 205 | websocket-extensions (0.1.5) 206 | zeitwerk (2.7.1) 207 | 208 | PLATFORMS 209 | aarch64-linux 210 | aarch64-linux-gnu 211 | aarch64-linux-musl 212 | arm-linux 213 | arm-linux-gnu 214 | arm-linux-musl 215 | arm64-darwin 216 | x86_64-darwin 217 | x86_64-linux 218 | x86_64-linux-gnu 219 | x86_64-linux-musl 220 | 221 | DEPENDENCIES 222 | active_record-union_relation! 223 | activerecord (~> 8.0) 224 | minitest 225 | rails 226 | rake 227 | sqlite3 (~> 2.0) 228 | syntax_tree 229 | 230 | BUNDLED WITH 231 | 2.5.16 232 | -------------------------------------------------------------------------------- /lib/active_record/union_relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "active_record/union_relation/version" 5 | 6 | if defined?(ActiveRecord::Result::IndexedRow) 7 | raise if ActiveRecord::Result::IndexedRow.method_defined?(:each) 8 | 9 | class ActiveRecord::Result::IndexedRow 10 | # Monkey-patch in the #each method so that we can treat it like a hash. 11 | def each(&block) 12 | @column_indexes.each { |column, index| yield column, @row[index] } 13 | end 14 | end 15 | end 16 | 17 | module ActiveRecord 18 | class UnionRelation 19 | class Error < StandardError 20 | end 21 | 22 | # Unions require that the number of columns coming from each subrelation all 23 | # match. When we pull the attributes out an instantiate the actual objects, 24 | # we then map them back to the original attribute names. 25 | class MismatchedColumnsError < Error 26 | def initialize(columns, sources) 27 | super("Expected #{columns.length} columns but got #{sources.length}") 28 | end 29 | end 30 | 31 | # If you attempt to use a union before you've added any subqueries, we'll 32 | # raise this error so there's not some weird undefined method behavior. 33 | class NoConfiguredSubqueriesError < Error 34 | def initialize 35 | super("No subqueries have been configured for this union") 36 | end 37 | end 38 | 39 | # This represents a combination of an ActiveRecord::Relation and a set of 40 | # columns that it will pull. 41 | class Subquery 42 | # Sometimes you need some columns in some subqeries that you don't need in 43 | # others. In order to accomplish that and still maintain the matching 44 | # number of columns, you can put a null in space of a column instead. 45 | NULL = Arel.sql("NULL") 46 | 47 | # A model name for a model that is not using single-table inheritance. In 48 | # this case we use the model name itself as the discriminator and only 49 | # need one entry in the mappings hash that maps records to the columns 50 | # that we are pulling from the result. 51 | class SingleModelName 52 | attr_reader :name 53 | 54 | def initialize(name) 55 | @name = name 56 | end 57 | 58 | def each_name 59 | yield name 60 | end 61 | 62 | def to_sql 63 | Arel.sql("'#{name}'") 64 | end 65 | end 66 | 67 | # A model name for a model that is using single-table inheritance. In this 68 | # case we use the inheritance column as the discriminator and need to 69 | # include all of the subclasses in the mappings hash. 70 | class MultiModelName 71 | attr_reader :inheritance_column, :names 72 | 73 | def initialize(inheritance_column, names) 74 | @inheritance_column = inheritance_column 75 | @names = names 76 | end 77 | 78 | def each_name(&block) 79 | names.each(&block) 80 | end 81 | 82 | def to_sql 83 | Arel.sql(inheritance_column) 84 | end 85 | end 86 | 87 | attr_reader :relation, :model_name, :sources 88 | 89 | def initialize(relation, sources) 90 | @relation = relation 91 | 92 | model = relation.model 93 | @model_name = 94 | if model._has_attribute?(model.inheritance_column) 95 | MultiModelName.new( 96 | quote_column_name(model.inheritance_column), 97 | model.descendants.map(&:name) 98 | ) 99 | else 100 | SingleModelName.new(model.name) 101 | end 102 | 103 | @sources = sources.map { |source| source ? source.to_s : NULL } 104 | end 105 | 106 | def to_arel(columns, discriminator) 107 | relation.select( 108 | model_name.to_sql.as(quote_column_name(discriminator)), 109 | *sources 110 | .zip(columns) 111 | .map do |(source, column)| 112 | Arel.sql(source.to_s).as(quote_column_name(column)) 113 | end 114 | ).arel 115 | end 116 | 117 | def merge_mappings(mappings, columns) 118 | # Remove the scope_name/table_name when using table_name.column 119 | mapping = 120 | columns.zip(sources.map { |source| source.split(".").last }).to_h 121 | model_name.each_name { |name| mappings[name] = mapping } 122 | end 123 | 124 | private 125 | 126 | def quote_column_name(name) 127 | relation.model.connection.quote_column_name(name) 128 | end 129 | end 130 | 131 | attr_reader :columns, :discriminator, :subqueries 132 | 133 | def initialize(columns, discriminator) 134 | @columns = columns.map(&:to_s) 135 | @discriminator = discriminator 136 | @subqueries = [] 137 | end 138 | 139 | # Adds a subquery to the overall union. 140 | def add(relation, *sources) 141 | if columns.length != sources.length 142 | raise MismatchedColumnsError.new(columns, sources) 143 | end 144 | 145 | subqueries << Subquery.new(relation, sources) 146 | end 147 | 148 | # Creates an ActiveRecord::Relation object that will pull all of the 149 | # subqueries together. 150 | def all 151 | raise NoConfiguredSubqueriesError if subqueries.empty? 152 | 153 | model = subqueries.first.relation.model 154 | subclass_for(model).from(union_for(model)).select(discriminator, *columns) 155 | end 156 | 157 | private 158 | 159 | def subclass_for(model) 160 | discriminator = self.discriminator 161 | 162 | mappings = {} 163 | subqueries.each { |subquery| subquery.merge_mappings(mappings, columns) } 164 | 165 | Class.new(model) do 166 | # Set the inheritance column and register the discriminator as a string 167 | # column so that Active Record will instantiate the right subclass. 168 | self.inheritance_column = discriminator 169 | attribute inheritance_column, :string 170 | 171 | define_singleton_method(:instantiate) do |attrs, columns = {}, &block| 172 | mapped = {} 173 | mapping = mappings[attrs[inheritance_column]] 174 | 175 | # Map the result set columns back to their original source column 176 | # names. This ensures that even though the UNION saw them as the same 177 | # columns our resulting records see them as their original names. 178 | attrs.each do |key, value| 179 | case mapping[key] 180 | when Subquery::NULL 181 | # Ignore columns that didn't have a value. 182 | when nil 183 | # If we don't have a mapping for this column, then it's the 184 | # discriminator. Map that column directly. 185 | mapped[key] = value 186 | else 187 | # Otherwise, use the mapping to map the column back to its 188 | # original name. 189 | mapped[mapping[key]] = value 190 | end 191 | end 192 | 193 | # Now that we've mapped all of the columns, we can call super with the 194 | # mapped values. 195 | super(mapped, columns, &block) 196 | end 197 | 198 | # Override the default find_sti_class method because it does sanity 199 | # checks to ensure that the class you're trying to instantiate is a 200 | # subclass of the current class. Since we want to explicitly _not_ do 201 | # that, we will instead just check that it is a valid model class. 202 | define_singleton_method(:find_sti_class) do |type_name| 203 | type = type_name.constantize 204 | type < ActiveRecord::Base ? type : super(type_name) 205 | end 206 | end 207 | end 208 | 209 | def union_for(model) 210 | Arel::Nodes::As.new( 211 | if subqueries.one? 212 | subqueries.first.to_arel(columns, discriminator) 213 | else 214 | subqueries 215 | .map { |subquery| subquery.to_arel(columns, discriminator).ast } 216 | .inject { |left, right| Arel::Nodes::Union.new(left, right) } 217 | end, 218 | Arel.sql(model.connection.quote_table_name("union")) 219 | ) 220 | end 221 | end 222 | 223 | # Unions require that you have an equal number of columns from each 224 | # subquery. The columns argument being passed here is any number of 225 | # symbols that represent the columns that will be queried. When you then go 226 | # to add sources into the union you'll need to pass the same number of 227 | # columns. 228 | # 229 | # One additional column will be added to the query in order to discriminate 230 | # between all of the unioned types. Then when the objects are going to be 231 | # instantiated, we map the columns back to their original names. 232 | def self.union(*columns, discriminator: "discriminator") 233 | UnionRelation.new(columns, discriminator).tap { |union| yield union }.all 234 | end 235 | end 236 | --------------------------------------------------------------------------------