├── .codeclimate.yml ├── .github ├── FUNDING.yml └── workflows │ ├── run_tests.yml │ └── run_tests_on_head.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── ALTERNATIVES_PROBLEMS.md ├── CHANGELOG.md ├── EXAMPLES.md ├── Gemfile ├── INTRODUCTION.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── activerecord_where_assoc.gemspec ├── bin ├── console ├── fixcop ├── setup ├── testall └── testone ├── docs ├── ActiveRecordWhereAssoc.html ├── ActiveRecordWhereAssoc │ ├── RelationReturningMethods.html │ └── SqlReturningMethods.html ├── css │ ├── fonts.css │ └── rdoc.css ├── docs_customization.css ├── fonts │ ├── Lato-Light.ttf │ ├── Lato-LightItalic.ttf │ ├── Lato-Regular.ttf │ ├── Lato-RegularItalic.ttf │ ├── SourceCodePro-Bold.ttf │ └── SourceCodePro-Regular.ttf ├── images │ ├── add.png │ ├── arrow_up.png │ ├── brick.png │ ├── brick_link.png │ ├── bug.png │ ├── bullet_black.png │ ├── bullet_toggle_minus.png │ ├── bullet_toggle_plus.png │ ├── date.png │ ├── delete.png │ ├── find.png │ ├── loadingAnimation.gif │ ├── macFFBgHack.png │ ├── package.png │ ├── page_green.png │ ├── page_white_text.png │ ├── page_white_width.png │ ├── plugin.png │ ├── ruby.png │ ├── tag_blue.png │ ├── tag_green.png │ ├── transparent.png │ ├── wrench.png │ ├── wrench_orange.png │ └── zoom.png ├── index.html ├── js │ ├── darkfish.js │ ├── navigation.js │ ├── navigation.js.gz │ ├── search.js │ ├── search_index.js │ ├── search_index.js.gz │ ├── searcher.js │ └── searcher.js.gz └── table_of_contents.html ├── docs_customization.css ├── examples ├── examples.rb ├── models.rb ├── schema.rb └── some_data.rb ├── gemfiles ├── rails_4_1.gemfile ├── rails_4_2.gemfile ├── rails_5_0.gemfile ├── rails_5_1.gemfile ├── rails_5_2.gemfile ├── rails_6_0.gemfile ├── rails_6_1.gemfile ├── rails_7_0.gemfile ├── rails_7_1.gemfile ├── rails_7_2.gemfile ├── rails_8_0.gemfile ├── rails_head.gemfile └── readme.txt ├── lib ├── active_record_where_assoc.rb ├── active_record_where_assoc │ ├── active_record_compat.rb │ ├── core_logic.rb │ ├── exceptions.rb │ ├── relation_returning_delegates.rb │ ├── relation_returning_methods.rb │ ├── sql_returning_methods.rb │ └── version.rb └── activerecord_where_assoc.rb └── test ├── support ├── base_test_model.rb ├── custom_asserts.rb ├── database_setup.rb ├── load_test_env.rb ├── models.rb └── schema.rb ├── test_helper.rb └── tests ├── conditions └── wa_has_one_test.rb ├── raw_sql_test.rb ├── scoping ├── wa_belongs_to_test.rb ├── wa_has_and_belongs_to_many_test.rb ├── wa_has_many_test.rb ├── wa_has_one_test.rb ├── wa_polymorphic_belongs_to_test.rb ├── wa_polymorphic_has_many_test.rb ├── wa_polymorphic_has_one_test.rb └── wa_with_no_possible_records_to_return_test.rb ├── wa_abstract_model_test.rb ├── wa_composite_keys_test.rb ├── wa_count_has_many_test.rb ├── wa_count_has_one_test.rb ├── wa_count_left_side_test.rb ├── wa_count_operators_test.rb ├── wa_count_swapped_operands_test.rb ├── wa_exceptions_test.rb ├── wa_has_one_exclusion_test.rb ├── wa_has_one_optimization_test.rb ├── wa_last_equality_wins_test.rb ├── wa_limit_offset_test.rb ├── wa_null_relation_test.rb ├── wa_options_test.rb ├── wa_recursive_association_test.rb ├── wa_sti_test.rb ├── wa_table_name_with_schema_test.rb └── wa_through_inter_macro_test.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | channel: rubocop-0-54 5 | 6 | ratings: 7 | paths: 8 | - "lib/**/*" 9 | - "**.rb" 10 | 11 | exclude_patterns: 12 | - 'docs/' 13 | # These are the defaults that need to be re-applied because I need a custom choice. 14 | - 'script/' 15 | - '**/spec/' 16 | - '**/test/' 17 | - '**/tests/' 18 | - '**/vendor/' 19 | - '**/*_test.go' 20 | - '**/*.d.ts' 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: MaxLap 2 | ko_fi: maxlap 3 | tidelift: "rubygems/activerecord_where_assoc" 4 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Test supported versions 2 | 3 | # Need the quotes, otherwise YAML.load, which we uses to generate run_tests_on_head.yml will interpret this 4 | # as the boolean `true`... 5 | 'on': 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | schedule: 11 | - cron: '0 10 1-7 * 6' 12 | workflow_dispatch: 13 | branches: [ master ] 14 | 15 | env: 16 | PGUSER: postgres 17 | PGPASSWORD: postgres 18 | MYSQL_USER: root 19 | MYSQL_PASSWORD: root 20 | # This set to false for run_tests_on_head by the rakefile 21 | CACHE_DEPENDENCIES: 'true' 22 | # Dumb workaround since it's not possible to clear caches in Github Actions 23 | CACHE_VERSION: '3' 24 | 25 | jobs: 26 | test: 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | include: 31 | - gemfile: gemfiles/rails_8_0.gemfile 32 | ruby_version: '3.4' 33 | - gemfile: gemfiles/rails_8_0.gemfile 34 | ruby_version: '3.4' 35 | 36 | - gemfile: gemfiles/rails_7_2.gemfile 37 | ruby_version: '3.3' 38 | - gemfile: gemfiles/rails_7_2.gemfile 39 | ruby_version: '3.1' 40 | 41 | - gemfile: gemfiles/rails_7_1.gemfile 42 | ruby_version: '3.2' 43 | - gemfile: gemfiles/rails_7_1.gemfile 44 | ruby_version: 2.7 45 | 46 | - gemfile: gemfiles/rails_7_0.gemfile 47 | ruby_version: '3.1' 48 | - gemfile: gemfiles/rails_7_0.gemfile 49 | ruby_version: 2.7 50 | 51 | - gemfile: gemfiles/rails_6_1.gemfile 52 | ruby_version: '3.0' 53 | - gemfile: gemfiles/rails_6_1.gemfile 54 | ruby_version: 2.5 55 | 56 | - gemfile: gemfiles/rails_6_0.gemfile 57 | ruby_version: 2.7 58 | - gemfile: gemfiles/rails_6_0.gemfile 59 | ruby_version: 2.5 60 | 61 | - gemfile: gemfiles/rails_5_2.gemfile 62 | ruby_version: 2.6 63 | - gemfile: gemfiles/rails_5_2.gemfile 64 | ruby_version: 2.3 65 | 66 | - gemfile: gemfiles/rails_5_1.gemfile 67 | ruby_version: 2.5 68 | - gemfile: gemfiles/rails_5_1.gemfile 69 | ruby_version: 2.3 70 | 71 | - gemfile: gemfiles/rails_5_0.gemfile 72 | ruby_version: 2.4 73 | - gemfile: gemfiles/rails_5_0.gemfile 74 | ruby_version: 2.3 75 | 76 | - gemfile: gemfiles/rails_4_2.gemfile 77 | ruby_version: 2.4 78 | - gemfile: gemfiles/rails_4_2.gemfile 79 | ruby_version: 2.1 80 | 81 | - gemfile: gemfiles/rails_4_1.gemfile 82 | ruby_version: 2.3 83 | - gemfile: gemfiles/rails_4_1.gemfile 84 | ruby_version: 2.1 85 | 86 | runs-on: ubuntu-latest 87 | services: 88 | db: 89 | image: postgres:11 90 | ports: ['5432:5432'] 91 | options: >- 92 | --health-cmd pg_isready 93 | --health-interval 10s 94 | --health-timeout 5s 95 | --health-retries 5 96 | env: 97 | POSTGRES_USER: ${{env.PGUSER}} 98 | POSTGRES_PASSWORD: ${{env.PGUSER}} 99 | env: 100 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 101 | BUNDLE_PATH: vendor/bundle 102 | 103 | steps: 104 | - uses: actions/checkout@v2 105 | - run: sudo service mysql start 106 | - name: Set up Ruby 107 | uses: ruby/setup-ruby@v1 108 | with: 109 | ruby-version: ${{ matrix.ruby_version }} 110 | - uses: actions/cache@v4 111 | if: ${{ env.CACHE_DEPENDENCIES == 'true' }} 112 | with: 113 | # The path given to bundler is used relatively to the directory of the gemfile 114 | # I keep the different gemfiles in the 'gemfiles' directory, so the path to cache is also there. 115 | path: gemfiles/vendor/bundle 116 | key: ResetCaches1-${{ runner.os }}-gems-${{ env.CACHE_VERSION }}-ruby${{ matrix.ruby_version }}-${{ matrix.gemfile }}-${{ hashFiles(matrix.gemfile) }}-${{ hashFiles('activerecord_where_assoc.gemspec') }} 117 | restore-keys: ResetCaches1-${{ runner.os }}-gems-${{ env.CACHE_VERSION }}-ruby${{ matrix.ruby_version }}-${{ matrix.gemfile }} 118 | - name: Install dependencies 119 | run: bundle install --jobs 4 --retry 3 120 | - run: psql --host=localhost --port=5432 -c 'CREATE DATABASE activerecord_where_assoc' 121 | - run: mysql -h 127.0.0.1 -u "${{ env.MYSQL_USER }}" -p${{ env.MYSQL_PASSWORD }} -e 'CREATE DATABASE activerecord_where_assoc' 122 | - run: DB=sqlite3 bundle exec rake test 123 | - run: DB=pg bundle exec rake test 124 | # PG build segfaults on older ruby version, no idea why and painful to debug, so just skip them. 125 | if: ${{ matrix.ruby_version >= 2.4 || matrix.ruby_version == 'head' }} 126 | - run: DB=mysql bundle exec rake test 127 | # MySQL build segfaults on older ruby version, no idea why and painful to debug, so just skip them. 128 | if: ${{ matrix.ruby_version >= 2.4 || matrix.ruby_version == 'head' }} 129 | -------------------------------------------------------------------------------- /.github/workflows/run_tests_on_head.yml: -------------------------------------------------------------------------------- 1 | # This file is generated from run_tests.yml, changes here will be lost next time `rake` is run 2 | --- 3 | name: Test future versions 4 | 'on': 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | schedule: 12 | - cron: 0 10 1-7 * 6 13 | workflow_dispatch: 14 | branches: 15 | - master 16 | env: 17 | PGUSER: postgres 18 | PGPASSWORD: postgres 19 | MYSQL_USER: root 20 | MYSQL_PASSWORD: root 21 | CACHE_DEPENDENCIES: false 22 | CACHE_VERSION: '3' 23 | jobs: 24 | test: 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | include: 29 | - gemfile: gemfiles/rails_head.gemfile 30 | ruby_version: head 31 | - gemfile: gemfiles/rails_head.gemfile 32 | ruby_version: '3.4' 33 | - gemfile: gemfiles/rails_8_0.gemfile 34 | ruby_version: head 35 | runs-on: ubuntu-latest 36 | services: 37 | db: 38 | image: postgres:11 39 | ports: 40 | - 5432:5432 41 | options: "--health-cmd pg_isready --health-interval 10s --health-timeout 5s 42 | --health-retries 5" 43 | env: 44 | POSTGRES_USER: "${{env.PGUSER}}" 45 | POSTGRES_PASSWORD: "${{env.PGUSER}}" 46 | env: 47 | BUNDLE_GEMFILE: "${{ matrix.gemfile }}" 48 | BUNDLE_PATH: vendor/bundle 49 | steps: 50 | - uses: actions/checkout@v2 51 | - run: sudo service mysql start 52 | - name: Set up Ruby 53 | uses: ruby/setup-ruby@v1 54 | with: 55 | ruby-version: "${{ matrix.ruby_version }}" 56 | - uses: actions/cache@v4 57 | if: "${{ env.CACHE_DEPENDENCIES == 'true' }}" 58 | with: 59 | path: gemfiles/vendor/bundle 60 | key: ResetCaches1-${{ runner.os }}-gems-${{ env.CACHE_VERSION }}-ruby${{ matrix.ruby_version 61 | }}-${{ matrix.gemfile }}-${{ hashFiles(matrix.gemfile) }}-${{ hashFiles('activerecord_where_assoc.gemspec') 62 | }} 63 | restore-keys: ResetCaches1-${{ runner.os }}-gems-${{ env.CACHE_VERSION }}-ruby${{ 64 | matrix.ruby_version }}-${{ matrix.gemfile }} 65 | - name: Install dependencies 66 | run: bundle install --jobs 4 --retry 3 67 | - run: psql --host=localhost --port=5432 -c 'CREATE DATABASE activerecord_where_assoc' 68 | - run: mysql -h 127.0.0.1 -u "${{ env.MYSQL_USER }}" -p${{ env.MYSQL_PASSWORD 69 | }} -e 'CREATE DATABASE activerecord_where_assoc' 70 | - run: DB=sqlite3 bundle exec rake test 71 | - run: DB=pg bundle exec rake test 72 | if: "${{ matrix.ruby_version >= 2.4 || matrix.ruby_version == 'head' }}" 73 | - run: DB=mysql bundle exec rake test 74 | if: "${{ matrix.ruby_version >= 2.4 || matrix.ruby_version == 'head' }}" 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /gemfiles/.bundle 3 | /.yardoc 4 | /.idea 5 | /Gemfile.lock 6 | /_yardoc/ 7 | /coverage/ 8 | /docs/created.rid 9 | /gemfiles/*.gemfile.lock 10 | /pkg/ 11 | /spec/reports/ 12 | /tmp/ 13 | /deep_cover 14 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | Include: 4 | - Rakefile 5 | - config.ru 6 | - bin/console 7 | - bin/fixcop 8 | - bin/testall 9 | - gemfiles/*.gemfile 10 | - lib/**/*.rake 11 | Exclude: 12 | - db/schema.rb 13 | - _*/**/* 14 | - private/**/* 15 | - public/**/* 16 | 17 | TargetRubyVersion: 2.1 18 | TargetRailsVersion: 5.1 19 | 20 | Layout/EmptyLines: 21 | Enabled: false 22 | 23 | Layout/EmptyLineBetweenDefs: 24 | NumberOfEmptyLines: [1, 2] 25 | 26 | Layout/FirstParameterIndentation: 27 | IndentationWidth: 4 28 | 29 | Layout/IndentArray: 30 | EnforcedStyle: align_brackets 31 | 32 | Layout/IndentHash: 33 | EnforcedStyle: align_braces 34 | 35 | Layout/MultilineArrayBraceLayout: 36 | EnforcedStyle: new_line 37 | 38 | Layout/MultilineHashBraceLayout: 39 | EnforcedStyle: new_line 40 | 41 | Lint/EmptyWhen: 42 | Enabled: false 43 | 44 | Lint/MissingCopEnableDirective: 45 | Enabled: false 46 | 47 | # Annoying when used with some api that have blocks with sometimes useful parameters 48 | Lint/UnusedBlockArgument: 49 | Enabled: false 50 | 51 | # Annoying because it wines for &block parameters, which helps make signature more explicit 52 | Lint/UnusedMethodArgument: 53 | Enabled: false 54 | 55 | Metrics/AbcSize: 56 | Enabled: false 57 | 58 | Metrics/BlockNesting: 59 | Enabled: false 60 | 61 | Metrics/BlockLength: 62 | Enabled: false 63 | 64 | Metrics/ClassLength: 65 | Enabled: false 66 | 67 | Metrics/CyclomaticComplexity: 68 | Enabled: false 69 | 70 | # Really, you aim for less than that, but we won't bug you unless you reach 150 71 | Metrics/LineLength: 72 | IgnoreCopDirectives: true 73 | Max: 150 74 | 75 | Metrics/MethodLength: 76 | Enabled: false 77 | 78 | Metrics/ModuleLength: 79 | Enabled: false 80 | 81 | Metrics/ParameterLists: 82 | Enabled: false 83 | 84 | Metrics/PerceivedComplexity: 85 | Enabled: false 86 | 87 | Naming/FileName: 88 | Enabled: false 89 | 90 | Naming/VariableNumber: 91 | Enabled: false 92 | 93 | Performance/RedundantBlockCall: 94 | Enabled: false 95 | 96 | Rails/FilePath: 97 | Enabled: false 98 | 99 | Style/AsciiComments: 100 | Enabled: false 101 | 102 | Style/ClassAndModuleChildren: 103 | Enabled: false 104 | 105 | Style/ConditionalAssignment: 106 | Enabled: false 107 | 108 | Style/Documentation: 109 | Enabled: false 110 | 111 | # Can use a single nil in the else clause to remove the warning 112 | Style/EmptyElse: 113 | EnforcedStyle: empty 114 | 115 | # Need String.new to have a non-freezed empty string supported down to 2.1 116 | Style/EmptyLiteral: 117 | Enabled: false 118 | 119 | Style/EmptyMethod: 120 | Enabled: false 121 | 122 | Style/FormatStringToken: 123 | Enabled: false 124 | 125 | # We target 2.2 to avoid cops that are not backward compatible, but we want this cop! 126 | Style/FrozenStringLiteralComment: 127 | EnforcedStyle: always 128 | 129 | Style/GuardClause: 130 | Enabled: false 131 | 132 | Style/IfUnlessModifier: 133 | Enabled: false 134 | 135 | Style/InverseMethods: 136 | InverseMethods: 137 | :present?: :blank? 138 | Exclude: 139 | - bin/* 140 | - gemfiles/* 141 | 142 | Style/NegatedIf: 143 | Enabled: false 144 | 145 | Style/NumericPredicate: 146 | Enabled: false 147 | 148 | Style/PercentLiteralDelimiters: 149 | Enabled: false 150 | 151 | # We tend to prefer the explicit aspect of sometimes using self. 152 | Style/RedundantSelf: 153 | Enabled: false 154 | 155 | # For a gem, i don't think it's our job to require 'English' 156 | Style/SpecialGlobalVars: 157 | Enabled: false 158 | 159 | Style/StringLiterals: 160 | EnforcedStyle: double_quotes 161 | 162 | Style/SymbolArray: 163 | EnforcedStyle: brackets 164 | 165 | # Doesn't look right to force it in this case: 166 | # where(belongs_to_reflection.foreign_type => value_class.base_class.name, 167 | # belongs_to_reflection.foreign_key => values.first.id) 168 | # But look better in some other cases. So disable 169 | Style/TrailingCommaInArguments: 170 | Enabled: false 171 | 172 | Style/TrailingCommaInArrayLiteral: 173 | EnforcedStyleForMultiline: consistent_comma 174 | 175 | Style/TrailingCommaInHashLiteral: 176 | EnforcedStyleForMultiline: consistent_comma 177 | 178 | Style/WordArray: 179 | Enabled: false 180 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.4 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | # 1.3.0 - 2025-03-04 4 | 5 | * The arguments of `#where_assoc_count` can now be swapped when comparing to a number or a range.
6 | So `where_assoc_count(:posts, :>, 5)` is now valid for having more than 5 posts. 7 | 8 | # 1.2.1 - 2024-12-05 9 | 10 | * Optimize `has_one` handling for `#where_assoc_exists` with a `has_one` as last association + without any conditions or offset. 11 | * Optimize `has_one` handling when the foreign_key has a unique index and there is no offset 12 | 13 | # 1.2.0 - 2024-08-31 14 | 15 | * Add support for composite primary keys in Rails 7.2 16 | 17 | # 1.1.5 - 2024-05-18 18 | 19 | * Add compatibility for Rails 7.2 20 | 21 | # 1.1.4 - 2023-10-10 22 | 23 | * Add compatibility for Rails 7.1 24 | 25 | # 1.1.3 - 2022-08-16 26 | 27 | * Add support for associations defined on abstract models 28 | 29 | # 1.1.2 - 2020-12-24 30 | 31 | * Add compatibility for Rails 6.1 32 | 33 | # 1.1.1 - 2020-04-13 34 | 35 | * Fix handling for ActiveRecord's NullRelation (MyModel.none) in block and association's conditions. 36 | 37 | # 1.1.0 - 2020-02-24 38 | 39 | * Added methods which return the SQL used by this gem: `assoc_exists_sql`, `assoc_not_exists_sql`, `compare_assoc_count_sql`, `only_assoc_count_sql` 40 | [Documentation for them](https://maxlap.github.io/activerecord_where_assoc/ActiveRecordWhereAssoc/SqlReturningMethods.html) 41 | 42 | # 1.0.1 43 | 44 | * Fix broken urls in error messages 45 | 46 | # 1.0.0 47 | 48 | * Now supports polymorphic belongs_to 49 | 50 | # 0.1.3 51 | 52 | * Use `SELECT 1` instead of `SELECT 0`... 53 | ... it just seems more natural that way. 54 | * Bugfixes 55 | 56 | # 0.1.2 57 | 58 | * It is now possible to pass a `Range` as first argument to `#where_assoc_count`. 59 | Ex: Users that have between 10 and 20 posts 60 | `User.where_assoc_count(10..20, :==, :posts)` 61 | The operator in that case must be either :== or :!=. 62 | This will use `BETWEEN` and `NOT BETWEEN`. 63 | Ranges that exclude the last value, i.e. `5...10`, are also supported, resulting in `BETWEEN 5 and 9`. 64 | Ranges with infinities are also supported. 65 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | Here are some example usages of the gem, along with the generated SQL. 2 | 3 | Each of those methods can be chained with scoping methods, so they can be used on `Post`, `my_user.posts`, `Post.where('hello')` or inside a scope. Note that for the `*_sql` variants, those should preferably be used on classes only, because otherwise, it could be confusing for a reader. 4 | 5 | The models can be found in [examples/models.md](examples/models.md). The comments in that file explain how to get a console to try the queries. There are also example uses of the gem for scopes. 6 | 7 | The content of this file is generated when running `rake` 8 | 9 | ------- 10 | 11 | ## Simple examples 12 | 13 | ```ruby 14 | # Posts that have a least one comment 15 | Post.where_assoc_exists(:comments) 16 | ``` 17 | ```sql 18 | SELECT "posts".* FROM "posts" 19 | WHERE (EXISTS ( 20 | SELECT 1 FROM "comments" 21 | WHERE "comments"."post_id" = "posts"."id" 22 | )) 23 | ``` 24 | 25 | --- 26 | 27 | ```ruby 28 | # Posts that have no comments 29 | Post.where_assoc_not_exists(:comments) 30 | ``` 31 | ```sql 32 | SELECT "posts".* FROM "posts" 33 | WHERE (NOT EXISTS ( 34 | SELECT 1 FROM "comments" 35 | WHERE "comments"."post_id" = "posts"."id" 36 | )) 37 | ``` 38 | 39 | --- 40 | 41 | ```ruby 42 | # Posts that have a least 50 comment 43 | Post.where_assoc_count(50, :<=, :comments) 44 | ``` 45 | ```sql 46 | SELECT "posts".* FROM "posts" 47 | WHERE ((50) <= COALESCE(( 48 | SELECT COUNT(*) FROM "comments" 49 | WHERE "comments"."post_id" = "posts"."id" 50 | ), 0)) 51 | ``` 52 | 53 | --- 54 | 55 | ```ruby 56 | # Users that have made posts 57 | User.where_assoc_exists(:posts) 58 | ``` 59 | ```sql 60 | SELECT "users".* FROM "users" 61 | WHERE (EXISTS ( 62 | SELECT 1 FROM "posts" 63 | WHERE "posts"."author_id" = "users"."id" 64 | )) 65 | ``` 66 | 67 | --- 68 | 69 | ```ruby 70 | # Users that have made posts that have comments 71 | User.where_assoc_exists([:posts, :comments]) 72 | ``` 73 | ```sql 74 | SELECT "users".* FROM "users" 75 | WHERE (EXISTS ( 76 | SELECT 1 FROM "posts" 77 | WHERE "posts"."author_id" = "users"."id" AND (EXISTS ( 78 | SELECT 1 FROM "comments" 79 | WHERE "comments"."post_id" = "posts"."id" 80 | )) 81 | )) 82 | ``` 83 | 84 | --- 85 | 86 | ```ruby 87 | # Users with a post or a comment (without using ActiveRecord's `or` method) 88 | # Using `my_users` to highlight that *_sql methods should always be called on the class 89 | my_users.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}") 90 | ``` 91 | ```sql 92 | SELECT "users".* FROM "users" 93 | WHERE (EXISTS ( 94 | SELECT 1 FROM "posts" 95 | WHERE "posts"."author_id" = "users"."id" 96 | ) OR EXISTS ( 97 | SELECT 1 FROM "comments" 98 | WHERE "comments"."author_id" = "users"."id" 99 | )) 100 | ``` 101 | 102 | --- 103 | 104 | ```ruby 105 | # Users with a post or a comment (using ActiveRecord's `or` method) 106 | User.where_assoc_exists(:posts).or(User.where_assoc_exists(:comments)) 107 | ``` 108 | ```sql 109 | SELECT "users".* FROM "users" 110 | WHERE (EXISTS ( 111 | SELECT 1 FROM "posts" 112 | WHERE "posts"."author_id" = "users"."id" 113 | ) OR EXISTS ( 114 | SELECT 1 FROM "comments" 115 | WHERE "comments"."author_id" = "users"."id" 116 | )) 117 | ``` 118 | 119 | --- 120 | 121 | ## Examples with condition / scope 122 | 123 | ```ruby 124 | # comments of `my_post` that were made by an admin (Using a hash) 125 | my_post.comments.where_assoc_exists(:author, is_admin: true) 126 | ``` 127 | ```sql 128 | SELECT "comments".* FROM "comments" 129 | WHERE "comments"."post_id" = 1 AND (EXISTS ( 130 | SELECT 1 FROM "users" 131 | WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1 132 | )) 133 | ``` 134 | 135 | --- 136 | 137 | ```ruby 138 | # comments of `my_post` that were not made by an admin (Using scope) 139 | my_post.comments.where_assoc_not_exists(:author, &:admins) 140 | ``` 141 | ```sql 142 | SELECT "comments".* FROM "comments" 143 | WHERE "comments"."post_id" = 1 AND (NOT EXISTS ( 144 | SELECT 1 FROM "users" 145 | WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1 146 | )) 147 | ``` 148 | 149 | --- 150 | 151 | ```ruby 152 | # Posts that have at least 5 reported comments (Using array condition) 153 | Post.where_assoc_count(5, :<=, :comments, ["is_reported = ?", true]) 154 | ``` 155 | ```sql 156 | SELECT "posts".* FROM "posts" 157 | WHERE ((5) <= COALESCE(( 158 | SELECT COUNT(*) FROM "comments" 159 | WHERE "comments"."post_id" = "posts"."id" AND (is_reported = 1) 160 | ), 0)) 161 | ``` 162 | 163 | --- 164 | 165 | ```ruby 166 | # Posts made by an admin (Using a string) 167 | Post.where_assoc_exists(:author, "is_admin = 't'") 168 | ``` 169 | ```sql 170 | SELECT "posts".* FROM "posts" 171 | WHERE (EXISTS ( 172 | SELECT 1 FROM "users" 173 | WHERE "users"."id" = "posts"."author_id" AND (is_admin = 't') 174 | )) 175 | ``` 176 | 177 | --- 178 | 179 | ```ruby 180 | # comments of `my_post` that were not made by an admin (Using block and a scope) 181 | my_post.comments.where_assoc_not_exists(:author) { admins } 182 | ``` 183 | ```sql 184 | SELECT "comments".* FROM "comments" 185 | WHERE "comments"."post_id" = 1 AND (NOT EXISTS ( 186 | SELECT 1 FROM "users" 187 | WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1 188 | )) 189 | ``` 190 | 191 | --- 192 | 193 | ```ruby 194 | # Posts that have 5 to 10 reported comments (Using block with #where and range for count) 195 | Post.where_assoc_count(5..10, :==, :comments) { where(is_reported: true) } 196 | ``` 197 | ```sql 198 | SELECT "posts".* FROM "posts" 199 | WHERE (COALESCE(( 200 | SELECT COUNT(*) FROM "comments" 201 | WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 1 202 | ), 0) BETWEEN 5 AND 10) 203 | ``` 204 | 205 | --- 206 | 207 | ```ruby 208 | # comments made in replies to my_user's post 209 | Comment.where_assoc_exists(:post, author_id: my_user.id) 210 | ``` 211 | ```sql 212 | SELECT "comments".* FROM "comments" 213 | WHERE (EXISTS ( 214 | SELECT 1 FROM "posts" 215 | WHERE "posts"."id" = "comments"."post_id" AND "posts"."author_id" = 1 216 | )) 217 | ``` 218 | 219 | --- 220 | 221 | ## Complex / powerful examples 222 | 223 | ```ruby 224 | # posts with a comment by an admin (uses array to go through multiple associations) 225 | Post.where_assoc_exists([:comments, :author], is_admin: true) 226 | ``` 227 | ```sql 228 | SELECT "posts".* FROM "posts" 229 | WHERE (EXISTS ( 230 | SELECT 1 FROM "comments" 231 | WHERE "comments"."post_id" = "posts"."id" AND (EXISTS ( 232 | SELECT 1 FROM "users" 233 | WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1 234 | )) 235 | )) 236 | ``` 237 | 238 | --- 239 | 240 | ```ruby 241 | # posts where the author also commented on the post (uses a conditions between tables) 242 | Post.where_assoc_exists(:comments, "posts.author_id = comments.author_id") 243 | ``` 244 | ```sql 245 | SELECT "posts".* FROM "posts" 246 | WHERE (EXISTS ( 247 | SELECT 1 FROM "comments" 248 | WHERE "comments"."post_id" = "posts"."id" AND (posts.author_id = comments.author_id) 249 | )) 250 | ``` 251 | 252 | --- 253 | 254 | ```ruby 255 | # posts with a reported comment made by an admin (must be the same comments) 256 | Post.where_assoc_exists(:comments, is_reported: true) { 257 | where_assoc_exists(:author, is_admin: true) 258 | } 259 | ``` 260 | ```sql 261 | SELECT "posts".* FROM "posts" 262 | WHERE (EXISTS ( 263 | SELECT 1 FROM "comments" 264 | WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 1 AND (EXISTS ( 265 | SELECT 1 FROM "users" 266 | WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1 267 | )) 268 | )) 269 | ``` 270 | 271 | --- 272 | 273 | ```ruby 274 | # posts with a reported comment and a comment by an admin (can be different or same comments) 275 | my_user.posts.where_assoc_exists(:comments, is_reported: true) 276 | .where_assoc_exists([:comments, :author], is_admin: true) 277 | ``` 278 | ```sql 279 | SELECT "posts".* FROM "posts" 280 | WHERE "posts"."author_id" = 1 AND (EXISTS ( 281 | SELECT 1 FROM "comments" 282 | WHERE "comments"."post_id" = "posts"."id" AND "comments"."is_reported" = 1 283 | )) AND (EXISTS ( 284 | SELECT 1 FROM "comments" 285 | WHERE "comments"."post_id" = "posts"."id" AND (EXISTS ( 286 | SELECT 1 FROM "users" 287 | WHERE "users"."id" = "comments"."author_id" AND "users"."is_admin" = 1 288 | )) 289 | )) 290 | ``` 291 | ```ruby 292 | # Users with more posts than comments 293 | # Using `my_users` to highlight that *_sql methods should always be called on the class 294 | my_users.where("#{User.only_assoc_count_sql(:posts)} > #{User.only_assoc_count_sql(:comments)}") 295 | ``` 296 | ```sql 297 | SELECT "users".* FROM "users" 298 | WHERE (COALESCE(( 299 | SELECT COUNT(*) FROM "posts" 300 | WHERE "posts"."author_id" = "users"."id" 301 | ), 0) > COALESCE(( 302 | SELECT COUNT(*) FROM "comments" 303 | WHERE "comments"."author_id" = "users"."id" 304 | ), 0)) 305 | ``` 306 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem 'prime' 8 | gem 'rails_sql_prettifier' 9 | 10 | # Specify your gem's dependencies in active_record_where_assoc.gemspec 11 | gemspec 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Maxime Handfield Lapointe 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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 | # Not using Rake::RDocTask because it won't update things if only the stylesheet changed 13 | desc "Generate documentation for the gem" 14 | task :run_rdoc do 15 | args = ["rdoc"] 16 | args << "--template-stylesheets=docs_customization.css" 17 | args << "--title=activerecord_where_assoc" 18 | args << "--output=docs" 19 | args << "--show-hash" 20 | args << "lib/active_record_where_assoc/relation_returning_methods.rb" 21 | args << "lib/active_record_where_assoc/sql_returning_methods.rb" 22 | 23 | Bundler.with_clean_env do 24 | exit(1) unless system(*args) 25 | end 26 | 27 | rdoc_css_path = File.join(__dir__, "docs/css/rdoc.css") 28 | rdoc_css = File.read(rdoc_css_path) 29 | # A little bug in rdoc's generated stuff... the urls in the CSS are wrong! 30 | rdoc_css.gsub!("url(images", "url(../images") 31 | File.write(rdoc_css_path, rdoc_css) 32 | 33 | relation_returning_methods_path = File.join(__dir__, "docs/ActiveRecordWhereAssoc/RelationReturningMethods.html") 34 | relation_returning_methods = File.read(relation_returning_methods_path) 35 | # A little bug in rdoc's generated stuff. The links to headings are broken! 36 | relation_returning_methods.gsub!(/#(label[^"]+)/, "#module-ActiveRecordWhereAssoc::RelationReturningMethods-\\1") 37 | File.write(relation_returning_methods_path, relation_returning_methods) 38 | end 39 | 40 | task :generate_examples do 41 | puts "Begin generating EXAMPLES.md" 42 | content = `ruby examples/examples.rb` 43 | if $?.success? 44 | File.write("EXAMPLES.md", content) 45 | puts "Finished generating EXAMPLES.md" 46 | else 47 | puts "Couldn't generate EXAMPLES.md" 48 | exit(1) 49 | end 50 | end 51 | 52 | task :generate_run_tests_on_head_workflow do 53 | require 'yaml' 54 | config = YAML.load_file('.github/workflows/run_tests.yml') 55 | config['name'] = 'Test future versions' 56 | config['env']['CACHE_DEPENDENCIES'] = false 57 | 58 | max_gemfile = config['jobs']['test']['strategy']['matrix']['include'].map { |c| c['gemfile'] }.max 59 | max_ruby_version = config['jobs']['test']['strategy']['matrix']['include'].map { |c| c['ruby_version'].to_s }.max 60 | 61 | config['jobs']['test']['strategy']['matrix']['include'] = [ 62 | {'gemfile' => 'gemfiles/rails_head.gemfile', 'ruby_version' => 'head'}, 63 | {'gemfile' => 'gemfiles/rails_head.gemfile', 'ruby_version' => max_ruby_version}, 64 | {'gemfile' => max_gemfile, 'ruby_version' => 'head'}, 65 | ] 66 | 67 | # 68 | # config['jobs']['test']['continue-on-error'] = true 69 | 70 | header = <<-TXT 71 | # This file is generated from run_tests.yml, changes here will be lost next time `rake` is run 72 | TXT 73 | 74 | File.write('.github/workflows/run_tests_on_head.yml', header + config.to_yaml) 75 | end 76 | 77 | task default: [:generate_run_tests_on_head_workflow, :generate_examples, :run_rdoc, :test] 78 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /activerecord_where_assoc.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/active_record_where_assoc/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "activerecord_where_assoc" 7 | spec.version = ActiveRecordWhereAssoc::VERSION 8 | spec.authors = ["Maxime Handfield Lapointe"] 9 | spec.email = ["maxhlap@gmail.com"] 10 | 11 | spec.summary = "Make ActiveRecord do conditions on your associations" 12 | spec.description = "Adds various #where_assoc_* methods to ActiveRecord to make it easy to do correct" \ 13 | " conditions on the associations of the model being queried." 14 | spec.homepage = "https://github.com/MaxLap/activerecord_where_assoc" 15 | spec.license = "MIT" 16 | 17 | lib_files = `git ls-files -z lib`.split("\x0") 18 | spec.files = [*lib_files, "CHANGELOG.md", "EXAMPLES.md", "LICENSE.txt", "README.md"] 19 | 20 | spec.add_dependency "activerecord", ">= 4.1.0" 21 | 22 | spec.add_development_dependency "bundler", ">= 1.15" 23 | spec.add_development_dependency "minitest", "~> 5.0" 24 | spec.add_development_dependency "pry" 25 | spec.add_development_dependency "rake", ">= 10.0" 26 | 27 | spec.add_development_dependency "deep-cover" 28 | spec.add_development_dependency "rubocop", "0.54.0" 29 | spec.add_development_dependency "simplecov" 30 | 31 | # Useful for the examples 32 | spec.add_development_dependency "niceql", ">= 0.1.23" 33 | 34 | # Normally, testing with sqlite3 is good enough 35 | spec.add_development_dependency "sqlite3" 36 | 37 | # Travis-CI takes care of the other ones 38 | # Using conditions because someone might not even be able to install the gems 39 | spec.add_development_dependency "pg" if ENV["CI"] || ENV["ALL_DB"] || ["pg", "postgres", "postgresql"].include?(ENV["DB"]) 40 | end 41 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require_relative "../test/support/load_test_env" 6 | require_relative "../examples/schema" 7 | require_relative "../examples/models" 8 | require_relative "../examples/some_data" 9 | 10 | ActiveRecord::Base.logger = Logger.new(STDOUT) 11 | require "irb" 12 | IRB.start(__FILE__) 13 | -------------------------------------------------------------------------------- /bin/fixcop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | COPS_TO_AUTO_CORRECT = [ 5 | # Magic comment frozen_string_literal: true 6 | "Style/FrozenStringLiteralComment", 7 | "Layout/EmptyLineAfterMagicComment", 8 | 9 | # String literals 10 | "Style/StringLiterals", 11 | "Style/StringLiteralsInInterpolation", 12 | 13 | # Indentation 14 | "Layout/IndentationWidth", 15 | "Layout/CommentIndentation", 16 | "Layout/IndentationConsistency", 17 | 18 | # Useless whitespace / newlines 19 | "Layout/EmptyLinesAroundBeginBody", 20 | "Layout/EmptyLinesAroundBlockBody", 21 | "Layout/EmptyLinesAroundClassBody", 22 | "Layout/EmptyLinesAroundMethodBody", 23 | "Layout/EmptyLinesAroundModuleBody", 24 | "Layout/TrailingWhitespace", 25 | "Layout/TrailingBlankLines", 26 | "Layout/ExtraSpacing", 27 | 28 | # Array stuff 29 | "Layout/MultilineArrayBraceLayout", 30 | "Layout/IndentArray", 31 | "Layout/AlignArray", 32 | 33 | # Hash stuff 34 | "Layout/MultilineHashBraceLayout", 35 | "Layout/SpaceInsideHashLiteralBraces", 36 | "Layout/IndentHash", 37 | "Layout/AlignHash", 38 | "Style/HashSyntax", 39 | "Layout/SpaceAfterColon", 40 | 41 | # Hash & Array 42 | "Layout/SpaceAfterComma", 43 | "Style/TrailingCommaInArrayLiteral", 44 | "Style/TrailingCommaInHashLiteral", 45 | 46 | # Block stuff 47 | "Layout/SpaceBeforeBlockBraces", 48 | "Layout/SpaceInsideBlockBraces", 49 | 50 | # Parens stuff 51 | "Layout/SpaceInsideParens", 52 | "Style/NestedParenthesizedCalls", 53 | 54 | # Method stuff 55 | "Style/BracesAroundHashParameters", 56 | "Style/MethodDefParentheses", 57 | "Layout/SpaceAroundEqualsInParameterDefault", 58 | "Layout/FirstParameterIndentation", 59 | 60 | # Lambda stuff 61 | "Layout/SpaceInLambdaLiteral", 62 | "Style/Lambda", 63 | 64 | # Misc 65 | "Layout/SpaceAroundOperators", 66 | "Layout/CaseIndentation", 67 | "Layout/ElseAlignment", 68 | "Layout/LeadingCommentSpace", 69 | "Layout/SpaceBeforeComment", 70 | 71 | # Code transformation for cleanup 72 | "Rails/Present", 73 | "Rails/Blank", 74 | "Style/EmptyCaseCondition", 75 | "Style/InverseMethods", 76 | "Style/RedundantReturn", 77 | ].freeze 78 | 79 | system("rubocop", "--only=#{COPS_TO_AUTO_CORRECT.join(',')}", "--auto-correct") 80 | 81 | # Then just run rubocop normally to print remaining problems 82 | system("rubocop") 83 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /bin/testall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # Will run rake test on every ruby version you have installed that matches the .travis-ci.yml 6 | # If you want to test on something else than SQLite3, specify the DB=pg or DB=mysql before calling it. 7 | # 8 | # This is a script that does basically what wwtd is meant to do (run tests following 9 | # the config in travis-ci), but: 10 | # * ignores the scripts and the envs which are meant for travis 11 | # * only runs rake test with the specified database 12 | # * ignores rails_head and ruby-head 13 | # * Way simpler 14 | # 15 | # Other differences from wwtd: 16 | # * automatically installs the bundler gem if it is missing from a ruby version. 17 | # 18 | require "English" 19 | require "term/ansicolor" 20 | require "yaml" 21 | TRAVIS_CONFIG = ".travis.yml".freeze 22 | 23 | def run_command(env_vars, command) 24 | puts "RUNNING: #{command} WITH: #{env_vars}" 25 | system(env_vars, command) 26 | end 27 | 28 | travis_yml = (File.exist?(TRAVIS_CONFIG) ? YAML.load_file(TRAVIS_CONFIG) : {}) 29 | ruby_versions = travis_yml["rvm"] || [] 30 | gemfiles = travis_yml["gemfile"] || [nil] 31 | matrix_options = travis_yml["matrix"] || {} 32 | matrix_include = matrix_options["include"] || [] 33 | matrix_exclude = matrix_options["exclude"] || [] 34 | 35 | configs = ruby_versions.product(gemfiles) 36 | matrix_include.each do |conf| 37 | configs << [conf["rvm"], conf["gemfile"] || nil] 38 | end 39 | matrix_exclude.each do |conf| 40 | configs.delete [conf["rvm"], conf["gemfile"] || nil] 41 | end 42 | 43 | rubies = {} 44 | results = [] 45 | 46 | `which rvm` 47 | has_rvm = $CHILD_STATUS.success? 48 | `which chruby-exec` 49 | has_chruby = $CHILD_STATUS.success? 50 | 51 | if has_rvm 52 | ruby_exec = "rvm-exec %{ruby_version} " 53 | elsif has_chruby 54 | ruby_exec = "chruby-exec %{version} -- " 55 | else 56 | abort("Couldn't find either rvm or chruby") 57 | end 58 | 59 | configs.each do |ruby_version, gemfile| 60 | next if ruby_version == "ruby-head" 61 | 62 | current_ruby_exec = format(ruby_exec, ruby_version: ruby_version) 63 | 64 | env_vars = { "BUNDLE_GEMFILE" => gemfile, "WITHOUT_PENDING" => ENV["WITHOUT_PENDING"] || "1" } 65 | gemfile_text = gemfile || "Default gemfile" 66 | success = true 67 | 68 | if !rubies.include?(ruby_version) 69 | `#{current_ruby_exec} ruby -v` 70 | has_ruby_version = $CHILD_STATUS.success? 71 | if has_ruby_version 72 | if !system("#{current_ruby_exec} gem list -i '^bundler$' 1>/dev/null") 73 | success &&= run_command(env_vars, "#{current_ruby_exec} gem install bundler") 74 | end 75 | rubies[ruby_version] = true 76 | else 77 | rubies[ruby_version] = false 78 | end 79 | end 80 | 81 | if rubies[ruby_version] == false 82 | results << Term::ANSIColor.yellow("MISING RUBY: #{ruby_version} for #{gemfile_text}") 83 | next 84 | end 85 | 86 | if success 87 | bundle_installed = run_command(env_vars, "#{current_ruby_exec} bundle check 1>/dev/null 2>&1") 88 | bundle_installed ||= run_command(env_vars, "#{current_ruby_exec} bundle install --quiet 1>/dev/null 2>&1") 89 | bundle_installed ||= run_command(env_vars, "#{current_ruby_exec} bundle update --quiet") 90 | success &&= bundle_installed 91 | end 92 | success &&= run_command(env_vars, "#{current_ruby_exec} bundle exec rake test") if success 93 | 94 | if success 95 | results << Term::ANSIColor.green("SUCCESS: #{ruby_version} for #{gemfile_text}") 96 | else 97 | results << Term::ANSIColor.red("FAILURE: #{ruby_version} for #{gemfile_text}") 98 | end 99 | end 100 | 101 | puts results 102 | -------------------------------------------------------------------------------- /bin/testone: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | full_path = ARGV.first 5 | abort "You need to pass a file_path" unless full_path 6 | path_only = full_path.sub(/:\d+$/, "") 7 | abort "File #{path_only} doesn't exist" unless File.exist?(path_only) 8 | 9 | # ruby -I test test/unit/my_model_test.rb -n test_invalid_with_bad_attributes 10 | args = ["ruby", "-I", "test", path_only] 11 | if ARGV[1] 12 | args << "-n" 13 | args << "/#{ARGV[1]}/" 14 | end 15 | system(*args) 16 | # system({"TEST" => path_only}, "rake", "test") 17 | 18 | # /home/max/semi/gems/activerecord_where_assoc/test/tests/scoping/wa_with_no_possible_records_to_return_test.rb:24 19 | -------------------------------------------------------------------------------- /docs/ActiveRecordWhereAssoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | module ActiveRecordWhereAssoc - activerecord_where_assoc 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 84 | 85 |
86 |

87 | module ActiveRecordWhereAssoc 88 |

89 | 90 |
91 | 92 |

See RelationReturningMethods

93 | 94 |
95 | 96 | 103 |
104 | 105 | -------------------------------------------------------------------------------- /docs/css/fonts.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), 3 | * with Reserved Font Name "Source". All Rights Reserved. Source is a 4 | * trademark of Adobe Systems Incorporated in the United States and/or other 5 | * countries. 6 | * 7 | * This Font Software is licensed under the SIL Open Font License, Version 8 | * 1.1. 9 | * 10 | * This license is copied below, and is also available with a FAQ at: 11 | * http://scripts.sil.org/OFL 12 | */ 13 | 14 | @font-face { 15 | font-family: "Source Code Pro"; 16 | font-style: normal; 17 | font-weight: 400; 18 | src: local("Source Code Pro"), 19 | local("SourceCodePro-Regular"), 20 | url("../fonts/SourceCodePro-Regular.ttf") format("truetype"); 21 | } 22 | 23 | @font-face { 24 | font-family: "Source Code Pro"; 25 | font-style: normal; 26 | font-weight: 700; 27 | src: local("Source Code Pro Bold"), 28 | local("SourceCodePro-Bold"), 29 | url("../fonts/SourceCodePro-Bold.ttf") format("truetype"); 30 | } 31 | 32 | /* 33 | * Copyright (c) 2010, Łukasz Dziedzic (dziedzic@typoland.com), 34 | * with Reserved Font Name Lato. 35 | * 36 | * This Font Software is licensed under the SIL Open Font License, Version 37 | * 1.1. 38 | * 39 | * This license is copied below, and is also available with a FAQ at: 40 | * http://scripts.sil.org/OFL 41 | */ 42 | 43 | @font-face { 44 | font-family: "Lato"; 45 | font-style: normal; 46 | font-weight: 300; 47 | src: local("Lato Light"), 48 | local("Lato-Light"), 49 | url("../fonts/Lato-Light.ttf") format("truetype"); 50 | } 51 | 52 | @font-face { 53 | font-family: "Lato"; 54 | font-style: italic; 55 | font-weight: 300; 56 | src: local("Lato Light Italic"), 57 | local("Lato-LightItalic"), 58 | url("../fonts/Lato-LightItalic.ttf") format("truetype"); 59 | } 60 | 61 | @font-face { 62 | font-family: "Lato"; 63 | font-style: normal; 64 | font-weight: 700; 65 | src: local("Lato Regular"), 66 | local("Lato-Regular"), 67 | url("../fonts/Lato-Regular.ttf") format("truetype"); 68 | } 69 | 70 | @font-face { 71 | font-family: "Lato"; 72 | font-style: italic; 73 | font-weight: 700; 74 | src: local("Lato Italic"), 75 | local("Lato-Italic"), 76 | url("../fonts/Lato-RegularItalic.ttf") format("truetype"); 77 | } 78 | 79 | /* 80 | * ----------------------------------------------------------- 81 | * SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 82 | * ----------------------------------------------------------- 83 | * 84 | * PREAMBLE 85 | * The goals of the Open Font License (OFL) are to stimulate worldwide 86 | * development of collaborative font projects, to support the font creation 87 | * efforts of academic and linguistic communities, and to provide a free and 88 | * open framework in which fonts may be shared and improved in partnership 89 | * with others. 90 | * 91 | * The OFL allows the licensed fonts to be used, studied, modified and 92 | * redistributed freely as long as they are not sold by themselves. The 93 | * fonts, including any derivative works, can be bundled, embedded, 94 | * redistributed and/or sold with any software provided that any reserved 95 | * names are not used by derivative works. The fonts and derivatives, 96 | * however, cannot be released under any other type of license. The 97 | * requirement for fonts to remain under this license does not apply 98 | * to any document created using the fonts or their derivatives. 99 | * 100 | * DEFINITIONS 101 | * "Font Software" refers to the set of files released by the Copyright 102 | * Holder(s) under this license and clearly marked as such. This may 103 | * include source files, build scripts and documentation. 104 | * 105 | * "Reserved Font Name" refers to any names specified as such after the 106 | * copyright statement(s). 107 | * 108 | * "Original Version" refers to the collection of Font Software components as 109 | * distributed by the Copyright Holder(s). 110 | * 111 | * "Modified Version" refers to any derivative made by adding to, deleting, 112 | * or substituting -- in part or in whole -- any of the components of the 113 | * Original Version, by changing formats or by porting the Font Software to a 114 | * new environment. 115 | * 116 | * "Author" refers to any designer, engineer, programmer, technical 117 | * writer or other person who contributed to the Font Software. 118 | * 119 | * PERMISSION & CONDITIONS 120 | * Permission is hereby granted, free of charge, to any person obtaining 121 | * a copy of the Font Software, to use, study, copy, merge, embed, modify, 122 | * redistribute, and sell modified and unmodified copies of the Font 123 | * Software, subject to the following conditions: 124 | * 125 | * 1) Neither the Font Software nor any of its individual components, 126 | * in Original or Modified Versions, may be sold by itself. 127 | * 128 | * 2) Original or Modified Versions of the Font Software may be bundled, 129 | * redistributed and/or sold with any software, provided that each copy 130 | * contains the above copyright notice and this license. These can be 131 | * included either as stand-alone text files, human-readable headers or 132 | * in the appropriate machine-readable metadata fields within text or 133 | * binary files as long as those fields can be easily viewed by the user. 134 | * 135 | * 3) No Modified Version of the Font Software may use the Reserved Font 136 | * Name(s) unless explicit written permission is granted by the corresponding 137 | * Copyright Holder. This restriction only applies to the primary font name as 138 | * presented to the users. 139 | * 140 | * 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 141 | * Software shall not be used to promote, endorse or advertise any 142 | * Modified Version, except to acknowledge the contribution(s) of the 143 | * Copyright Holder(s) and the Author(s) or with their explicit written 144 | * permission. 145 | * 146 | * 5) The Font Software, modified or unmodified, in part or in whole, 147 | * must be distributed entirely under this license, and must not be 148 | * distributed under any other license. The requirement for fonts to 149 | * remain under this license does not apply to any document created 150 | * using the Font Software. 151 | * 152 | * TERMINATION 153 | * This license becomes null and void if any of the above conditions are 154 | * not met. 155 | * 156 | * DISCLAIMER 157 | * THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 158 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 159 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 160 | * OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 161 | * COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 162 | * INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 163 | * DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 164 | * FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 165 | * OTHER DEALINGS IN THE FONT SOFTWARE. 166 | */ 167 | 168 | -------------------------------------------------------------------------------- /docs/docs_customization.css: -------------------------------------------------------------------------------- 1 | 2 | /* This css is used to customize the documentation */ 3 | 4 | main { 5 | /* Seems like a good size */ 6 | max-width: 1024px; 7 | } 8 | 9 | main .method-args { 10 | font-weight: normal; 11 | font-size: 90%; 12 | } 13 | 14 | main .method-detail { 15 | cursor: initial; 16 | } 17 | 18 | main .method-heading { 19 | cursor: pointer; 20 | } 21 | 22 | main ul, main ol { 23 | margin-top: 0; 24 | margin-bottom: 0.5em; 25 | } 26 | 27 | main p { 28 | margin: 0; 29 | } 30 | 31 | main p+p { 32 | margin-top: 0.5em; 33 | } 34 | 35 | main li > p { 36 | margin: 0; 37 | } 38 | 39 | main li > p+p { 40 | margin-top: 0.5em; 41 | } 42 | 43 | main dl, main dt { 44 | margin: 0; 45 | } 46 | 47 | main dd { 48 | margin-left: 1.5em; 49 | margin-bottom: 0.5em; 50 | } 51 | 52 | body { 53 | font-family: Georgia, "Times New Roman", serif; 54 | } 55 | 56 | code { 57 | /* Similar formatting to Github's */ 58 | background-color: rgba(27,31,35,.03); 59 | border-radius: 3px; 60 | font-size: 85%; 61 | margin: 0; 62 | padding: .2em .4em; 63 | } 64 | 65 | .ruby-comment { 66 | color: #ff0000 67 | } 68 | 69 | .method-section > header:first-child { 70 | display: none; 71 | } 72 | -------------------------------------------------------------------------------- /docs/fonts/Lato-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/fonts/Lato-Light.ttf -------------------------------------------------------------------------------- /docs/fonts/Lato-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/fonts/Lato-LightItalic.ttf -------------------------------------------------------------------------------- /docs/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/Lato-RegularItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/fonts/Lato-RegularItalic.ttf -------------------------------------------------------------------------------- /docs/fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /docs/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /docs/images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/add.png -------------------------------------------------------------------------------- /docs/images/arrow_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/arrow_up.png -------------------------------------------------------------------------------- /docs/images/brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/brick.png -------------------------------------------------------------------------------- /docs/images/brick_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/brick_link.png -------------------------------------------------------------------------------- /docs/images/bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/bug.png -------------------------------------------------------------------------------- /docs/images/bullet_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/bullet_black.png -------------------------------------------------------------------------------- /docs/images/bullet_toggle_minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/bullet_toggle_minus.png -------------------------------------------------------------------------------- /docs/images/bullet_toggle_plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/bullet_toggle_plus.png -------------------------------------------------------------------------------- /docs/images/date.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/date.png -------------------------------------------------------------------------------- /docs/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/delete.png -------------------------------------------------------------------------------- /docs/images/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/find.png -------------------------------------------------------------------------------- /docs/images/loadingAnimation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/loadingAnimation.gif -------------------------------------------------------------------------------- /docs/images/macFFBgHack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/macFFBgHack.png -------------------------------------------------------------------------------- /docs/images/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/package.png -------------------------------------------------------------------------------- /docs/images/page_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/page_green.png -------------------------------------------------------------------------------- /docs/images/page_white_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/page_white_text.png -------------------------------------------------------------------------------- /docs/images/page_white_width.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/page_white_width.png -------------------------------------------------------------------------------- /docs/images/plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/plugin.png -------------------------------------------------------------------------------- /docs/images/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/ruby.png -------------------------------------------------------------------------------- /docs/images/tag_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/tag_blue.png -------------------------------------------------------------------------------- /docs/images/tag_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/tag_green.png -------------------------------------------------------------------------------- /docs/images/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/transparent.png -------------------------------------------------------------------------------- /docs/images/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/wrench.png -------------------------------------------------------------------------------- /docs/images/wrench_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/wrench_orange.png -------------------------------------------------------------------------------- /docs/images/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaxLap/activerecord_where_assoc/578e6de4ed698722f28b4411434a0f9f6ddf2649/docs/images/zoom.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | activerecord_where_assoc 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 89 | 90 |
91 |

This is the API documentation for activerecord_where_assoc. 92 |

93 | 94 | -------------------------------------------------------------------------------- /docs/js/darkfish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Darkfish Page Functions 4 | * $Id: darkfish.js 53 2009-01-07 02:52:03Z deveiant $ 5 | * 6 | * Author: Michael Granger 7 | * 8 | */ 9 | 10 | /* Provide console simulation for firebug-less environments */ 11 | /* 12 | if (!("console" in window) || !("firebug" in console)) { 13 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", 14 | "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; 15 | 16 | window.console = {}; 17 | for (var i = 0; i < names.length; ++i) 18 | window.console[names[i]] = function() {}; 19 | }; 20 | */ 21 | 22 | 23 | function showSource( e ) { 24 | var target = e.target; 25 | while (!target.classList.contains('method-detail')) { 26 | target = target.parentNode; 27 | } 28 | if (typeof target !== "undefined" && target !== null) { 29 | target = target.querySelector('.method-source-code'); 30 | } 31 | if (typeof target !== "undefined" && target !== null) { 32 | target.classList.toggle('active-menu') 33 | } 34 | }; 35 | 36 | function hookSourceViews() { 37 | document.querySelectorAll('.method-source-toggle').forEach(function (codeObject) { 38 | codeObject.addEventListener('click', showSource); 39 | }); 40 | }; 41 | 42 | function hookSearch() { 43 | var input = document.querySelector('#search-field'); 44 | var result = document.querySelector('#search-results'); 45 | result.classList.remove("initially-hidden"); 46 | 47 | var search_section = document.querySelector('#search-section'); 48 | search_section.classList.remove("initially-hidden"); 49 | 50 | var search = new Search(search_data, input, result); 51 | 52 | search.renderItem = function(result) { 53 | var li = document.createElement('li'); 54 | var html = ''; 55 | 56 | // TODO add relative path to 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 75 |
76 |

Table of Contents - activerecord_where_assoc

77 | 78 | 79 |

Classes and Modules

80 | 103 | 104 |

Methods

105 | 142 |
143 | 144 | -------------------------------------------------------------------------------- /docs_customization.css: -------------------------------------------------------------------------------- 1 | 2 | /* This css is used to customize the documentation */ 3 | 4 | main { 5 | /* Seems like a good size */ 6 | max-width: 1024px; 7 | } 8 | 9 | main .method-args { 10 | font-weight: normal; 11 | font-size: 90%; 12 | } 13 | 14 | main .method-detail { 15 | cursor: initial; 16 | } 17 | 18 | main .method-heading { 19 | cursor: pointer; 20 | } 21 | 22 | main ul, main ol { 23 | margin-top: 0; 24 | margin-bottom: 0.5em; 25 | } 26 | 27 | main p { 28 | margin: 0; 29 | } 30 | 31 | main p+p { 32 | margin-top: 0.5em; 33 | } 34 | 35 | main li > p { 36 | margin: 0; 37 | } 38 | 39 | main li > p+p { 40 | margin-top: 0.5em; 41 | } 42 | 43 | main dl, main dt { 44 | margin: 0; 45 | } 46 | 47 | main dd { 48 | margin-left: 1.5em; 49 | margin-bottom: 0.5em; 50 | } 51 | 52 | body { 53 | font-family: Georgia, "Times New Roman", serif; 54 | } 55 | 56 | code { 57 | /* Similar formatting to Github's */ 58 | background-color: rgba(27,31,35,.03); 59 | border-radius: 3px; 60 | font-size: 85%; 61 | margin: 0; 62 | padding: .2em .4em; 63 | } 64 | 65 | .ruby-comment { 66 | color: #ff0000 67 | } 68 | 69 | .method-section > header:first-child { 70 | display: none; 71 | } 72 | -------------------------------------------------------------------------------- /examples/examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # To update the examples, run this from the project's root dir: 4 | # `ruby examples/examples.rb > EXAMPLES.md` 5 | 6 | # Avoid a message about default database used 7 | ENV["DB"] ||= "sqlite3" 8 | require "active_support/core_ext/string/strip" 9 | require_relative "../test/support/load_test_env" 10 | require_relative "schema" 11 | require_relative "models" 12 | require_relative "some_data" 13 | require "rails_sql_prettifier" 14 | 15 | class Examples 16 | def puts_doc 17 | puts <<-HEADER.strip_heredoc 18 | Here are some example usages of the gem, along with the generated SQL. 19 | 20 | Each of those methods can be chained with scoping methods, so they can be used on `Post`, `my_user.posts`, `Post.where('hello')` or inside a scope. Note that for the `*_sql` variants, those should preferably be used on classes only, because otherwise, it could be confusing for a reader. 21 | 22 | The models can be found in [examples/models.md](examples/models.md). The comments in that file explain how to get a console to try the queries. There are also example uses of the gem for scopes. 23 | 24 | The content of this file is generated when running `rake` 25 | 26 | ------- 27 | 28 | HEADER 29 | 30 | puts "## Simple examples" 31 | puts 32 | 33 | output_example(<<-DESC, <<-RUBY) 34 | Posts that have a least one comment 35 | DESC 36 | Post.where_assoc_exists(:comments) 37 | RUBY 38 | 39 | output_example(<<-DESC, <<-RUBY) 40 | Posts that have no comments 41 | DESC 42 | Post.where_assoc_not_exists(:comments) 43 | RUBY 44 | 45 | output_example(<<-DESC, <<-RUBY) 46 | Posts that have a least 50 comment 47 | DESC 48 | Post.where_assoc_count(50, :<=, :comments) 49 | RUBY 50 | 51 | output_example(<<-DESC, <<-RUBY) 52 | Users that have made posts 53 | DESC 54 | User.where_assoc_exists(:posts) 55 | RUBY 56 | 57 | output_example(<<-DESC, <<-RUBY) 58 | Users that have made posts that have comments 59 | DESC 60 | User.where_assoc_exists([:posts, :comments]) 61 | RUBY 62 | 63 | output_example(<<-DESC, <<-RUBY) 64 | Users with a post or a comment (without using ActiveRecord's `or` method) 65 | Using `my_users` to highlight that *_sql methods should always be called on the class 66 | DESC 67 | my_users.where("\#{User.assoc_exists_sql(:posts)} OR \#{User.assoc_exists_sql(:comments)}") 68 | RUBY 69 | 70 | output_example(<<-DESC, <<-RUBY) 71 | Users with a post or a comment (using ActiveRecord's `or` method) 72 | DESC 73 | User.where_assoc_exists(:posts).or(User.where_assoc_exists(:comments)) 74 | RUBY 75 | 76 | puts "## Examples with condition / scope" 77 | puts 78 | 79 | output_example(<<-DESC, <<-RUBY) 80 | comments of `my_post` that were made by an admin (Using a hash) 81 | DESC 82 | my_post.comments.where_assoc_exists(:author, is_admin: true) 83 | RUBY 84 | 85 | output_example(<<-DESC, <<-RUBY) 86 | comments of `my_post` that were not made by an admin (Using scope) 87 | DESC 88 | my_post.comments.where_assoc_not_exists(:author, &:admins) 89 | RUBY 90 | 91 | output_example(<<-DESC, <<-RUBY) 92 | Posts that have at least 5 reported comments (Using array condition) 93 | DESC 94 | Post.where_assoc_count(5, :<=, :comments, ["is_reported = ?", true]) 95 | RUBY 96 | 97 | output_example(<<-DESC, <<-RUBY) 98 | Posts made by an admin (Using a string) 99 | DESC 100 | Post.where_assoc_exists(:author, "is_admin = 't'") 101 | RUBY 102 | 103 | output_example(<<-DESC, <<-RUBY) 104 | comments of `my_post` that were not made by an admin (Using block and a scope) 105 | DESC 106 | my_post.comments.where_assoc_not_exists(:author) { admins } 107 | RUBY 108 | 109 | output_example(<<-DESC, <<-RUBY) 110 | Posts that have 5 to 10 reported comments (Using block with #where and range for count) 111 | DESC 112 | Post.where_assoc_count(5..10, :==, :comments) { where(is_reported: true) } 113 | RUBY 114 | 115 | output_example(<<-DESC, <<-RUBY) 116 | comments made in replies to my_user's post 117 | DESC 118 | Comment.where_assoc_exists(:post, author_id: my_user.id) 119 | RUBY 120 | 121 | puts "## Complex / powerful examples" 122 | puts 123 | 124 | output_example(<<-DESC, <<-RUBY) 125 | posts with a comment by an admin (uses array to go through multiple associations) 126 | DESC 127 | Post.where_assoc_exists([:comments, :author], is_admin: true) 128 | RUBY 129 | 130 | output_example(<<-DESC, <<-RUBY) 131 | posts where the author also commented on the post (uses a conditions between tables) 132 | DESC 133 | Post.where_assoc_exists(:comments, "posts.author_id = comments.author_id") 134 | RUBY 135 | 136 | output_example(<<-DESC, <<-RUBY) 137 | posts with a reported comment made by an admin (must be the same comments) 138 | DESC 139 | Post.where_assoc_exists(:comments, is_reported: true) { 140 | where_assoc_exists(:author, is_admin: true) 141 | } 142 | RUBY 143 | 144 | output_example(<<-DESC, <<-RUBY, footer: false) 145 | posts with a reported comment and a comment by an admin (can be different or same comments) 146 | DESC 147 | my_user.posts.where_assoc_exists(:comments, is_reported: true) 148 | .where_assoc_exists([:comments, :author], is_admin: true) 149 | RUBY 150 | 151 | output_example(<<-DESC, <<-RUBY, footer: false) 152 | Users with more posts than comments 153 | Using `my_users` to highlight that *_sql methods should always be called on the class 154 | DESC 155 | my_users.where("\#{User.only_assoc_count_sql(:posts)} > \#{User.only_assoc_count_sql(:comments)}") 156 | RUBY 157 | end 158 | 159 | 160 | # Below is just helpers for #puts_doc 161 | 162 | def my_post 163 | Post.order(:id).first 164 | end 165 | 166 | def my_user 167 | User.order(:id).first 168 | end 169 | 170 | def my_users 171 | User.all 172 | end 173 | 174 | def my_comment 175 | User.order(:id).first 176 | end 177 | 178 | def output_example(description, ruby, footer: true) 179 | description = description.strip_heredoc 180 | ruby = ruby.strip_heredoc 181 | 182 | # The +2 is for skipping the line with the call, and the DESC line 183 | initial_line_no = caller_locations[0].lineno + description.count("\n") + 2 184 | 185 | relation = eval(ruby, nil, __FILE__, initial_line_no) # rubocop:disable Security/Eval 186 | # Just making sure the query doesn't fail 187 | relation.to_a 188 | 189 | # #to_niceql formats the SQL a little 190 | sql = relation.to_niceql 191 | 192 | # Remove stupid indentation everywhere after the first line... 193 | sql = sql.gsub(/^ /, '') 194 | 195 | puts "```ruby" 196 | puts description.split("\n").map { |s| "# #{s}" }.join("\n") 197 | puts ruby 198 | puts "```" 199 | puts "```sql\n#{sql}\n```" 200 | 201 | return unless footer 202 | 203 | puts 204 | puts "---" 205 | puts 206 | end 207 | end 208 | 209 | # Lets make this a little denser 210 | module Niceql::Prettifier 211 | new_inline_keywords = INLINE_KEYWORDS + "|FROM" 212 | remove_const(:INLINE_KEYWORDS) 213 | INLINE_KEYWORDS = new_inline_keywords 214 | 215 | new_new_line_keywords = NEW_LINE_KEYWORDS.sub('FROM|', '') 216 | remove_const(:NEW_LINE_KEYWORDS) 217 | NEW_LINE_KEYWORDS = new_new_line_keywords 218 | 219 | remove_const(:KEYWORDS) 220 | KEYWORDS = "(#{NEW_LINE_KEYWORDS}|#{INLINE_KEYWORDS})#{AFTER_KEYWORD_SPACE}" 221 | end 222 | 223 | Examples.new.puts_doc 224 | -------------------------------------------------------------------------------- /examples/models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # These models are available in bin/console 4 | # 5 | # And easy way to play with these (it will create an sqlite3 DB in memory): 6 | # 7 | # git clone git@github.com:MaxLap/activerecord_where_assoc.git 8 | # cd activerecord_where_assoc 9 | # bundle install 10 | # bin/console 11 | # 12 | class User < ActiveRecord::Base 13 | has_many :posts, foreign_key: "author_id" 14 | has_many :comments, foreign_key: "author_id" 15 | 16 | scope :admins, -> { where(is_admin: true) } 17 | end 18 | 19 | class Post < ActiveRecord::Base 20 | belongs_to :author, class_name: "User" 21 | has_many :comments 22 | has_many :comments_author, through: :comments 23 | has_one :last_comment, -> { order("created_at DESC") }, class_name: "Comment" 24 | 25 | # Easy and powerful scope examples 26 | scope :by_admin, -> { where_assoc_exists(:author, &:admins) } 27 | scope :commented_on_by_admin, -> { where_assoc_exists(:comments, &:by_admin) } 28 | scope :with_many_reported_comments, ->(min_nb = 5) { where_assoc_count(min_nb, :<=, :comments, &:reported) } 29 | end 30 | 31 | class Comment < ActiveRecord::Base 32 | belongs_to :post 33 | belongs_to :author, class_name: "User" 34 | 35 | scope :reported, -> { where(is_reported: true) } 36 | scope :spam, -> { where(is_spam: true) } 37 | 38 | # Easy and powerful scope examples 39 | scope :by_admin, -> { where_assoc_exists(:author, &:admins) } 40 | end 41 | -------------------------------------------------------------------------------- /examples/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.verbose = false 4 | 5 | ActiveRecord::Schema.define do 6 | create_table :users do |t| 7 | t.string :username, null: false 8 | t.boolean :is_admin, default: false, null: false 9 | 10 | t.timestamps 11 | end 12 | 13 | create_table :posts do |t| 14 | t.references :author 15 | t.text :title 16 | t.text :content 17 | 18 | t.timestamps 19 | end 20 | 21 | create_table :comments do |t| 22 | t.references :author 23 | t.references :post 24 | t.text :content 25 | t.boolean :is_spam, default: false, null: false 26 | t.boolean :is_reported, default: false, null: false 27 | 28 | t.timestamps 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /examples/some_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | user1 = User.create!(username: "maxlap") 4 | 5 | my_post = user1.posts.create!(title: "First post", content: "This is new") 6 | my_post.comments.create!(author: user1, content: "Commenting on my own post!") 7 | -------------------------------------------------------------------------------- /gemfiles/rails_4_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.1.0" 6 | gem "sqlite3", "~> 1.3.6" 7 | 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_4_2.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.2.0" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "pg", "< 1.0.0" 8 | 9 | # Ruby 2.4 tried to use bigdecimal 3, which removed BigDecimal.new 10 | gem "bigdecimal", "1.3.5" 11 | 12 | gemspec path: "../" 13 | -------------------------------------------------------------------------------- /gemfiles/rails_5_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.0.0" 6 | gem "sqlite3", "~> 1.3.6" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails_5_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.1.0" 6 | gem "i18n", "< 1.6.0" 7 | gem "sqlite3", "~> 1.3.6" 8 | gem "mysql2", "~> 0.4.0" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_5_2.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.2.0" 6 | gem "sqlite3", "~> 1.3.6" 7 | gem "mysql2", "~> 0.4.0" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_6_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 6.0.0" 6 | gem "sqlite3", "~> 1.4.0" 7 | gem "mysql2", "~> 0.4.0" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/rails_6_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 6.1.0" 6 | gem "sqlite3", "~> 1.4.0" 7 | gem "pg", "~> 1.1" 8 | gem "mysql2", "~> 0.5" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 9 | gem "prime" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_7_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 7.0.0" 6 | gem "sqlite3", "~> 1.4.0" 7 | gem "pg", "~> 1.1" 8 | gem "mysql2", "~> 0.5" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 9 | gem "prime" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_7_1.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 7.1.0" 6 | gem "sqlite3", "~> 1.4.0" 7 | gem "pg", "~> 1.1" 8 | gem "mysql2", "~> 0.5" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 9 | gem "prime" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_7_2.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", github: "rails/rails", branch: "7-2-stable" 6 | gem "sqlite3", "~> 1.4.0" 7 | gem "pg", "~> 1.1" 8 | gem "mysql2", "~> 0.5" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 9 | gem "prime" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_8_0.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", github: "rails/rails", branch: "8-0-stable" 6 | gem "sqlite3" 7 | gem "pg", "~> 1.1" 8 | gem "mysql2", "~> 0.5" if ENV["CI"] || ENV["ALL_DB"] || ENV["DB"] == "mysql" 9 | gem "prime" 10 | 11 | gemspec path: "../" 12 | -------------------------------------------------------------------------------- /gemfiles/rails_head.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # rubocop:disable Bundler/DuplicatedGem 4 | source "https://rubygems.org" 5 | 6 | # In order to get the latest ref to rails, we use the github's API 7 | # We need to pass an access token when on Travis-CI because all requests to 8 | # github come from the same IP, going over the unauthenticated limits. 9 | 10 | require "json" 11 | require "net/http" 12 | require "uri" 13 | 14 | if ENV["GITHUB_ACCESS_TOKEN"] 15 | # Thnx to https://jhawthorn.github.io/curl-to-ruby/ 16 | 17 | req_github = lambda do |url| 18 | uri = ::URI.parse(url) 19 | Net::HTTP.start(uri.host, uri.port, 20 | use_ssl: uri.scheme == "https") do |http| 21 | 22 | request = Net::HTTP::Get.new uri.request_uri 23 | request.basic_auth "maxlap", ENV["GITHUB_ACCESS_TOKEN"] 24 | 25 | http.request(request) 26 | end 27 | end 28 | 29 | response = req_github.call("https://api.github.com/repos/rails/rails/branches/master") 30 | rails_commit_sha = JSON.parse(response.body)["commit"]["sha"] 31 | 32 | response = req_github.call("https://api.github.com/repos/rails/arel/branches/master") 33 | arel_commit_sha = JSON.parse(response.body)["commit"]["sha"] 34 | 35 | gem "activerecord", git: "https://github.com/rails/rails.git", ref: rails_commit_sha 36 | gem "arel", git: "https://github.com/rails/arel.git", ref: arel_commit_sha 37 | else 38 | gem "activerecord", git: "https://github.com/rails/rails.git" 39 | gem "arel", git: "https://github.com/rails/arel.git" 40 | end 41 | gem "mysql2", "~> 0.5" 42 | gem "pg", "~> 1.1" 43 | gem "prime" 44 | 45 | gemspec path: "../" 46 | -------------------------------------------------------------------------------- /gemfiles/readme.txt: -------------------------------------------------------------------------------- 1 | If you want to run your stuff against a specific gemfile: 2 | 3 | Then you must set the BUNDLE_GEMFILE environment variable. Ex: 4 | BUNDLE_GEMFILE=gemfiles/rails_6_0.gemfile bundle exec rake test 5 | -------------------------------------------------------------------------------- /lib/active_record_where_assoc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "active_record_where_assoc/version" 4 | require "active_record" 5 | 6 | module ActiveRecordWhereAssoc 7 | # Default options for the gem. Meant to be modified in place by external code, such as in 8 | # an initializer. 9 | # Ex: 10 | # ActiveRecordWhereAssoc.default_options[:ignore_limit] = true 11 | # 12 | # A description for each can be found in RelationReturningMethods@Options. 13 | # 14 | # :ignore_limit is the only one to consider changing, when you are using MySQL, since limit are 15 | # never supported on it. Otherwise, the safety of having to pass the options yourself 16 | # and noticing you made a mistake / avoiding the need for extra queries is worth the extra code. 17 | def self.default_options 18 | @default_options ||= { 19 | ignore_limit: false, 20 | never_alias_limit: false, 21 | poly_belongs_to: :raise, 22 | } 23 | end 24 | end 25 | 26 | require_relative "active_record_where_assoc/core_logic" 27 | require_relative "active_record_where_assoc/relation_returning_methods" 28 | require_relative "active_record_where_assoc/relation_returning_delegates" 29 | require_relative "active_record_where_assoc/sql_returning_methods" 30 | 31 | ActiveSupport.on_load(:active_record) do 32 | ActiveRecord.eager_load! 33 | 34 | ActiveRecord::Relation.include(ActiveRecordWhereAssoc::RelationReturningMethods) 35 | ActiveRecord::Base.extend(ActiveRecordWhereAssoc::RelationReturningDelegates) 36 | ActiveRecord::Base.extend(ActiveRecordWhereAssoc::SqlReturningMethods) 37 | end 38 | -------------------------------------------------------------------------------- /lib/active_record_where_assoc/active_record_compat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecordWhereAssoc 4 | module ActiveRecordCompat 5 | if ActiveRecord.gem_version >= Gem::Version.new("6.1.0.rc1") 6 | JoinKeys = Struct.new(:key, :foreign_key) 7 | def self.join_keys(reflection, poly_belongs_to_klass) 8 | if poly_belongs_to_klass 9 | JoinKeys.new(reflection.join_primary_key(poly_belongs_to_klass), reflection.join_foreign_key) 10 | else 11 | JoinKeys.new(reflection.join_primary_key, reflection.join_foreign_key) 12 | end 13 | end 14 | 15 | elsif ActiveRecord.gem_version >= Gem::Version.new("5.1") 16 | def self.join_keys(reflection, poly_belongs_to_klass) 17 | if poly_belongs_to_klass 18 | reflection.get_join_keys(poly_belongs_to_klass) 19 | else 20 | reflection.join_keys 21 | end 22 | end 23 | elsif ActiveRecord.gem_version >= Gem::Version.new("4.2") 24 | def self.join_keys(reflection, poly_belongs_to_klass) 25 | reflection.join_keys(poly_belongs_to_klass || reflection.klass) 26 | end 27 | else 28 | # 4.1 change that introduced JoinKeys: 29 | # https://github.com/rails/rails/commit/5823e429981dc74f8f53187d2ab573823381bf28#diff-523caff658498027f61cae9d91c8503dL108 30 | JoinKeys = Struct.new(:key, :foreign_key) 31 | def self.join_keys(reflection, poly_belongs_to_klass) 32 | if reflection.source_macro == :belongs_to 33 | key = reflection.association_primary_key(poly_belongs_to_klass) 34 | foreign_key = reflection.foreign_key 35 | else 36 | key = reflection.foreign_key 37 | foreign_key = reflection.active_record_primary_key 38 | end 39 | 40 | JoinKeys.new(key, foreign_key) 41 | end 42 | end 43 | 44 | if ActiveRecord.gem_version >= Gem::Version.new("5.0") 45 | def self.chained_reflection_and_chained_constraints(reflection) 46 | pairs = reflection.chain.map do |ref| 47 | # PolymorphicReflection is a super weird thing. Like a partial reflection, I don't get it. 48 | # Seems like just bypassing it works for our needs. 49 | # When doing a has_many through that has a polymorphic source and a source_type, this ends up 50 | # part of the chain instead of the regular HasManyReflection that one would expect. 51 | ref = ref.instance_variable_get(:@reflection) if ref.is_a?(ActiveRecord::Reflection::PolymorphicReflection) 52 | 53 | [ref, ref.constraints] 54 | end 55 | 56 | pairs.transpose 57 | end 58 | else 59 | def self.chained_reflection_and_chained_constraints(reflection) 60 | [reflection.chain, reflection.scope_chain] 61 | end 62 | end 63 | 64 | if ActiveRecord.gem_version >= Gem::Version.new("5.0") 65 | def self.parent_reflection(reflection) 66 | reflection.parent_reflection 67 | end 68 | else 69 | def self.parent_reflection(reflection) 70 | _parent_name, parent_refl = reflection.parent_reflection 71 | parent_refl 72 | end 73 | end 74 | 75 | if ActiveRecord.gem_version >= Gem::Version.new("4.2") && ActiveRecord.gem_version < Gem::Version.new("7.2.0.alpha") 76 | def self.normalize_association_name(association_name) 77 | association_name.to_s 78 | end 79 | else 80 | def self.normalize_association_name(association_name) 81 | association_name.to_sym 82 | end 83 | end 84 | 85 | if ActiveRecord.gem_version >= Gem::Version.new("5.0") 86 | def self.through_reflection?(reflection) 87 | reflection.through_reflection? 88 | end 89 | else 90 | def self.through_reflection?(reflection) 91 | reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) 92 | end 93 | end 94 | 95 | if ActiveRecord.gem_version >= Gem::Version.new("7.1.0.alpha") 96 | def self.null_relation?(reflection) 97 | reflection.null_relation? 98 | end 99 | else 100 | def self.null_relation?(reflection) 101 | reflection.is_a?(ActiveRecord::NullRelation) 102 | end 103 | end 104 | 105 | if ActiveRecord.gem_version >= Gem::Version.new("6.0") 106 | def self.indexes(model) 107 | model.connection.schema_cache.indexes(model.table_name) 108 | end 109 | else 110 | def self.indexes(model) 111 | model.connection.indexes(model.table_name) 112 | end 113 | end 114 | 115 | @unique_indexes_cache = {} 116 | def self.has_unique_index?(model, column_names) 117 | column_names = Array(column_names).map(&:to_s) 118 | @unique_indexes_cache.fetch([model, column_names]) do |k| 119 | unique_indexes = indexes(model).select(&:unique) 120 | columns_names_set = Set.new(column_names) 121 | 122 | # We check for an index whose columns are a subset of the columns we specify 123 | # This way, a composite column_names will find uniqueness if just a single of the column is unique 124 | @unique_indexes_cache[k] = unique_indexes.any? { |ui| Set.new(ui.columns) <= columns_names_set } 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/active_record_where_assoc/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecordWhereAssoc 4 | class MySQLDoesntSupportSubLimitError < StandardError 5 | end 6 | 7 | class PolymorphicBelongsToWithoutClasses < StandardError 8 | end 9 | 10 | class NeverAliasLimitDoesntWorkWithCompositePrimaryKeysError < StandardError 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/active_record_where_assoc/relation_returning_delegates.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Needed for delegate 4 | require "active_support" 5 | 6 | module ActiveRecordWhereAssoc 7 | module RelationReturningDelegates 8 | # Delegating the methods in RelationReturningMethods from ActiveRecord::Base to :all. Same thing ActiveRecord does for #where. 9 | new_relation_returning_methods = RelationReturningMethods.public_instance_methods 10 | delegate(*new_relation_returning_methods, to: :all) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/active_record_where_assoc/sql_returning_methods.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecordWhereAssoc 4 | # The methods in this module return partial SQL queries. These are used by the main methods of 5 | # this gem: the #where_assoc_* methods located in RelationReturningMethods. But in some situation, the SQL strings can be useful to 6 | # do complex manual queries by embedding them in your own SQL code. 7 | # 8 | # Those methods should be used directly on your model's class. You can use them from a relation, but the result will be 9 | # the same, so your intent will be clearer by doing it on the class directly. 10 | # 11 | # # This is the recommended way: 12 | # sql = User.assoc_exists_sql(:posts) 13 | # 14 | # # While this also works, it may be confusing when reading the code: 15 | # sql = my_filtered_users.assoc_exists_sql(:posts) 16 | # # the sql variable is not affected by my_filtered_users. 17 | module SqlReturningMethods 18 | # This method returns a string containing the SQL condition used by RelationReturningMethods#where_assoc_exists. 19 | # You can pass that SQL string directly to #where to get the same result as RelationReturningMethods#where_assoc_exists. 20 | # This can be useful to get the SQL of an EXISTS query for use in your own SQL code. 21 | # 22 | # For example: 23 | # # Users with a post or a comment 24 | # User.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}") 25 | # my_users.where("#{User.assoc_exists_sql(:posts)} OR #{User.assoc_exists_sql(:comments)}") 26 | # 27 | # The parameters are the same as RelationReturningMethods#where_assoc_exists, including the 28 | # possibility of specifying a list of association_name. 29 | def assoc_exists_sql(association_name, conditions = nil, options = {}, &block) 30 | ActiveRecordWhereAssoc::CoreLogic.assoc_exists_sql(self, association_name, conditions, options, &block) 31 | end 32 | 33 | # This method generates the SQL query used by RelationReturningMethods#where_assoc_not_exists. 34 | # This method is the same as #assoc_exists_sql, but for RelationReturningMethods#where_assoc_not_exists. 35 | # 36 | # The parameters are the same as RelationReturningMethods#where_assoc_not_exists, including the 37 | # possibility of specifying a list of association_name. 38 | def assoc_not_exists_sql(association_name, conditions = nil, options = {}, &block) 39 | ActiveRecordWhereAssoc::CoreLogic.assoc_not_exists_sql(self, association_name, conditions, options, &block) 40 | end 41 | 42 | # This method returns a string containing the SQL condition used by RelationReturningMethods#where_assoc_count. 43 | # You can pass that SQL string directly to #where to get the same result as RelationReturningMethods#where_assoc_count. 44 | # This can be useful to get the SQL query to compare the count of an association for use in your own SQL code. 45 | # 46 | # For example: 47 | # # Users with at least 10 posts or at least 10 comment 48 | # User.where("#{User.compare_assoc_count_sql(:posts, :>=, 10)} OR #{User.compare_assoc_count_sql(:comments, :>=, 10)}") 49 | # my_users.where("#{User.compare_assoc_count_sql(:posts, :>=, 10)} OR #{User.compare_assoc_count_sql(:comments, :>=, 10)}") 50 | # 51 | # # The older way of doing the same thing (the order of parameter used to be reversed, 52 | # # both order work when using a number or a range as one of the operand) 53 | # User.where("#{User.compare_assoc_count_sql(10, :<=, :posts)} OR #{User.compare_assoc_count_sql(10, :<=, :comments)}") 54 | # my_users.where("#{User.compare_assoc_count_sql(10, :<=, :posts)} OR #{User.compare_assoc_count_sql(10, :<=, :comments)}") 55 | # 56 | # The parameters are the same as RelationReturningMethods#where_assoc_count, including the 57 | # possibility of specifying a list of association_name. 58 | def compare_assoc_count_sql(left_assoc_or_value, operator, right_assoc_or_value, conditions = nil, options = {}, &block) 59 | ActiveRecordWhereAssoc::CoreLogic.compare_assoc_count_sql(self, left_assoc_or_value, operator, right_assoc_or_value, conditions, options, &block) 60 | end 61 | 62 | # This method returns a string containing the SQL to count an association used by RelationReturningMethods#where_assoc_count. 63 | # The returned SQL does not do a comparison, only the counting part. So you can do the comparison yourself. 64 | # This can be useful to get the SQL to count the an association query for use in your own SQL code. 65 | # 66 | # For example: 67 | # # Users with more posts than comments 68 | # User.where("#{User.only_assoc_count_sql(:posts)} > #{User.only_assoc_count_sql(:comments)}") 69 | # my_users.where("#{User.only_assoc_count_sql(:posts)} > #{User.only_assoc_count_sql(:comments)}") 70 | # 71 | # Since the comparison is not made by this method, the first 2 parameters (left_operand and operator) 72 | # of RelationReturningMethods#where_assoc_count are not accepted by this method. The remaining 73 | # parameters of RelationReturningMethods#where_assoc_count are accepted, which are the same 74 | # the same as those of RelationReturningMethods#where_assoc_exists. 75 | def only_assoc_count_sql(association_name, conditions = nil, options = {}, &block) 76 | ActiveRecordWhereAssoc::CoreLogic.only_assoc_count_sql(self, association_name, conditions, options, &block) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/active_record_where_assoc/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecordWhereAssoc 4 | VERSION = "1.3.0".freeze 5 | end 6 | -------------------------------------------------------------------------------- /lib/activerecord_where_assoc.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Just in case of typo in the require. Call the right one automatically 4 | require_relative "active_record_where_assoc" 5 | -------------------------------------------------------------------------------- /test/support/database_setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Based on: 4 | # https://github.com/ReneB/activerecord-like/blob/72ca9d3c11f3d5a34f9bee530df5d43303259f51/test/helper.rb 5 | 6 | module Test 7 | module Postgres 8 | def self.connect_db 9 | ActiveRecord::Base.establish_connection(postgres_config) 10 | end 11 | 12 | def self.drop_and_create_database 13 | # drops and create need to be performed with a connection to the 'postgres' (system) database 14 | temp_connection = postgres_config.merge(database: "postgres", schema_search_path: "public") 15 | ActiveRecord::Base.establish_connection(temp_connection) 16 | 17 | # drop the old database (if it exists) 18 | ActiveRecord::Base.connection.drop_database(database_name) 19 | 20 | # create new 21 | ActiveRecord::Base.connection.create_database(database_name) 22 | end 23 | 24 | def self.postgres_config 25 | @postgres_config ||= { 26 | adapter: "postgresql", 27 | host: "localhost", 28 | port: 5432, 29 | database: database_name, 30 | username: db_user_name, 31 | password: db_password, 32 | } 33 | end 34 | 35 | def self.database_name 36 | "activerecord_where_assoc" 37 | end 38 | 39 | def self.db_user_name 40 | return ENV["PGUSER"] if ENV["PGUSER"].present? 41 | `whoami`.strip 42 | end 43 | 44 | def self.db_password 45 | return ENV["PGPASSWORD"] if ENV["PGPASSWORD"].present? 46 | nil 47 | end 48 | end 49 | 50 | module SQLite3 51 | def self.connect_db 52 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 53 | end 54 | 55 | def self.drop_and_create_database 56 | # NOOP for SQLite3 57 | end 58 | end 59 | 60 | module MySQL 61 | def self.connect_db 62 | ActiveRecord::Base.establish_connection(mysql_config) 63 | end 64 | 65 | def self.drop_and_create_database 66 | temp_connection = mysql_config.merge(database: "mysql") 67 | 68 | ActiveRecord::Base.establish_connection(temp_connection) 69 | 70 | # drop the old database (if it exists) 71 | ActiveRecord::Base.connection.drop_database(database_name) 72 | 73 | # create new 74 | ActiveRecord::Base.connection.create_database(database_name) 75 | end 76 | 77 | def self.mysql_config 78 | @mysql_config ||= { 79 | adapter: "mysql2", 80 | database: database_name, 81 | username: db_user_name, 82 | password: db_password, 83 | collation: 'utf8_general_ci', 84 | } 85 | end 86 | 87 | def self.db_user_name 88 | return ENV["MYSQL_USER"] if ENV["MYSQL_USER"].present? 89 | `whoami` 90 | end 91 | 92 | def self.db_password 93 | return ENV["MYSQL_PASSWORD"] if ENV["MYSQL_PASSWORD"].present? 94 | nil 95 | end 96 | 97 | def self.database_name 98 | "activerecord_where_assoc" 99 | end 100 | end 101 | end 102 | 103 | if ENV["DB"].blank? 104 | puts "No DB environment variable provided, testing using SQLite3" 105 | ENV["DB"] = "sqlite3" 106 | end 107 | 108 | case ENV["DB"] 109 | when "pg", "postgres", "postgresql" 110 | Test::SelectedDBHelper = Test::Postgres 111 | when "sqlite3" 112 | Test::SelectedDBHelper = Test::SQLite3 113 | when "mysql" 114 | Test::SelectedDBHelper = Test::MySQL 115 | else 116 | raise "Unhandled DB parameter: #{ENV['DB'].inspect}" 117 | end 118 | 119 | Test::SelectedDBHelper.drop_and_create_database unless ENV["CI"] 120 | Test::SelectedDBHelper.connect_db 121 | -------------------------------------------------------------------------------- /test/support/load_test_env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "pry" 5 | 6 | require_relative "../../lib/active_record_where_assoc" 7 | 8 | if ENV["DB"] == "mysql" && [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR].join('.') < '5.1' 9 | puts "Exiting from tests with MySQL as success without doing them." 10 | puts "This is because automated test won't seem to run MySQL for some reason for this old Rails version." 11 | exit 0 12 | end 13 | 14 | require "active_support" 15 | 16 | require_relative "database_setup" 17 | require_relative "schema" 18 | require_relative "models" 19 | 20 | require "niceql" if RUBY_VERSION >= "2.3.0" 21 | 22 | 23 | module TestHelpers 24 | def self.condition_value_result_for(*source_associations) 25 | source_associations.map do |source_association| 26 | model_name, association = source_association.to_s.split("_", 2) 27 | value = BaseTestModel.model_associations_conditions[[model_name, association]] 28 | 29 | raise "No condition #{source_association} found" if value.nil? 30 | 31 | value 32 | end.inject(:*) 33 | end 34 | delegate :condition_value_result_for, to: "TestHelpers" 35 | end 36 | -------------------------------------------------------------------------------- /test/support/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.verbose = false 4 | 5 | # Every table is a step. In tests, you always go toward the bigger step. 6 | # You can do it using a belongs_to, or has_one/has_many. 7 | # Try to make most columns unique so that any wrong column used is obvious in an error message. 8 | 9 | ActiveRecord::Schema.define do 10 | create_table :s0s do |t| 11 | t.integer :s1_id 12 | 13 | t.integer :s0s_belongs_to_poly_id 14 | t.string :s0s_belongs_to_poly_type 15 | 16 | t.integer :s0s_column, limit: 8 17 | t.integer :s0s_adhoc_column, limit: 8 18 | end 19 | 20 | create_table :s1s do |t| 21 | t.integer :s0_id 22 | t.integer :s0_u_id 23 | t.integer :s2_id 24 | 25 | t.integer :has_s1s_poly_id 26 | t.string :has_s1s_poly_type 27 | t.integer :s1s_belongs_to_poly_id 28 | t.string :s1s_belongs_to_poly_type 29 | 30 | t.integer :s1s_column, limit: 8 31 | t.integer :s1s_adhoc_column, limit: 8 32 | 33 | t.index ["s0_u_id"], name: "index_s1s__s0_u_id", unique: true 34 | end 35 | 36 | create_table :s2s do |t| 37 | t.integer :s1_id 38 | t.integer :s1_u_id 39 | t.integer :s3_id 40 | 41 | t.integer :has_s2s_poly_id 42 | t.string :has_s2s_poly_type 43 | t.integer :s2s_belongs_to_poly_id 44 | t.string :s2s_belongs_to_poly_type 45 | 46 | t.integer :s2s_column, limit: 8 47 | t.integer :s2s_adhoc_column, limit: 8 48 | 49 | t.index ["s1_u_id"], name: "index_s2s__s1_u_id", unique: true 50 | end 51 | 52 | create_table :s3s do |t| 53 | t.integer :s2_id 54 | 55 | t.integer :has_s3s_poly_id 56 | t.string :has_s3s_poly_type 57 | 58 | t.integer :s3s_column, limit: 8 59 | t.integer :s3s_adhoc_column, limit: 8 60 | end 61 | 62 | create_join_table :s0s, :s1s 63 | create_join_table :s1s, :s2s 64 | create_join_table :s2s, :s3s 65 | 66 | if Test::SelectedDBHelper == Test::Postgres 67 | execute <<-SQL 68 | CREATE SCHEMA foo_schema; 69 | SQL 70 | 71 | execute <<-SQL 72 | CREATE SCHEMA bar_schema; 73 | SQL 74 | 75 | execute <<-SQL 76 | CREATE SCHEMA spam_schema; 77 | SQL 78 | elsif Test::SelectedDBHelper == Test::SQLite3 79 | # ATTACH DATABASE (the equivalent) is not supported by active record. 80 | # See https://github.com/rails/rails/pull/35339#issuecomment-466265426 81 | elsif Test::SelectedDBHelper == Test::MySQL 82 | execute <<-SQL 83 | CREATE DATABASE foo_schema; 84 | SQL 85 | 86 | execute <<-SQL 87 | CREATE DATABASE bar_schema; 88 | SQL 89 | 90 | execute <<-SQL 91 | CREATE DATABASE spam_schema; 92 | SQL 93 | end 94 | 95 | if Test::SelectedDBHelper != Test::SQLite3 96 | create_table "foo_schema.schema_s0s" do |t| 97 | t.integer :schema_s1_id 98 | end 99 | 100 | create_table "bar_schema.schema_s1s" do |t| 101 | t.integer :schema_s0_id 102 | t.integer :schema_s0_u_id 103 | t.integer :schema_s2_id 104 | 105 | t.index ["schema_s0_u_id"], name: "index_schema_s1s__schema_s0_u_id", unique: true 106 | end 107 | 108 | create_join_table "schema_s0s", "schema_s1s", table_name: "spam_schema.schema_s0s_schema_s1s" 109 | 110 | create_table "bar_schema.schema_s2s" do |t| 111 | t.integer :schema_s1_id 112 | t.integer :schema_s1_u_id 113 | 114 | t.index ["schema_s1_u_id"], name: "index_schema_s2s__schema_s1_u_id", unique: true 115 | end 116 | end 117 | 118 | create_table "sti_s0s" do |t| 119 | t.integer :sti_s1_id 120 | t.string :sti_s1_type 121 | t.string :type 122 | end 123 | 124 | create_table "sti_s1s" do |t| 125 | t.integer :sti_s0_id 126 | t.string :sti_s0_type 127 | t.string :type 128 | end 129 | 130 | create_join_table "sti_s0s", "sti_s1s", table_name: "sti_s0s_sti_s1s" 131 | 132 | create_table "lew_s0s" do |t| 133 | t.integer :lew_s1_id 134 | 135 | t.string :lew_s0s_column 136 | end 137 | 138 | create_table "lew_s1s" do |t| 139 | t.integer :lew_s0_id 140 | 141 | t.string :lew_s1s_column 142 | end 143 | 144 | create_join_table "lew_s0s", "lew_s1s", table_name: "lew_s0s_lew_s1s" 145 | 146 | create_table :recursive_s do |t| 147 | t.integer :recursive_s_column 148 | 149 | t.integer :belongs_id 150 | t.string :belongs_type 151 | 152 | t.integer :has_id 153 | t.string :has_type 154 | end 155 | 156 | create_table :unabstract_models do |t| 157 | t.integer :belongs_id 158 | t.string :belongs_type 159 | 160 | t.integer :has_id 161 | t.string :has_type 162 | 163 | t.integer :unabstracted_models_column, limit: 8 164 | t.integer :unabstracted_models_adhoc_column, limit: 8 165 | end 166 | 167 | create_table :never_abstracted_models do |t| 168 | t.integer :belongs_id 169 | t.string :belongs_type 170 | 171 | t.integer :has_id 172 | t.string :has_type 173 | 174 | t.integer :never_abstracted_models_column, limit: 8 175 | t.integer :never_abstracted_models_adhoc_column, limit: 8 176 | end 177 | 178 | create_table :ck0s, primary_key: [:an_id0, :a_str0] do |t| 179 | t.integer :an_id0 180 | t.string :a_str0, limit: 20 # Needed for MySQL's primary keys 181 | 182 | t.integer :ck0s_column 183 | t.integer :ck0s_adhoc_column 184 | end 185 | 186 | create_table :ck1s, primary_key: [:an_id1, :a_str1] do |t| 187 | t.integer :an_id1 188 | t.string :a_str1, limit: 20 # Needed for MySQL's primary keys 189 | 190 | t.integer :an_id0 191 | t.string :a_str0, limit: 20 # Needed for MySQL's primary keys 192 | 193 | t.integer :ck1s_column 194 | t.integer :ck1s_adhoc_column 195 | end 196 | 197 | create_join_table "ck0s", "ck1s" 198 | 199 | 200 | create_table :ck2s, primary_key: [:an_id2, :a_str2] do |t| 201 | t.integer :an_id2 202 | t.string :a_str2, limit: 20 # Needed for MySQL's primary keys 203 | 204 | t.integer :an_id1 205 | t.string :a_str1, limit: 20 # Needed for MySQL's primary keys 206 | 207 | t.integer :ck2s_column 208 | t.integer :ck2s_adhoc_column 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | coverage_config = proc do 4 | add_filter "/test/" 5 | end 6 | 7 | if ENV["CI"] 8 | # No doing coverage badge at the moment.. Coveralls stopped working right after switching to 9 | # Github actions, and its doc is too bad for me to figure it out. A few hours lost is more 10 | # than I wanted to put into this. 11 | else 12 | require "deep_cover" 13 | end 14 | 15 | require "logger" 16 | require_relative "support/load_test_env" 17 | require "minitest/autorun" 18 | require_relative "support/custom_asserts" 19 | 20 | 21 | class MyMinitestSpec < Minitest::Spec 22 | include TestHelpers 23 | 24 | # Annoying stuff for tests to run in transactions 25 | include ActiveRecord::TestFixtures 26 | if ActiveRecord.gem_version >= Gem::Version.new("5.0") 27 | self.use_transactional_tests = true 28 | def run_in_transaction? 29 | self.use_transactional_tests 30 | end 31 | else 32 | self.use_transactional_fixtures = true 33 | def run_in_transaction? 34 | self.use_transactional_fixtures 35 | end 36 | end 37 | 38 | if %w(1 true).include?(ENV["SQL_WITH_FAILURES"]) 39 | before do 40 | @prev_logger = ActiveRecord::Base.logger 41 | @my_logged_string_io = StringIO.new 42 | @my_logger = Logger.new(@my_logged_string_io) 43 | @my_logger.formatter = proc do |severity, datetime, progname, msg| 44 | "#{msg}\n" 45 | end 46 | ActiveRecord::Base.logger = @my_logger 47 | end 48 | 49 | after do |test_case| 50 | ActiveRecord::Base.logger = @prev_logger 51 | next if test_case.passed? || test_case.skipped? 52 | 53 | @my_logged_string_io.rewind 54 | logged_lines = @my_logged_string_io.readlines 55 | 56 | # Ignore lines that are about the savepoints. Need to remove color codes first. 57 | logged_lines.reject! { |line| line.gsub(/\e\[[0-9;]*m/, "")[/\)\s*(?:RELEASE )?SAVEPOINT/i] } 58 | 59 | logged_string = logged_lines.join 60 | if logged_string.present? 61 | exc = test_case.failure 62 | orig_message = exc.message 63 | exc.define_singleton_method(:message) do 64 | "#{orig_message}\n#{logged_string}" 65 | end 66 | end 67 | end 68 | end 69 | end 70 | 71 | # Use my custom test case for the specs 72 | Minitest::Spec.register_spec_type(//, MyMinitestSpec) 73 | -------------------------------------------------------------------------------- /test/tests/conditions/wa_has_one_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa has_one" do 6 | # MySQL doesn't support has_one 7 | next if Test::SelectedDBHelper == Test::MySQL 8 | 9 | let(:s0) { S0.create_default! } 10 | 11 | it "matches with Arel condition" do 12 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 13 | assert_wa(1, :o1, S1.arel_table[S1.adhoc_column_name].eq(1)) 14 | assert_wa(0, :o1, S1.arel_table[S1.adhoc_column_name].eq(2)) 15 | end 16 | 17 | it "matches with Array-String condition" do 18 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 19 | assert_wa(1, :o1, ["#{S1.adhoc_column_name} = ?", 1]) 20 | assert_wa(0, :o1, ["#{S1.adhoc_column_name} = ?", 2]) 21 | end 22 | 23 | it "matches with a block condition" do 24 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 25 | assert_wa(1, :o1) { |s| s.where(S1.adhoc_column_name => 1) } 26 | assert_wa(0, :o1) { |s| s.where(S1.adhoc_column_name => 2) } 27 | end 28 | 29 | it "matches with a block condition that returns nil" do 30 | s0 31 | assert_wa(0, :o1) { |s| nil } 32 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 33 | assert_wa(1, :o1) { |s| nil } 34 | end 35 | 36 | it "matches with a no arg block condition" do 37 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 38 | assert_wa(1, :o1) { where(S1.adhoc_column_name => 1) } 39 | assert_wa(0, :o1) { where(S1.adhoc_column_name => 2) } 40 | end 41 | 42 | it "matches with a no arg block condition that returns nil" do 43 | s0 44 | assert_wa(0, :o1) { nil } 45 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 46 | assert_wa(1, :o1) { nil } 47 | end 48 | 49 | it "matches with Hash condition" do 50 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 51 | assert_wa(1, :o1, S1.adhoc_column_name => 1) 52 | assert_wa(0, :o1, S1.adhoc_column_name => 2) 53 | end 54 | 55 | it "matches with String condition" do 56 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 57 | assert_wa(1, :o1, "#{S1.adhoc_column_name} = 1") 58 | assert_wa(0, :o1, "#{S1.adhoc_column_name} = 2") 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/tests/raw_sql_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | # rubocop:disable Metrics/LineLength 6 | describe "SQL methods" do 7 | it "#assoc_exists_sql generates expected sql" do 8 | sql = S0.assoc_exists_sql(:m1) 9 | expected_sql = %(EXISTS (SELECT 1 FROM "s1s" WHERE ("s1s"."s1s_column" % __NUMBER__ = 0) AND ("s1s"."s1s_column" % __NUMBER__ = 0) AND "s1s"."s0_id" = "s0s"."id")) 10 | expected_sql_regex = Regexp.new(Regexp.quote(expected_sql).gsub("__NUMBER__", '\d+').gsub('"', '["`]?')) 11 | assert_match expected_sql_regex, sql.gsub(/\s+/, ' ') 12 | end 13 | 14 | it "#assoc_not_exists_sql generates expected sql" do 15 | sql = S0.assoc_not_exists_sql(:m1) 16 | expected_sql = %(NOT EXISTS (SELECT 1 FROM "s1s" WHERE ("s1s"."s1s_column" % __NUMBER__ = 0) AND ("s1s"."s1s_column" % __NUMBER__ = 0) AND "s1s"."s0_id" = "s0s"."id")) 17 | expected_sql_regex = Regexp.new(Regexp.quote(expected_sql).gsub("__NUMBER__", '\d+').gsub('"', '["`]?')) 18 | assert_match expected_sql_regex, sql.gsub(/\s+/, ' ') 19 | end 20 | 21 | it "#only_assoc_count_sql generates expected sql" do 22 | sql = S0.only_assoc_count_sql(:m1) 23 | expected_sql = %(COALESCE((SELECT COUNT(*) FROM "s1s" WHERE ("s1s"."s1s_column" % __NUMBER__ = 0) AND ("s1s"."s1s_column" % __NUMBER__ = 0) AND "s1s"."s0_id" = "s0s"."id"), 0)) 24 | expected_sql_regex = Regexp.new(Regexp.quote(expected_sql).gsub("__NUMBER__", '\d+').gsub('"', '["`]?')) 25 | assert_match expected_sql_regex, sql.gsub(/\s+/, ' ') 26 | end 27 | 28 | it "#compare_assoc_count_sql generates expected sql" do 29 | sql = S0.compare_assoc_count_sql(5, :<, :m1) 30 | expected_sql = %((5) < COALESCE((SELECT COUNT(*) FROM "s1s" WHERE ("s1s"."s1s_column" % __NUMBER__ = 0) AND ("s1s"."s1s_column" % __NUMBER__ = 0) AND "s1s"."s0_id" = "s0s"."id"), 0)) 31 | expected_sql_regex = Regexp.new(Regexp.quote(expected_sql).gsub("__NUMBER__", '\d+').gsub('"', '["`]?')) 32 | assert_match expected_sql_regex, sql.gsub(/\s+/, ' ') 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_belongs_to_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "finds the right matching belongs_tos" do 9 | s0_1 = s0 10 | s0_1.create_assoc!(:b1, :S0_b1) 11 | 12 | _s0_2 = S0.create_default! 13 | 14 | s0_3 = S0.create_default! 15 | s0_3.create_assoc!(:b1, :S0_b1) 16 | 17 | _s0_4 = S0.create_default! 18 | 19 | assert_equal [s0_1, s0_3], S0.where_assoc_count(1, :==, :b1).to_a.sort_by(&:id) 20 | end 21 | 22 | it "finds a matching belongs_to" do 23 | s0.create_assoc!(:b1, :S0_b1) 24 | s0.create_assoc!(:b1, :S0_b1) 25 | 26 | assert_wa(1, :b1) 27 | end 28 | 29 | it "doesn't find without any belongs_to" do 30 | s0 31 | assert_wa(0, :b1) 32 | end 33 | 34 | it "doesn't find with a non matching belongs_to" do 35 | s0.create_bad_assocs!(:b1, :S0_b1) 36 | 37 | assert_wa(0, :b1) 38 | end 39 | 40 | it "finds a matching has_many through belongs_to" do 41 | b1 = s0.create_assoc!(:b1, :S0_b1) 42 | b1.create_assoc!(:m2, :S0_m2b1, :S1_m2) 43 | b1.create_assoc!(:m2, :S0_m2b1, :S1_m2) 44 | 45 | assert_wa(2, :m2b1) 46 | end 47 | 48 | it "doesn't find without any has_many through belongs_to" do 49 | s0 50 | assert_wa(0, :m2b1) 51 | end 52 | 53 | it "doesn't find with a non matching has_many through belongs_to" do 54 | b1 = s0.create_assoc!(:b1, :S0_b1) 55 | b1.create_bad_assocs!(:m2, :S0_m2b1, :S1_m2) 56 | 57 | assert_wa(0, :m2b1) 58 | end 59 | 60 | it "finds a matching has_many through belongs_to using an array for the association" do 61 | b1 = s0.create_assoc!(:b1, :S0_b1) 62 | b1.create_assoc!(:m2, :S1_m2) 63 | b1.create_assoc!(:m2, :S1_m2) 64 | 65 | assert_wa(2, [:b1, :m2]) 66 | end 67 | 68 | it "doesn't find without any has_many through belongs_to using an array for the association" do 69 | s0 70 | assert_wa(0, [:b1, :m2]) 71 | end 72 | 73 | it "doesn't find with a non matching has_many through belongs_to using an array for the association" do 74 | b1 = s0.create_assoc!(:b1, :S0_b1) 75 | b1.create_bad_assocs!(:m2, :S1_m2) 76 | 77 | assert_wa(0, [:b1, :m2]) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_has_and_belongs_to_many_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "finds the right matching has_and_belongs_to_manys" do 9 | s0_1 = s0 10 | s0_1.create_assoc!(:z1, :S0_z1) 11 | 12 | _s0_2 = S0.create_default! 13 | 14 | s0_3 = S0.create_default! 15 | s0_3.create_assoc!(:z1, :S0_z1) 16 | 17 | _s0_4 = S0.create_default! 18 | 19 | assert_equal [s0_1, s0_3], S0.where_assoc_count(1, :==, :z1).to_a.sort_by(&:id) 20 | end 21 | 22 | it "finds a matching has_and_belongs_to_many" do 23 | s0.create_assoc!(:z1, :S0_z1) 24 | s0.create_assoc!(:z1, :S0_z1) 25 | 26 | assert_wa(2, :z1) 27 | end 28 | 29 | it "doesn't find without any matching has_and_belongs_to_many" do 30 | s0 31 | assert_wa(0, :z1) 32 | end 33 | 34 | it "doesn't find with a non matching has_and_belongs_to_many" do 35 | s0.create_bad_assocs!(:z1, :S0_z1) 36 | 37 | assert_wa(0, :z1) 38 | end 39 | 40 | it "finds a matching has_many through has_and_belongs_to_many" do 41 | z1 = s0.create_assoc!(:z1, :S0_z1) 42 | z1.create_assoc!(:m2, :S0_m2z1, :S1_m2) 43 | z1.create_assoc!(:m2, :S0_m2z1, :S1_m2) 44 | 45 | assert_wa(2, :m2z1) 46 | end 47 | 48 | it "doesn't find without any has_many through has_and_belongs_to_many" do 49 | s0 50 | assert_wa(0, :m2z1) 51 | end 52 | 53 | it "doesn't find with a non matching has_many through has_and_belongs_to_many" do 54 | z1 = s0.create_assoc!(:z1, :S0_z1) 55 | z1.create_bad_assocs!(:m2, :S0_m2z1, :S1_m2) 56 | 57 | assert_wa(0, :m2z1) 58 | end 59 | 60 | it "finds a matching has_many through has_and_belongs_to_many using an array for the association" do 61 | z1 = s0.create_assoc!(:z1, :S0_z1) 62 | z1.create_assoc!(:m2, :S1_m2) 63 | z1.create_assoc!(:m2, :S1_m2) 64 | 65 | assert_wa(2, [:z1, :m2]) 66 | end 67 | 68 | it "doesn't find without any has_many through has_and_belongs_to_many using an array for the association" do 69 | s0 70 | assert_wa(0, [:z1, :m2]) 71 | end 72 | 73 | it "doesn't find with a non matching has_many through has_and_belongs_to_many using an array for the association" do 74 | z1 = s0.create_assoc!(:z1, :S0_z1) 75 | z1.create_bad_assocs!(:m2, :S1_m2) 76 | 77 | assert_wa(0, [:z1, :m2]) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_has_many_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "finds the right matching has_manys" do 9 | s0_1 = s0 10 | s0_1.create_assoc!(:m1, :S0_m1) 11 | 12 | _s0_2 = S0.create_default! 13 | 14 | s0_3 = S0.create_default! 15 | s0_3.create_assoc!(:m1, :S0_m1) 16 | 17 | _s0_4 = S0.create_default! 18 | 19 | assert_equal [s0_1, s0_3], S0.where_assoc_count(1, :==, :m1).to_a.sort_by(&:id) 20 | end 21 | 22 | it "finds a matching has_many" do 23 | s0.create_assoc!(:m1, :S0_m1) 24 | s0.create_assoc!(:m1, :S0_m1) 25 | 26 | assert_wa(2, :m1) 27 | end 28 | 29 | it "doesn't find without any has_many" do 30 | s0 31 | assert_wa(0, :m1) 32 | end 33 | 34 | it "doesn't find with a non matching has_many" do 35 | s0.create_bad_assocs!(:m1, :S0_m1) 36 | 37 | assert_wa(0, :m1) 38 | end 39 | 40 | it "finds a matching has_many through has_many" do 41 | m1 = s0.create_assoc!(:m1, :S0_m1) 42 | m1.create_assoc!(:m2, :S0_m2m1, :S1_m2) 43 | m1.create_assoc!(:m2, :S0_m2m1, :S1_m2) 44 | 45 | assert_wa(2, :m2m1) 46 | end 47 | 48 | it "doesn't find without any has_many through has_many" do 49 | s0 50 | assert_wa(0, :m2m1) 51 | end 52 | 53 | it "doesn't find with a non matching has_many through has_many" do 54 | m1 = s0.create_assoc!(:m1, :S0_m1) 55 | m1.create_bad_assocs!(:m2, :S0_m2m1, :S1_m2) 56 | 57 | assert_wa(0, :m2m1) 58 | end 59 | 60 | it "finds a matching has_many through has_many using an array for the association" do 61 | m1 = s0.create_assoc!(:m1, :S0_m1) 62 | m1.create_assoc!(:m2, :S1_m2) 63 | m1.create_assoc!(:m2, :S1_m2) 64 | 65 | assert_wa(2, [:m1, :m2]) 66 | end 67 | 68 | it "doesn't find without any has_many through has_many using an array for the association" do 69 | s0 70 | assert_wa(0, [:m1, :m2]) 71 | end 72 | 73 | it "doesn't find with a non matching has_many through has_many using an array for the association" do 74 | m1 = s0.create_assoc!(:m1, :S0_m1) 75 | m1.create_bad_assocs!(:m2, :S1_m2) 76 | 77 | assert_wa(0, [:m1, :m2]) 78 | end 79 | 80 | it "finds a matching has_many through has_many through has_many" do 81 | m1 = s0.create_assoc!(:m1, :S0_m1) 82 | m2 = m1.create_assoc!(:m2, :S0_m2m1, :S1_m2) 83 | m2.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 84 | m2.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 85 | 86 | assert_wa(2, :m3m2m1) 87 | end 88 | 89 | it "doesn't find without any has_many through has_many through has_many" do 90 | s0 91 | assert_wa(0, :m3m2m1) 92 | end 93 | 94 | it "doesn't find with a non matching has_many through has_many through has_many" do 95 | m1 = s0.create_assoc!(:m1, :S0_m1) 96 | m2 = m1.create_assoc!(:m2, :S0_m2m1, :S1_m2) 97 | m2.create_bad_assocs!(:m3, :S0_m3m2m1, :S2_m3) 98 | 99 | assert_wa(0, :m3m2m1) 100 | end 101 | 102 | it "finds a matching has_many through a has_many with a source that is a has_many through" do 103 | m1 = s0.create_assoc!(:m1, :S0_m1) 104 | m2 = m1.create_assoc!(:m2, :S1_m2) 105 | m2.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 106 | m2.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 107 | 108 | assert_wa(2, :m3m1_m3m2) 109 | end 110 | 111 | it "doesn't find without any has_many through a has_many with a source that is a has_many through" do 112 | s0 113 | assert_wa(0, :m3m1_m3m2) 114 | end 115 | 116 | it "doesn't find with a non matching has_many through a has_many with a source that is a has_many through" do 117 | m1 = s0.create_assoc!(:m1, :S0_m1) 118 | m2 = m1.create_assoc!(:m2, :S1_m2) 119 | m2.create_bad_assocs!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 120 | 121 | assert_wa(0, :m3m1_m3m2) 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_has_one_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | # MySQL doesn't support has_one 7 | next if Test::SelectedDBHelper == Test::MySQL 8 | 9 | let(:s0) { S0.create_default! } 10 | 11 | it "finds the right matching has_ones" do 12 | s0_1 = s0 13 | s0_1.create_assoc!(:o1, :S0_o1) 14 | 15 | _s0_2 = S0.create_default! 16 | 17 | s0_3 = S0.create_default! 18 | s0_3.create_assoc!(:o1, :S0_o1) 19 | 20 | _s0_4 = S0.create_default! 21 | 22 | assert_equal [s0_1, s0_3], S0.where_assoc_count(1, :==, :o1).to_a.sort_by(&:id) 23 | end 24 | 25 | it "finds a matching has_one" do 26 | s0.create_assoc!(:o1, :S0_o1) 27 | s0.create_assoc!(:o1, :S0_o1) 28 | 29 | assert_wa(1, :o1) 30 | end 31 | 32 | it "doesn't find without any has_one" do 33 | s0 34 | assert_wa(0, :o1) 35 | end 36 | 37 | it "doesn't find with a non matching has_one" do 38 | s0.create_bad_assocs!(:o1, :S0_o1) 39 | 40 | assert_wa(0, :o1) 41 | end 42 | 43 | it "finds a matching has_one through has_one" do 44 | o1 = s0.create_assoc!(:o1, :S0_o1) 45 | o1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 46 | o1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 47 | 48 | assert_wa(1, :o2o1) 49 | end 50 | 51 | it "doesn't find without any has_one through has_one" do 52 | s0 53 | assert_wa(0, :o2o1) 54 | end 55 | 56 | it "doesn't find with a non matching has_one through has_one" do 57 | o1 = s0.create_assoc!(:o1, :S0_o1) 58 | o1.create_bad_assocs!(:o2, :S0_o2o1, :S1_o2) 59 | 60 | assert_wa(0, :o2o1) 61 | end 62 | 63 | it "finds a matching has_one through has_one using an array for the association" do 64 | o1 = s0.create_assoc!(:o1, :S0_o1) 65 | o1.create_assoc!(:o2, :S1_o2) 66 | o1.create_assoc!(:o2, :S1_o2) 67 | 68 | assert_wa(1, [:o1, :o2]) 69 | end 70 | 71 | it "doesn't find without any has_one through has_one using an array for the association" do 72 | s0 73 | assert_wa(0, [:o1, :o2]) 74 | end 75 | 76 | it "doesn't find with a non matching has_one through has_one using an array for the association" do 77 | o1 = s0.create_assoc!(:o1, :S0_o1) 78 | o1.create_bad_assocs!(:o2, :S1_o2) 79 | 80 | assert_wa(0, [:o1, :o2]) 81 | end 82 | 83 | it "finds a matching has_one through has_one through has_one" do 84 | o1 = s0.create_assoc!(:o1, :S0_o1) 85 | o2 = o1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 86 | o2.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 87 | o2.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 88 | 89 | assert_wa(1, :o3o2o1) 90 | end 91 | 92 | it "doesn't find without any has_one through has_one through has_one" do 93 | s0 94 | assert_wa(0, :o3o2o1) 95 | end 96 | 97 | it "doesn't find with a non matching has_one through has_one through has_one" do 98 | o1 = s0.create_assoc!(:o1, :S0_o1) 99 | o2 = o1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 100 | o2.create_bad_assocs!(:o3, :S0_o3o2o1, :S2_o3) 101 | 102 | assert_wa(0, :o3o2o1) 103 | end 104 | 105 | it "finds a matching has_one through a has_one with a source that is a has_one through" do 106 | o1 = s0.create_assoc!(:o1, :S0_o1) 107 | o2 = o1.create_assoc!(:o2, :S1_o2) 108 | o2.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 109 | o2.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 110 | 111 | assert_wa(1, :o3o1_o3o2) 112 | end 113 | 114 | it "doesn't find without any has_one through a has_one with a source that is a has_one through" do 115 | s0 116 | assert_wa(0, :o3o1_o3o2) 117 | end 118 | 119 | it "doesn't find with a non matching has_one through a has_one with a source that is a has_one through" do 120 | o1 = s0.create_assoc!(:o1, :S0_o1) 121 | o2 = o1.create_assoc!(:o2, :S1_o2) 122 | o2.create_bad_assocs!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 123 | 124 | assert_wa(0, :o3o1_o3o2) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_polymorphic_has_many_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "finds the right matching poly has_manys" do 9 | s0_1 = s0 10 | s0_1.create_assoc!(:mp1, :S0_mp1) 11 | 12 | _s0_2 = S0.create_default! 13 | 14 | s0_3 = S0.create_default! 15 | s0_3.create_assoc!(:mp1, :S0_mp1) 16 | 17 | _s0_4 = S0.create_default! 18 | 19 | assert_equal [s0_1, s0_3], S0.where_assoc_count(1, :==, :mp1).to_a.sort_by(&:id) 20 | end 21 | 22 | it "finds a matching poly has_many" do 23 | s0.create_assoc!(:mp1, :S0_mp1) 24 | s0.create_assoc!(:mp1, :S0_mp1) 25 | 26 | assert_wa(2, :mp1) 27 | end 28 | 29 | it "doesn't find without any poly has_many" do 30 | s0 31 | assert_wa(0, :mp1) 32 | end 33 | 34 | it "doesn't find with a non matching poly has_many" do 35 | s0.create_bad_assocs!(:mp1, :S0_mp1) 36 | 37 | assert_wa(0, :mp1) 38 | end 39 | 40 | it "finds a matching poly has_many through poly has_many" do 41 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 42 | mp1.create_assoc!(:mp2, :S0_mp2mp1, :S1_mp2) 43 | mp1.create_assoc!(:mp2, :S0_mp2mp1, :S1_mp2) 44 | 45 | assert_wa(2, :mp2mp1) 46 | end 47 | 48 | it "doesn't find without any poly has_many through poly has_many" do 49 | s0 50 | assert_wa(0, :mp2mp1) 51 | end 52 | 53 | it "doesn't find with a non matching poly has_many through poly has_many" do 54 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 55 | mp1.create_bad_assocs!(:mp2, :S0_mp2mp1, :S1_mp2) 56 | 57 | assert_wa(0, :mp2mp1) 58 | end 59 | 60 | it "finds a matching poly has_many through poly has_many using an array for the association" do 61 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 62 | mp1.create_assoc!(:mp2, :S1_mp2) 63 | mp1.create_assoc!(:mp2, :S1_mp2) 64 | 65 | assert_wa(2, [:mp1, :mp2]) 66 | end 67 | 68 | it "doesn't find without any poly has_many through poly has_many using an array for the association" do 69 | s0 70 | assert_wa(0, [:mp1, :mp2]) 71 | end 72 | 73 | it "doesn't find with a non matching poly has_many through poly has_many using an array for the association" do 74 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 75 | mp1.create_bad_assocs!(:mp2, :S1_mp2) 76 | 77 | assert_wa(0, [:mp1, :mp2]) 78 | end 79 | 80 | it "finds a matching poly has_many through poly has_many through poly has_many" do 81 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 82 | mp2 = mp1.create_assoc!(:mp2, :S0_mp2mp1, :S1_mp2) 83 | mp2.create_assoc!(:mp3, :S0_mp3mp2mp1, :S2_mp3) 84 | mp2.create_assoc!(:mp3, :S0_mp3mp2mp1, :S2_mp3) 85 | 86 | assert_wa(2, :mp3mp2mp1) 87 | end 88 | 89 | it "doesn't find without any poly has_many through poly has_many through poly has_many" do 90 | s0 91 | assert_wa(0, :mp3mp2mp1) 92 | end 93 | 94 | it "doesn't find with a non matching poly has_many through poly has_many through poly has_many" do 95 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 96 | mp2 = mp1.create_assoc!(:mp2, :S0_mp2mp1, :S1_mp2) 97 | mp2.create_bad_assocs!(:mp3, :S0_mp3mp2mp1, :S2_mp3) 98 | 99 | assert_wa(0, :mp3mp2mp1) 100 | end 101 | 102 | it "finds a matching poly has_many through a poly has_many with a source that is a poly has_many through" do 103 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 104 | mp2 = mp1.create_assoc!(:mp2, :S1_mp2) 105 | mp2.create_assoc!(:mp3, :S0_mp3mp1_mp3mp2, :S1_mp3mp2, :S2_mp3) 106 | mp2.create_assoc!(:mp3, :S0_mp3mp1_mp3mp2, :S1_mp3mp2, :S2_mp3) 107 | 108 | assert_wa(2, :mp3mp1_mp3mp2) 109 | end 110 | 111 | it "doesn't find without any poly has_many through a poly has_many with a source that is a poly has_many through" do 112 | s0 113 | assert_wa(0, :mp3mp1_mp3mp2) 114 | end 115 | 116 | it "doesn't find with a non matching poly has_many through a poly has_many with a source that is a poly has_many through" do 117 | mp1 = s0.create_assoc!(:mp1, :S0_mp1) 118 | mp2 = mp1.create_assoc!(:mp2, :S1_mp2) 119 | mp2.create_bad_assocs!(:mp3, :S0_mp3mp1_mp3mp2, :S1_mp3mp2, :S2_mp3) 120 | 121 | assert_wa(0, :mp3mp1_mp3mp2) 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_polymorphic_has_one_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | # MySQL doesn't support has_one 7 | next if Test::SelectedDBHelper == Test::MySQL 8 | 9 | let(:s0) { S0.create_default! } 10 | 11 | it "finds the right matching poly has_ones" do 12 | s0_1 = s0 13 | s0_1.create_assoc!(:op1, :S0_op1) 14 | 15 | _s0_2 = S0.create_default! 16 | 17 | s0_3 = S0.create_default! 18 | s0_3.create_assoc!(:op1, :S0_op1) 19 | 20 | _s0_4 = S0.create_default! 21 | 22 | assert_equal [s0_1, s0_3], S0.where_assoc_count(1, :==, :op1).to_a.sort_by(&:id) 23 | end 24 | 25 | it "finds a matching poly has_one" do 26 | s0.create_assoc!(:op1, :S0_op1) 27 | s0.create_assoc!(:op1, :S0_op1) 28 | 29 | assert_wa(1, :op1) 30 | end 31 | 32 | it "doesn't find without any poly has_one" do 33 | s0 34 | assert_wa(0, :op1) 35 | end 36 | 37 | it "doesn't find with a non matching poly has_one" do 38 | s0.create_bad_assocs!(:op1, :S0_op1) 39 | 40 | assert_wa(0, :op1) 41 | end 42 | 43 | it "finds a matching poly has_one through poly has_one" do 44 | op1 = s0.create_assoc!(:op1, :S0_op1) 45 | op1.create_assoc!(:op2, :S0_op2op1, :S1_op2) 46 | op1.create_assoc!(:op2, :S0_op2op1, :S1_op2) 47 | 48 | assert_wa(1, :op2op1) 49 | end 50 | 51 | it "doesn't find without any poly has_one through poly has_one" do 52 | s0 53 | assert_wa(0, :op2op1) 54 | end 55 | 56 | it "doesn't find with a non matching poly has_one through poly has_one" do 57 | op1 = s0.create_assoc!(:op1, :S0_op1) 58 | op1.create_bad_assocs!(:op2, :S0_op2op1, :S1_op2) 59 | 60 | assert_wa(0, :op2op1) 61 | end 62 | 63 | it "finds a matching poly has_one through poly has_one using an array for the association" do 64 | op1 = s0.create_assoc!(:op1, :S0_op1) 65 | op1.create_assoc!(:op2, :S1_op2) 66 | op1.create_assoc!(:op2, :S1_op2) 67 | 68 | assert_wa(1, [:op1, :op2]) 69 | end 70 | 71 | it "doesn't find without any poly has_one through poly has_one using an array for the association" do 72 | s0 73 | assert_wa(0, [:op1, :op2]) 74 | end 75 | 76 | it "doesn't find with a non matching poly has_one through poly has_one using an array for the association" do 77 | op1 = s0.create_assoc!(:op1, :S0_op1) 78 | op1.create_bad_assocs!(:op2, :S1_op2) 79 | 80 | assert_wa(0, [:op1, :op2]) 81 | end 82 | 83 | it "finds a matching poly has_one through poly has_one through poly has_one" do 84 | op1 = s0.create_assoc!(:op1, :S0_op1) 85 | op2 = op1.create_assoc!(:op2, :S0_op2op1, :S1_op2) 86 | op2.create_assoc!(:op3, :S0_op3op2op1, :S2_op3) 87 | op2.create_assoc!(:op3, :S0_op3op2op1, :S2_op3) 88 | 89 | assert_wa(1, :op3op2op1) 90 | end 91 | 92 | it "doesn't find without any poly has_one through poly has_one through poly has_one" do 93 | s0 94 | assert_wa(0, :op3op2op1) 95 | end 96 | 97 | it "doesn't find with a non matching poly has_one through poly has_one through poly has_one" do 98 | op1 = s0.create_assoc!(:op1, :S0_op1) 99 | op2 = op1.create_assoc!(:op2, :S0_op2op1, :S1_op2) 100 | op2.create_bad_assocs!(:op3, :S0_op3op2op1, :S2_op3) 101 | 102 | assert_wa(0, :op3op2op1) 103 | end 104 | 105 | it "finds a matching poly has_one through a poly has_one with a source that is a poly has_one through" do 106 | op1 = s0.create_assoc!(:op1, :S0_op1) 107 | op2 = op1.create_assoc!(:op2, :S1_op2) 108 | op2.create_assoc!(:op3, :S0_op3op1_op3op2, :S1_op3op2, :S2_op3) 109 | op2.create_assoc!(:op3, :S0_op3op1_op3op2, :S1_op3op2, :S2_op3) 110 | 111 | assert_wa(1, :op3op1_op3op2) 112 | end 113 | 114 | it "doesn't find without any poly has_one through a poly has_one with a source that is a poly has_one through" do 115 | s0 116 | assert_wa(0, :op3op1_op3op2) 117 | end 118 | 119 | it "doesn't find with a non matching poly has_one through a poly has_one with a source that is a poly has_one through" do 120 | op1 = s0.create_assoc!(:op1, :S0_op1) 121 | op2 = op1.create_assoc!(:op2, :S1_op2) 122 | op2.create_bad_assocs!(:op3, :S0_op3op1_op3op2, :S1_op3op2, :S2_op3) 123 | 124 | assert_wa(0, :op3op1_op3op2) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/tests/scoping/wa_with_no_possible_records_to_return_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create_default! } 7 | 8 | def check_association(association, &block) 9 | # With nothing at all 10 | assert !S0.where_assoc_count(1, :==, association).exists?, caller(0) 11 | assert !S0.where_assoc_count(1, :!=, association).exists? 12 | assert !S0.where_assoc_exists(association).exists? 13 | assert !S0.where_assoc_not_exists(association).exists? 14 | 15 | # With a record of the association 16 | assoc_record = S1.create_default!("S0_#{association}") 17 | assert !S0.where_assoc_count(1, :==, association).exists? 18 | assert !S0.where_assoc_count(1, :!=, association).exists? 19 | assert !S0.where_assoc_exists(association).exists? 20 | assert !S0.where_assoc_not_exists(association).exists? 21 | 22 | # Also with a non-matching record in the source model 23 | s0 24 | assert_wa(0, association) 25 | 26 | # The block is to make sure that th scoping is done correctly. It must fix things up so 27 | # that there is now a match 28 | yield assoc_record 29 | 30 | assert_wa(1, association) 31 | rescue Minitest::Assertion 32 | # Adding more of the backtrace to the message to make it easier to know where things failed. 33 | raise $!, "#{$!}\n#{Minitest.filter_backtrace($!.backtrace).join("\n")}", $!.backtrace 34 | end 35 | 36 | it "always returns no result for belongs_to if no possible ones exists" do 37 | check_association(:b1) do |b1| 38 | s0.update!(s1_id: b1.id) 39 | end 40 | end 41 | 42 | it "always returns no result for has_and_belongs_to_many if no possible ones exists" do 43 | check_association(:z1) do |z1| 44 | s0.z1 << z1 45 | end 46 | end 47 | 48 | it "always returns no result for has_many if no possible ones exists" do 49 | check_association(:m1) do |m1| 50 | m1.update!(s0_id: s0.id) 51 | end 52 | end 53 | 54 | it "always returns no result for has_one if no possible ones exists" do 55 | skip if Test::SelectedDBHelper == Test::MySQL 56 | check_association(:o1) do |o1| 57 | o1.update!(s0_id: s0.id) 58 | end 59 | end 60 | 61 | it "always returns no result for polymorphic has_many if no possible ones exists" do 62 | check_association(:mp1) do |mp1| 63 | mp1.update!(has_s1s_poly_id: s0.id, has_s1s_poly_type: "S0") 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /test/tests/wa_abstract_model_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { STIS0.create! } 7 | 8 | it "belongs_to on abstract model works on its descendant" do 9 | a = UnabstractModel.create! 10 | 11 | a.create_b1!(never_abstracted_models_column: 42) 12 | a.save! 13 | assert_wa_from(UnabstractModel, 1, :b1, never_abstracted_models_column: 42) 14 | assert_wa_from(UnabstractModel, 0, :b1, never_abstracted_models_column: 43) 15 | end 16 | 17 | it "has_one on abstract model works on its descendant" do 18 | skip if Test::SelectedDBHelper == Test::MySQL 19 | 20 | a = UnabstractModel.create! 21 | 22 | a.create_o1!(never_abstracted_models_column: 42) 23 | 24 | assert_wa_from(UnabstractModel, 1, :o1, never_abstracted_models_column: 42) 25 | assert_wa_from(UnabstractModel, 0, :o1, never_abstracted_models_column: 43) 26 | end 27 | 28 | it "has_many on abstract model works on its descendant" do 29 | a = UnabstractModel.create! 30 | 31 | a.m1.create!(never_abstracted_models_column: 42) 32 | a.m1.create!(never_abstracted_models_column: 42) 33 | 34 | assert_wa_from(UnabstractModel, 2, :m1, never_abstracted_models_column: 42) 35 | assert_wa_from(UnabstractModel, 0, :m1, never_abstracted_models_column: 43) 36 | end 37 | 38 | 39 | it "polymorphic has_many on abstract model works on its descendant" do 40 | a = UnabstractModel.create! 41 | 42 | a.mp1.create!(never_abstracted_models_column: 42) 43 | a.mp1.create!(never_abstracted_models_column: 42) 44 | 45 | assert_wa_from(UnabstractModel, 2, :mp1, never_abstracted_models_column: 42) 46 | assert_wa_from(UnabstractModel, 0, :mp1, never_abstracted_models_column: 43) 47 | end 48 | 49 | it "polymorphic has_one on abstract model works on its descendant" do 50 | skip if Test::SelectedDBHelper == Test::MySQL 51 | 52 | a = UnabstractModel.create! 53 | 54 | a.create_has_one!(:op1, never_abstracted_models_column: 42) 55 | a.create_has_one!(:op1, never_abstracted_models_column: 42) 56 | 57 | assert_wa_from(UnabstractModel, 1, :op1, never_abstracted_models_column: 42) 58 | assert_wa_from(UnabstractModel, 0, :op1, never_abstracted_models_column: 43) 59 | end 60 | 61 | it "polymorphic belongs_to on abstract model works on its descendant with poly_belongs_to: :pluck" do 62 | a = UnabstractModel.create! 63 | b = NeverAbstractedModel.create!(never_abstracted_models_column: 42) 64 | a.bp1 = b 65 | a.save! 66 | 67 | assert_wa_from(UnabstractModel, 1, :bp1, {never_abstracted_models_column: 42}, poly_belongs_to: :pluck) 68 | assert_wa_from(UnabstractModel, 0, :bp1, {never_abstracted_models_column: 43}, poly_belongs_to: :pluck) 69 | end 70 | 71 | it "polymorphic belongs_to on abstract model works on its descendant with poly_belongs_to: Class" do 72 | a = UnabstractModel.create! 73 | b = NeverAbstractedModel.create!(never_abstracted_models_column: 42) 74 | a.bp1 = b 75 | a.save! 76 | 77 | assert_wa_from(UnabstractModel, 1, :bp1, {never_abstracted_models_column: 42}, poly_belongs_to: NeverAbstractedModel) 78 | assert_wa_from(UnabstractModel, 0, :bp1, {never_abstracted_models_column: 43}, poly_belongs_to: NeverAbstractedModel) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/tests/wa_composite_keys_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | next if ActiveRecord.gem_version < Gem::Version.new("7.2") 7 | 8 | it "belongs_to with composite_key works" do 9 | ck1 = Ck1.create_default!(an_id1: 0, a_str1: "hi") 10 | ck1.create_assoc!(:b0, :Ck1_b0, attributes: {an_id0: 1, a_str0: "bar"}) 11 | ck0_spam = ck1.create_assoc!(:b0, :Ck1_b0, attributes: {an_id0: 1, a_str0: "spam"}) 12 | ck1.save! # Save the updates ids 13 | 14 | assert_wa_from(Ck1, 1, :b0) 15 | 16 | ck1_1 = Ck1.create_default!(an_id1: 1, a_str1: "foo") 17 | ck1_2 = Ck1.create_default!(an_id1: 12, a_str1: "foo") 18 | 19 | assert_equal [ck1], Ck1.where_assoc_count(1, :==, :b0).to_a.sort_by(&:an_id1) 20 | assert_equal [ck1_1, ck1_2], Ck1.where_assoc_count(0, :==, :b0).to_a.sort_by(&:an_id1) 21 | end 22 | 23 | it "has_many with composite_key works" do 24 | ck0 = Ck0.create_default!(an_id0: 1, a_str0: "foo") 25 | ck0.create_assoc!(:m1, :Ck0_m1, attributes: {an_id1: 1, a_str1: "bar"}) 26 | ck0.create_assoc!(:m1, :Ck0_m1, attributes: {an_id1: 1, a_str1: "spam"}) 27 | ck0.create_assoc!(:m1, :Ck0_m1, attributes: {an_id1: 2, a_str1: "bar"}) 28 | 29 | Ck1.create_default!(an_id1: 42, a_str1: "foo") 30 | 31 | assert_wa_from(Ck0, 3, :m1) 32 | end 33 | 34 | it "has_one with composite_key works" do 35 | skip if Test::SelectedDBHelper == Test::MySQL 36 | 37 | ck0 = Ck0.create_default!(an_id0: 1, a_str0: "foo") 38 | ck0.create_assoc!(:o1, :Ck0_o1, attributes: {an_id1: 1, a_str1: "bar"}) 39 | ck0.create_assoc!(:o1, :Ck0_o1, attributes: {an_id1: 1, a_str1: "spam"}) 40 | ck0.create_assoc!(:o1, :Ck0_o1, attributes: {an_id1: 2, a_str1: "bar"}) 41 | 42 | assert_wa_from(Ck0, 1, :o1) 43 | end 44 | 45 | # I don't think has_and_belongs_to_many supports composite keys? 46 | # it "has_and_belongs_to_many with composite_key works" do 47 | # ck0 = Ck0.create_default!(an_id0: 1, a_str0: "foo") 48 | # ck0.create_assoc!(:z1, :Ck0_z1, attributes: {an_id1: 1, a_str1: "bar"}) 49 | # ck0.create_assoc!(:z1, :Ck0_z1, attributes: {an_id1: 1, a_str1: "spam"}) 50 | # ck0.create_assoc!(:z1, :Ck0_z1, attributes: {an_id1: 2, a_str1: "bar"}) 51 | 52 | # assert_wa_from(Ck0, 3, :z1) 53 | # end 54 | 55 | 56 | it "has_many through has_many with composite_key works" do 57 | ck0 = Ck0.create_default!(an_id0: 1, a_str0: "foo") 58 | ck0.create_assoc!(:m1, :Ck0_m1, attributes: {an_id1: 1, a_str1: "bar"}) 59 | ck1_2 = ck0.create_assoc!(:m1, :Ck0_m1, attributes: {an_id1: 1, a_str1: "spam"}) 60 | ck0.create_assoc!(:m1, :Ck0_m1, attributes: {an_id1: 2, a_str1: "bar"}) 61 | 62 | assert_wa_from(Ck0, 0, :m2m1) 63 | 64 | ck1_2.create_assoc!(:m2, :Ck1_m2, :Ck0_m2m1, attributes: {an_id2: 130, a_str2: "hello"}) 65 | assert_wa_from(Ck0, 1, :m2m1) 66 | end 67 | 68 | it "raise on composite_key with never_alias_limit" do 69 | skip if Test::SelectedDBHelper == Test::MySQL 70 | 71 | sql = Ck0.where_assoc_exists(:o1) { from("hello") }.to_sql 72 | assert !sql.include?("an_int0") 73 | 74 | assert_raises(ActiveRecordWhereAssoc::NeverAliasLimitDoesntWorkWithCompositePrimaryKeysError) { 75 | Ck0.where_assoc_exists(:o1, nil, never_alias_limit: true) { from("hello") }.to_sql 76 | } 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/tests/wa_count_has_many_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa_count" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "counts every matching has_many through has_many through has_many" do 9 | m1_1 = s0.create_assoc!(:m1, :S0_m1) 10 | m1_2 = s0.create_assoc!(:m1, :S0_m1) 11 | 12 | m2_11 = m1_1.create_assoc!(:m2, :S0_m2m1, :S1_m2) 13 | m2_12 = m1_1.create_assoc!(:m2, :S0_m2m1, :S1_m2) 14 | 15 | m2_21 = m1_2.create_assoc!(:m2, :S0_m2m1, :S1_m2) 16 | m2_22 = m1_2.create_assoc!(:m2, :S0_m2m1, :S1_m2) 17 | 18 | assert_wa_count(0, :m3m2m1) 19 | 20 | m2_11.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 21 | m2_11.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 22 | m2_12.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 23 | m2_12.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 24 | m2_21.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 25 | m2_21.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 26 | m2_22.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 27 | m2_22.create_assoc!(:m3, :S0_m3m2m1, :S2_m3) 28 | 29 | assert_wa_count(8, :m3m2m1) 30 | end 31 | 32 | it "counts every matching has_many through a has_many with a source that is a has_many through" do 33 | m1_1 = s0.create_assoc!(:m1, :S0_m1) 34 | m1_2 = s0.create_assoc!(:m1, :S0_m1) 35 | 36 | m2_11 = m1_1.create_assoc!(:m2, :S1_m2) 37 | m2_12 = m1_1.create_assoc!(:m2, :S1_m2) 38 | 39 | m2_21 = m1_2.create_assoc!(:m2, :S0_m2m1, :S1_m2) 40 | m2_22 = m1_2.create_assoc!(:m2, :S0_m2m1, :S1_m2) 41 | 42 | assert_wa_count(0, :m3m1_m3m2) 43 | 44 | m2_11.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 45 | m2_11.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 46 | m2_12.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 47 | m2_12.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 48 | m2_21.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 49 | m2_21.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 50 | m2_22.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 51 | m2_22.create_assoc!(:m3, :S0_m3m1_m3m2, :S1_m3m2, :S2_m3) 52 | 53 | assert_wa_count(8, :m3m1_m3m2) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/tests/wa_count_has_one_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa_count" do 6 | # MySQL doesn't support has_one 7 | next if Test::SelectedDBHelper == Test::MySQL 8 | 9 | let(:s0) { S0.create_default! } 10 | 11 | it "counts matching has_one through has_one through has_one as at most 1" do 12 | o1_1 = s0.create_assoc!(:o1, :S0_o1) 13 | o1_2 = s0.create_assoc!(:o1, :S0_o1) 14 | 15 | o2_11 = o1_1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 16 | o2_12 = o1_1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 17 | 18 | o2_21 = o1_2.create_assoc!(:o2, :S0_o2o1, :S1_o2) 19 | o2_22 = o1_2.create_assoc!(:o2, :S0_o2o1, :S1_o2) 20 | 21 | assert_wa_count(0, :o3o2o1) 22 | 23 | o2_11.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 24 | o2_11.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 25 | o2_12.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 26 | o2_12.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 27 | o2_21.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 28 | o2_21.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 29 | o2_22.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 30 | o2_22.create_assoc!(:o3, :S0_o3o2o1, :S2_o3) 31 | 32 | assert_wa_count(1, :o3o2o1) 33 | end 34 | 35 | it "counts matching has_one through a has_one with a source that is a has_one through as at most 1" do 36 | o1_1 = s0.create_assoc!(:o1, :S0_o1) 37 | o1_2 = s0.create_assoc!(:o1, :S0_o1) 38 | 39 | o2_11 = o1_1.create_assoc!(:o2, :S1_o2) 40 | o2_12 = o1_1.create_assoc!(:o2, :S1_o2) 41 | 42 | o2_21 = o1_2.create_assoc!(:o2, :S0_o2o1, :S1_o2) 43 | o2_22 = o1_2.create_assoc!(:o2, :S0_o2o1, :S1_o2) 44 | 45 | assert_wa_count(0, :o3o1_o3o2) 46 | 47 | o2_11.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 48 | o2_11.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 49 | o2_12.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 50 | o2_12.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 51 | o2_21.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 52 | o2_21.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 53 | o2_22.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 54 | o2_22.create_assoc!(:o3, :S0_o3o1_o3o2, :S1_o3o2, :S2_o3) 55 | 56 | assert_wa_count(1, :o3o1_o3o2) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/tests/wa_count_left_side_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa_count" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "compare to a column using a string on left_side with has_many" do 9 | s0.update(s0s_adhoc_column: 2) 10 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :m1).exists? 11 | 12 | s0.create_assoc!(:m1, :S0_m1) 13 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :m1).exists? 14 | 15 | s0.create_assoc!(:m1, :S0_m1) 16 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :m1).exists? 17 | 18 | s0.create_assoc!(:m1, :S0_m1) 19 | assert S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :m1).exists? 20 | 21 | s0.update(s0s_adhoc_column: 3) 22 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :m1).exists? 23 | end 24 | 25 | it "compare to a column using a string on left_side with has_and_belongs_to_many" do 26 | s0.update(s0s_adhoc_column: 2) 27 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :z1).exists? 28 | 29 | s0.create_assoc!(:z1, :S0_z1) 30 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :z1).exists? 31 | 32 | s0.create_assoc!(:z1, :S0_z1) 33 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :z1).exists? 34 | 35 | s0.create_assoc!(:z1, :S0_z1) 36 | assert S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :z1).exists? 37 | 38 | s0.update(s0s_adhoc_column: 3) 39 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :<, :z1).exists? 40 | end 41 | 42 | it "compare to a column using a string on left_side with has_one" do 43 | skip if Test::SelectedDBHelper == Test::MySQL 44 | 45 | s0.update(s0s_adhoc_column: 0) 46 | assert S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :o1).exists? 47 | 48 | o1 = s0.create_assoc!(:o1, :S0_o1) 49 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :o1).exists? 50 | 51 | o1.destroy 52 | assert S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :o1).exists? 53 | 54 | s0.update(s0s_adhoc_column: 1) 55 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :o1).exists? 56 | end 57 | 58 | it "compare to a column using a string on left_side with belongs_to" do 59 | s0.update(s0s_adhoc_column: 0) 60 | assert S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :b1).exists? 61 | 62 | b1 = s0.create_assoc!(:b1, :S0_b1) 63 | s0.save! 64 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :b1).exists? 65 | 66 | b1.destroy 67 | assert S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :b1).exists? 68 | 69 | s0.update(s0s_adhoc_column: 1) 70 | assert !S0.where_assoc_count("s0s.s0s_adhoc_column", :==, :b1).exists? 71 | end 72 | 73 | it "compare against a Range on left_side with has_many" do 74 | s0 75 | 76 | assert_assoc_count_in_range(0..10) 77 | assert_assoc_count_not_in_range(3..5) 78 | 79 | s0.create_assoc!(:m1, :S0_m1) 80 | assert_assoc_count_not_in_range(3..5) 81 | 82 | s0.create_assoc!(:m1, :S0_m1) 83 | s0.create_assoc!(:m1, :S0_m1) 84 | 85 | assert_assoc_count_in_range(3..5) 86 | 87 | s0.create_assoc!(:m1, :S0_m1) 88 | s0.create_assoc!(:m1, :S0_m1) 89 | 90 | assert_assoc_count_in_range(3..5) 91 | 92 | s0.create_assoc!(:m1, :S0_m1) 93 | 94 | assert_assoc_count_not_in_range(3..5) 95 | end 96 | 97 | it "compare against an exclusive Range on left_side with has_many" do 98 | s0 99 | assert_assoc_count_in_range(0...2) 100 | 101 | s0.create_assoc!(:m1, :S0_m1) 102 | 103 | assert_assoc_count_in_range(0...2) 104 | 105 | s0.create_assoc!(:m1, :S0_m1) 106 | 107 | assert_assoc_count_not_in_range(0...2) 108 | end 109 | 110 | it "compare against an exclusive float Range on left_side with has_many" do 111 | s0 112 | assert_assoc_count_in_range(-0.01...2.0) 113 | assert_assoc_count_in_range(0.0...1.99) 114 | assert_assoc_count_in_range(0.0...2.0) 115 | assert_assoc_count_in_range(0.0...2.01) 116 | assert_assoc_count_not_in_range(0.01...2.0) 117 | 118 | s0.create_assoc!(:m1, :S0_m1) 119 | 120 | assert_assoc_count_in_range(-0.01...2.0) 121 | assert_assoc_count_in_range(0.0...1.99) 122 | assert_assoc_count_in_range(0.0...2.0) 123 | assert_assoc_count_in_range(0.0...2.01) 124 | assert_assoc_count_in_range(0.01...2.0) 125 | 126 | s0.create_assoc!(:m1, :S0_m1) 127 | 128 | assert_assoc_count_not_in_range(-0.01...2.0) 129 | assert_assoc_count_not_in_range(0.0...1.99) 130 | assert_assoc_count_not_in_range(0.0...2.0) 131 | assert_assoc_count_in_range(0.0...2.01) 132 | assert_assoc_count_not_in_range(0.01...2.0) 133 | end 134 | 135 | infinite_range_right_values = [Float::INFINITY] 136 | infinite_range_right_values << nil if RUBY_VERSION >= "2.6.0" # Ruby 2.6's new `12..` syntax for infinite range puts a nil 137 | infinite_range_right_values.each do |infinite_range_value| 138 | it "compares against an infinite in Range's right side (#{infinite_range_value.inspect})" do 139 | s0 140 | 141 | assert_assoc_count_in_range(0..infinite_range_value) 142 | assert_assoc_count_in_range(0...infinite_range_value) 143 | assert_assoc_count_not_in_range(1..infinite_range_value) 144 | assert_assoc_count_not_in_range(1...infinite_range_value) 145 | 146 | s0.create_assoc!(:m1, :S0_m1) 147 | s0.create_assoc!(:m1, :S0_m1) 148 | 149 | assert_assoc_count_in_range(0..infinite_range_value) 150 | assert_assoc_count_in_range(0...infinite_range_value) 151 | assert_assoc_count_in_range(2..infinite_range_value) 152 | assert_assoc_count_in_range(2...infinite_range_value) 153 | assert_assoc_count_not_in_range(3..infinite_range_value) 154 | assert_assoc_count_not_in_range(3...infinite_range_value) 155 | end 156 | end 157 | 158 | it "compares against an infinite in Range's left side" do 159 | s0 160 | 161 | assert_assoc_count_not_in_range(-Float::INFINITY..-1) 162 | assert_assoc_count_not_in_range(-Float::INFINITY...0) 163 | assert_assoc_count_in_range(-Float::INFINITY..0) 164 | assert_assoc_count_in_range(-Float::INFINITY...1) 165 | 166 | s0.create_assoc!(:m1, :S0_m1) 167 | s0.create_assoc!(:m1, :S0_m1) 168 | 169 | assert_assoc_count_not_in_range(-Float::INFINITY..1) 170 | assert_assoc_count_not_in_range(-Float::INFINITY...2) 171 | assert_assoc_count_in_range(-Float::INFINITY..2) 172 | assert_assoc_count_in_range(-Float::INFINITY...3) 173 | end 174 | 175 | def assert_assoc_count_in_range(range) 176 | assert S0.where_assoc_count(range, :==, :m1).exists?, "(==) Should exist but doesn't" 177 | assert !S0.where_assoc_count(range, :!=, :m1).exists?, "(!=) Should not exist but does" 178 | end 179 | 180 | def assert_assoc_count_not_in_range(range) 181 | assert !S0.where_assoc_count(range, :==, :m1).exists?, "(==) Should not exist but does" 182 | assert S0.where_assoc_count(range, :!=, :m1).exists?, "(!=) Should exist but doesn't" 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /test/tests/wa_count_operators_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa_count" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "counts every matching has_many with every operators" do 9 | s0 10 | assert_wa_count_full(0, :m1) 11 | 12 | s0.create_assoc!(:m1, :S0_m1) 13 | assert_wa_count_full(1, :m1) 14 | 15 | s0.create_assoc!(:m1, :S0_m1) 16 | assert_wa_count_full(2, :m1) 17 | end 18 | 19 | it "counts every matching has_and_belongs_to_many with every operators" do 20 | s0 21 | assert_wa_count_full(0, :z1) 22 | 23 | s0.create_assoc!(:z1, :S0_z1) 24 | assert_wa_count_full(1, :z1) 25 | 26 | s0.create_assoc!(:z1, :S0_z1) 27 | assert_wa_count_full(2, :z1) 28 | end 29 | 30 | it "counts matching has_one as at most 1 with every operators" do 31 | skip if Test::SelectedDBHelper == Test::MySQL 32 | 33 | s0 34 | assert_wa_count_full(0, :o1) 35 | 36 | s0.create_assoc!(:o1, :S0_o1) 37 | assert_wa_count_full(1, :o1) 38 | 39 | s0.create_assoc!(:o1, :S0_o1) 40 | assert_wa_count_full(1, :o1) 41 | end 42 | 43 | it "counts matching belongs_to as at most 1 with every operators" do 44 | s0 45 | assert_wa_count_full(0, :b1) 46 | 47 | s0.create_assoc!(:b1, :S0_b1) 48 | assert_wa_count_full(1, :b1) 49 | 50 | s0.create_assoc!(:b1, :S0_b1) 51 | assert_wa_count_full(1, :b1) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/tests/wa_count_swapped_operands_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa_count" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "Properly reverses the operands and operators when a number is used as 3rd argument" do 9 | s0 10 | assert !S0.where_assoc_count(:m1, :==, 1).exists? 11 | assert !S0.where_assoc_count(:m1, "=", 1).exists? 12 | assert S0.where_assoc_count(:m1, :!=, 1).exists? 13 | assert S0.where_assoc_count(:m1, "<>", 1).exists? 14 | assert S0.where_assoc_count(:m1, :==, 0).exists? 15 | assert S0.where_assoc_count(:m1, "=", 0).exists? 16 | assert !S0.where_assoc_count(:m1, :!=, 0).exists? 17 | assert !S0.where_assoc_count(:m1, "<>", 0).exists? 18 | 19 | assert !S0.where_assoc_count(:m1, :>, 0).exists? 20 | assert S0.where_assoc_count(:m1, :>=, 0).exists? 21 | assert S0.where_assoc_count(:m1, :<, 1).exists? 22 | assert S0.where_assoc_count(:m1, :<=, 1).exists? 23 | 24 | s0.create_assoc!(:m1, :S0_m1) 25 | 26 | assert S0.where_assoc_count(:m1, :==, 1).exists? 27 | assert S0.where_assoc_count(:m1, "=", 1).exists? 28 | assert !S0.where_assoc_count(:m1, :!=, 1).exists? 29 | assert !S0.where_assoc_count(:m1, "<>", 1).exists? 30 | assert !S0.where_assoc_count(:m1, :==, 0).exists? 31 | assert !S0.where_assoc_count(:m1, "=", 0).exists? 32 | assert S0.where_assoc_count(:m1, :!=, 0).exists? 33 | assert S0.where_assoc_count(:m1, "<>", 0).exists? 34 | 35 | assert S0.where_assoc_count(:m1, :>, 0).exists? 36 | assert S0.where_assoc_count(:m1, :>=, 0).exists? 37 | assert !S0.where_assoc_count(:m1, :<, 1).exists? 38 | assert S0.where_assoc_count(:m1, :<=, 1).exists? 39 | 40 | s0.create_assoc!(:m1, :S0_m1) 41 | 42 | assert !S0.where_assoc_count(:m1, :==, 1).exists? 43 | assert !S0.where_assoc_count(:m1, "=", 1).exists? 44 | assert S0.where_assoc_count(:m1, :!=, 1).exists? 45 | assert S0.where_assoc_count(:m1, "<>", 1).exists? 46 | assert !S0.where_assoc_count(:m1, :==, 0).exists? 47 | assert !S0.where_assoc_count(:m1, "=", 0).exists? 48 | assert S0.where_assoc_count(:m1, :!=, 0).exists? 49 | assert S0.where_assoc_count(:m1, "<>", 0).exists? 50 | 51 | assert S0.where_assoc_count(:m1, :>, 0).exists? 52 | assert S0.where_assoc_count(:m1, :>=, 0).exists? 53 | assert !S0.where_assoc_count(:m1, :<, 1).exists? 54 | assert !S0.where_assoc_count(:m1, :<=, 1).exists? 55 | end 56 | 57 | it "Properly reverses the operands when a range is used as 3rd argument" do 58 | s0 59 | assert !S0.where_assoc_count(:m1, :==, 1..10).exists? 60 | assert !S0.where_assoc_count(:m1, "=", 1..10).exists? 61 | assert S0.where_assoc_count(:m1, :!=, 1..10).exists? 62 | assert S0.where_assoc_count(:m1, "<>", 1..10).exists? 63 | assert S0.where_assoc_count(:m1, :==, 0..10).exists? 64 | assert S0.where_assoc_count(:m1, "=", 0..10).exists? 65 | assert !S0.where_assoc_count(:m1, :!=, 0..10).exists? 66 | assert !S0.where_assoc_count(:m1, "<>", 0..10).exists? 67 | 68 | s0.create_assoc!(:m1, :S0_m1) 69 | 70 | assert S0.where_assoc_count(:m1, :==, 1..10).exists? 71 | assert S0.where_assoc_count(:m1, "=", 1..10).exists? 72 | assert !S0.where_assoc_count(:m1, :!=, 1..10).exists? 73 | assert !S0.where_assoc_count(:m1, "<>", 1..10).exists? 74 | assert S0.where_assoc_count(:m1, :==, 0..10).exists? 75 | assert S0.where_assoc_count(:m1, "=", 0..10).exists? 76 | assert !S0.where_assoc_count(:m1, :!=, 0..10).exists? 77 | assert !S0.where_assoc_count(:m1, "<>", 0..10).exists? 78 | 79 | 10.times { s0.create_assoc!(:m1, :S0_m1) } 80 | 81 | assert !S0.where_assoc_count(:m1, :==, 1..10).exists? 82 | assert !S0.where_assoc_count(:m1, "=", 1..10).exists? 83 | assert S0.where_assoc_count(:m1, :!=, 1..10).exists? 84 | assert S0.where_assoc_count(:m1, "<>", 1..10).exists? 85 | assert !S0.where_assoc_count(:m1, :==, 0..10).exists? 86 | assert !S0.where_assoc_count(:m1, "=", 0..10).exists? 87 | assert S0.where_assoc_count(:m1, :!=, 0..10).exists? 88 | assert S0.where_assoc_count(:m1, "<>", 0..10).exists? 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/tests/wa_exceptions_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create! } 7 | 8 | it "_exists raises ActiveRecord::AssociationNotFoundError if missing association" do 9 | assert_raises(ActiveRecord::AssociationNotFoundError) do 10 | S0.where_assoc_exists(:this_doesnt_exist) 11 | end 12 | end 13 | 14 | it "_exists raises MySQLDoesntSupportSubLimitError for has_one with MySQL" do 15 | skip if Test::SelectedDBHelper != Test::MySQL 16 | 17 | assert_raises(ActiveRecordWhereAssoc::MySQLDoesntSupportSubLimitError) do 18 | S0.where_assoc_exists(:o1, id: 1) 19 | end 20 | end 21 | 22 | it "_exists doesn't raise MySQLDoesntSupportSubLimitError for has_one with MySQL if option[:ignore_limit]" do 23 | skip if Test::SelectedDBHelper != Test::MySQL 24 | assert_nothing_raised do 25 | S0.where_assoc_exists(:o1, nil, ignore_limit: true) 26 | 27 | with_wa_default_options(ignore_limit: true) do 28 | S0.where_assoc_exists(:o1) 29 | end 30 | end 31 | end 32 | 33 | it "_exists raises MySQLDoesntSupportSubLimitError for has_many with limit with MySQL" do 34 | skip if Test::SelectedDBHelper != Test::MySQL 35 | 36 | assert_raises(ActiveRecordWhereAssoc::MySQLDoesntSupportSubLimitError) do 37 | LimOffOrdS0.where_assoc_exists(:ml1) 38 | end 39 | end 40 | 41 | it "_exists doesn't raise MySQLDoesntSupportSubLimitError for has_many with limit with MySQL if option[:ignore_limit]" do 42 | skip if Test::SelectedDBHelper != Test::MySQL 43 | 44 | assert_nothing_raised do 45 | LimOffOrdS0.where_assoc_exists(:ml1, nil, ignore_limit: true) 46 | 47 | with_wa_default_options(ignore_limit: true) do 48 | LimOffOrdS0.where_assoc_exists(:ml1) 49 | end 50 | end 51 | end 52 | 53 | it "_exists raises PolymorphicBelongsToWithoutClasses for polymorphic belongs_to without :poly_belongs_to option" do 54 | assert_raises(ActiveRecordWhereAssoc::PolymorphicBelongsToWithoutClasses) do 55 | S0.where_assoc_exists(:bp1) 56 | end 57 | end 58 | 59 | it "_exists raises PolymorphicBelongsToWithoutClasses for polymorphic belongs_to without :poly_belongs_to option" do 60 | assert_raises(ActiveRecordWhereAssoc::PolymorphicBelongsToWithoutClasses) do 61 | S0.where_assoc_exists(:mbp2mp1) 62 | end 63 | end 64 | 65 | it "_count refuses ranges with wrong operators" do 66 | %w(< > <= >=).each do |operator| 67 | exc = assert_raises(ArgumentError) do 68 | S0.where_assoc_count(0..10, operator, :m1) 69 | end 70 | 71 | assert_includes exc.message, operator 72 | end 73 | end 74 | 75 | it "_exists fails nicely if given a bad :poly_belongs_to" do 76 | assert_raises(ArgumentError) do 77 | S0.where_assoc_exists(:mbp2mp1, nil, poly_belongs_to: 123) 78 | end 79 | assert_raises(ArgumentError) do 80 | S0.where_assoc_exists(:mbp2mp1, nil, poly_belongs_to: [123]) 81 | end 82 | assert_raises(ArgumentError) do 83 | # There is a different error message to try to be helpful if someone does that 84 | S0.where_assoc_exists(:mbp2mp1, nil, poly_belongs_to: [S0.new]) 85 | end 86 | end 87 | 88 | it "_exists fails nicely if given a has_many :through a polymorphic belongs_to" do 89 | assert_raises(ActiveRecord::HasManyThroughAssociationPolymorphicThroughError) do 90 | S0.where_assoc_exists(:mp2bp1) 91 | end 92 | end 93 | 94 | it "_exists fails nicely if given a has_one :through a polymorphic belongs_to" do 95 | exc = if defined?(ActiveRecord::HasOneAssociationPolymorphicThroughError) 96 | ActiveRecord::HasOneAssociationPolymorphicThroughError 97 | else 98 | ActiveRecord::HasManyThroughAssociationPolymorphicThroughError 99 | end 100 | assert_raises(exc) do 101 | S0.where_assoc_exists(:op2bp1) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/tests/wa_has_one_exclusion_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | # Has_one associations should behave similarly to a belongs_to in that only 6 | # one record should be tested: the one that would be returned by using the 7 | # association on a record. This is the only record that must match (or 8 | # not match) the condition given to the where_assoc_* methods. 9 | # 10 | # All the has_one associations have a order('id DESC') for the tests, so a 11 | # record created later must shadow earlier ones, as long as it matches the 12 | # scopes on the associations and the default_scope of the record. 13 | 14 | describe "wa has_one" do 15 | # MySQL doesn't support has_one 16 | next if Test::SelectedDBHelper == Test::MySQL 17 | 18 | let(:s0) { S0.create_default! } 19 | 20 | it "only check against the last associated record" do 21 | s0.create_assoc!(:o1, :S0_o1, adhoc_value: 1) 22 | assert_wa(1, :o1, S1.adhoc_column_name => 1) 23 | 24 | s0.create_assoc!(:o1, :S0_o1) # Shadows the one with an adhoc_value 25 | assert_wa(0, :o1, S1.adhoc_column_name => 1) 26 | end 27 | 28 | it "only check against the last associated record when using a through association" do 29 | o1 = s0.create_assoc!(:o1, :S0_o1) 30 | o1.create_assoc!(:o2, :S0_o2o1, :S1_o2, adhoc_value: 1) 31 | assert_wa(1, :o2o1, S2.adhoc_column_name => 1) 32 | 33 | o1.create_assoc!(:o2, :S0_o2o1, :S1_o2) # Shadows the final association that would match 34 | assert_wa(0, :o2o1, S2.adhoc_column_name => 1) 35 | end 36 | 37 | it "only check against the last associated record when using an array for the association" do 38 | o1 = s0.create_assoc!(:o1, :S0_o1) 39 | o1.create_assoc!(:o2, :S1_o2, adhoc_value: 1) 40 | assert_wa(1, [:o1, :o2], S2.adhoc_column_name => 1) 41 | 42 | o1.create_assoc!(:o2, :S1_o2) # Shadows the final association that would match 43 | assert_wa(0, [:o1, :o2], S2.adhoc_column_name => 1) 44 | end 45 | 46 | it "only check against the last intermediary record when using a through association" do 47 | o1 = s0.create_assoc!(:o1, :S0_o1) 48 | o1.create_assoc!(:o2, :S0_o2o1, :S1_o2) 49 | assert_wa(1, :o2o1) 50 | 51 | s0.create_assoc!(:o1, :S0_o1) # Shadows the intermediary association that would match 52 | without_manual_wa_test do # ActiveRecord checks every possible match of o1 instead of only the last one... 53 | assert_wa(0, :o2o1) 54 | end 55 | end 56 | 57 | it "only check against the last intermediary record when using an array for the association" do 58 | o1 = s0.create_assoc!(:o1, :S0_o1) 59 | o1.create_assoc!(:o2, :S1_o2) 60 | assert_wa(1, [:o1, :o2]) 61 | 62 | s0.create_assoc!(:o1, :S0_o1) # Shadows the intermediary association that would match 63 | assert_wa(0, [:o1, :o2]) 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/tests/wa_last_equality_wins_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | # Last Equality Wins is a special behavior in rails since 4.0 6 | # https://github.com/rails/rails/issues/7365 7 | # 8 | # Basically, when using #merge, if the receiving relation has a hash 9 | # condition on an attribute and the passed relation also has one on 10 | # the same attribute with a different value, only the value of the 11 | # passed relation will be kept. 12 | # 13 | # In rails, this behavior is only applied in one situation: When an 14 | # association on a model has a scope and the target model has a 15 | # default_scope. In that situation, the association's scope wins. 16 | # 17 | # We need to make sure this happens in our system too. 18 | 19 | describe "wa" do 20 | let(:s0) { LEWS0.create! } 21 | 22 | it "finds a matching belongs_to (LEW) that has the value of the association's scope" do 23 | s0.create_b1!(lew_s1s_column: "belongs_to") 24 | # need to save the changed column 25 | s0.save! 26 | 27 | assert_wa_from(LEWS0, 1, :b1) 28 | end 29 | 30 | it "doesn't find a belongs_to (LEW) that has the value of the model's default_scope" do 31 | s0.create_b1(lew_s1s_column: "default_scope") 32 | # need to save the changed column 33 | s0.save! 34 | 35 | assert_wa_from(LEWS0, 0, :b1) 36 | end 37 | 38 | it "finds a matching has_and_belongs_to_many (LEW) that has the value of the association's scope" do 39 | s0.z1.create!(lew_s1s_column: "habtm") 40 | s0.z1.create!(lew_s1s_column: "habtm") 41 | s0.z1.create!(lew_s1s_column: "default_scope") 42 | s0.z1.create!(lew_s1s_column: "none") 43 | 44 | assert_wa_from(LEWS0, 2, :z1) 45 | end 46 | 47 | it "doesn't find a has_and_belongs_to_many (LEW) that has the value of the model's default_scope" do 48 | s0.z1.create!(lew_s1s_column: "default_scope") 49 | s0.z1.create!(lew_s1s_column: "none") 50 | 51 | assert_wa_from(LEWS0, 0, :z1) 52 | end 53 | 54 | it "finds a matching has_many (LEW) that has the value of the association's scope" do 55 | s0.m1.create!(lew_s1s_column: "has_many") 56 | s0.m1.create!(lew_s1s_column: "has_many") 57 | s0.m1.create!(lew_s1s_column: "default_scope") 58 | s0.m1.create!(lew_s1s_column: "none") 59 | 60 | assert_wa_from(LEWS0, 2, :m1) 61 | end 62 | 63 | it "doesn't find a has_many (LEW) that has the value of the model's default_scope" do 64 | s0.m1.create!(lew_s1s_column: "default_scope") 65 | s0.m1.create!(lew_s1s_column: "none") 66 | 67 | assert_wa_from(LEWS0, 0, :m1) 68 | end 69 | 70 | it "finds a matching has_one (LEW) that has the value of the association's scope" do 71 | skip if Test::SelectedDBHelper == Test::MySQL 72 | 73 | s0.create_o1!(lew_s1s_column: "has_one") 74 | s0.create_o1!(lew_s1s_column: "has_one") 75 | s0.create_o1!(lew_s1s_column: "default_scope") 76 | s0.create_o1!(lew_s1s_column: "none") 77 | # #create of has_one will unlink the existing one 78 | LEWS1.unscoped.update_all(lew_s0_id: s0.id) 79 | 80 | assert_wa_from(LEWS0, 1, :o1) 81 | end 82 | 83 | it "doesn't find a has_one (LEW) that has the value of the model's default_scope" do 84 | skip if Test::SelectedDBHelper == Test::MySQL 85 | 86 | s0.create_o1(lew_s1s_column: "default_scope") 87 | s0.create_o1(lew_s1s_column: "none") 88 | # #create of has_one will unlink the existing one 89 | LEWS1.unscoped.update_all(lew_s0_id: s0.id) 90 | 91 | assert_wa_from(LEWS0, 0, :o1) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/tests/wa_limit_offset_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | # Since the goal is to check only against the records that would be returned by the association, 6 | # we need to follow the expected behavior for limits, offset and order. 7 | 8 | describe "wa" do 9 | let(:s0) { LimOffOrdS0.create! } 10 | 11 | it "Count with belongs_to handles (ignore) offsets and limit" do 12 | s0 13 | assert_wa_from(LimOffOrdS0, 0, :b1) 14 | assert_wa_from(LimOffOrdS0, 0, :bl1) 15 | 16 | without_manual_wa_test do # ActiveRecord doesn't ignore offset for belongs_to... 17 | s0.create_b1! 18 | s0.save! 19 | assert_wa_from(LimOffOrdS0, 1, :b1) 20 | assert_wa_from(LimOffOrdS0, 1, :bl1) 21 | 22 | s0.create_b1! 23 | s0.save! 24 | assert_wa_from(LimOffOrdS0, 1, :b1) 25 | assert_wa_from(LimOffOrdS0, 1, :bl1) 26 | end 27 | end 28 | 29 | it "Count with has_many follows limits and offsets" do 30 | skip if Test::SelectedDBHelper == Test::MySQL 31 | s0 32 | assert_wa_from(LimOffOrdS0, 0, :m1) 33 | assert_wa_from(LimOffOrdS0, 0, :ml1) 34 | 35 | s0.m1.create! 36 | assert_wa_from(LimOffOrdS0, 0, :m1) 37 | assert_wa_from(LimOffOrdS0, 0, :ml1) 38 | 39 | s0.m1.create! 40 | assert_wa_from(LimOffOrdS0, 1, :m1) 41 | assert_wa_from(LimOffOrdS0, 0, :ml1) 42 | 43 | s0.m1.create! 44 | assert_wa_from(LimOffOrdS0, 2, :m1) 45 | assert_wa_from(LimOffOrdS0, 1, :ml1) 46 | 47 | s0.m1.create! 48 | assert_wa_from(LimOffOrdS0, 3, :m1) 49 | assert_wa_from(LimOffOrdS0, 2, :ml1) 50 | 51 | s0.m1.create! 52 | assert_wa_from(LimOffOrdS0, 3, :m1) 53 | assert_wa_from(LimOffOrdS0, 2, :ml1) 54 | end 55 | 56 | it "Count with has_many and options[:ignore_limit] ignores offsets and limits" do 57 | with_wa_default_options(ignore_limit: true) do 58 | without_manual_wa_test do 59 | s0 60 | assert_wa_from(LimOffOrdS0, 0, :m1) 61 | assert_wa_from(LimOffOrdS0, 0, :ml1) 62 | 63 | s0.m1.create! 64 | assert_wa_from(LimOffOrdS0, 1, :m1) 65 | assert_wa_from(LimOffOrdS0, 1, :ml1) 66 | 67 | s0.m1.create! 68 | assert_wa_from(LimOffOrdS0, 2, :m1) 69 | assert_wa_from(LimOffOrdS0, 2, :ml1) 70 | end 71 | end 72 | end 73 | 74 | it "Count with has_one follows offsets and limit is set to 1" do 75 | skip if Test::SelectedDBHelper == Test::MySQL 76 | s0 77 | assert_wa_from(LimOffOrdS0, 0, :o1) 78 | assert_wa_from(LimOffOrdS0, 0, :ol1) 79 | 80 | s0.create_has_one!(:o1) 81 | assert_wa_from(LimOffOrdS0, 0, :o1) 82 | assert_wa_from(LimOffOrdS0, 0, :ol1) 83 | 84 | s0.create_has_one!(:o1) 85 | assert_wa_from(LimOffOrdS0, 1, :o1) 86 | assert_wa_from(LimOffOrdS0, 0, :ol1) 87 | 88 | s0.create_has_one!(:o1) 89 | assert_wa_from(LimOffOrdS0, 1, :o1) 90 | assert_wa_from(LimOffOrdS0, 1, :ol1) 91 | 92 | s0.create_has_one!(:o1) 93 | assert_wa_from(LimOffOrdS0, 1, :o1) 94 | assert_wa_from(LimOffOrdS0, 1, :ol1) 95 | end 96 | 97 | it "Count with has_one and options[:ignore_limit] ignores offsets and limits and acts like has_many" do 98 | with_wa_default_options(ignore_limit: true) do 99 | without_manual_wa_test do 100 | s0 101 | assert_wa_from(LimOffOrdS0, 0, :o1) 102 | assert_wa_from(LimOffOrdS0, 0, :ol1) 103 | 104 | s0.create_has_one!(:o1) 105 | assert_wa_from(LimOffOrdS0, 1, :o1) 106 | assert_wa_from(LimOffOrdS0, 1, :ol1) 107 | 108 | s0.create_has_one!(:o1) 109 | assert_wa_from(LimOffOrdS0, 2, :o1) 110 | assert_wa_from(LimOffOrdS0, 2, :ol1) 111 | end 112 | end 113 | end 114 | 115 | it "Count with has_and_belongs_to_many follows limits and offsets" do 116 | skip if Test::SelectedDBHelper == Test::MySQL 117 | s0 118 | assert_wa_from(LimOffOrdS0, 0, :z1) 119 | assert_wa_from(LimOffOrdS0, 0, :zl1) 120 | 121 | s0.z1.create! 122 | assert_wa_from(LimOffOrdS0, 0, :z1) 123 | assert_wa_from(LimOffOrdS0, 0, :zl1) 124 | 125 | s0.z1.create! 126 | assert_wa_from(LimOffOrdS0, 1, :z1) 127 | assert_wa_from(LimOffOrdS0, 0, :zl1) 128 | 129 | s0.z1.create! 130 | assert_wa_from(LimOffOrdS0, 2, :z1) 131 | assert_wa_from(LimOffOrdS0, 1, :zl1) 132 | 133 | s0.z1.create! 134 | assert_wa_from(LimOffOrdS0, 3, :z1) 135 | assert_wa_from(LimOffOrdS0, 2, :zl1) 136 | 137 | s0.z1.create! 138 | assert_wa_from(LimOffOrdS0, 3, :z1) 139 | assert_wa_from(LimOffOrdS0, 2, :zl1) 140 | end 141 | 142 | it "Count with has_and_belongs_to_many and options[:ignore_limit] ignores offsets and limits" do 143 | with_wa_default_options(ignore_limit: true) do 144 | without_manual_wa_test do 145 | s0 146 | assert_wa_from(LimOffOrdS0, 0, :z1) 147 | assert_wa_from(LimOffOrdS0, 0, :zl1) 148 | 149 | s0.z1.create! 150 | assert_wa_from(LimOffOrdS0, 1, :z1) 151 | assert_wa_from(LimOffOrdS0, 1, :zl1) 152 | 153 | s0.z1.create! 154 | assert_wa_from(LimOffOrdS0, 2, :z1) 155 | assert_wa_from(LimOffOrdS0, 2, :zl1) 156 | end 157 | end 158 | end 159 | 160 | # Classes for the following tests only 161 | class LimThroughS0 < ActiveRecord::Base 162 | self.table_name = "s0s" 163 | has_many :m1, class_name: "LimThroughS1", foreign_key: "s0_id" 164 | has_many :limited_m2m1, -> { limit(2).reorder("s2s.id desc") }, class_name: "LimThroughS2", through: :m1, source: :m2 165 | end 166 | 167 | class LimThroughS1 < ActiveRecord::Base 168 | self.table_name = "s1s" 169 | has_many :m2, class_name: "LimThroughS2", foreign_key: "s1_id" 170 | end 171 | 172 | class LimThroughS2 < ActiveRecord::Base 173 | self.table_name = "s2s" 174 | end 175 | 176 | it "_* ignores limit from has_many :through's scope" do 177 | s0 = LimThroughS0.create! 178 | s1 = s0.m1.create! 179 | s1.m2.create! 180 | s1.m2.create! 181 | s1.m2.create! 182 | 183 | without_manual_wa_test do # Different handling of limit on :through associations 184 | assert_wa_from(LimThroughS0, 3, :limited_m2m1) 185 | end 186 | end 187 | 188 | 189 | # Classes for the following tests only 190 | class OffThroughS0 < ActiveRecord::Base 191 | self.table_name = "s0s" 192 | has_many :m1, class_name: "OffThroughS1", foreign_key: "s0_id" 193 | has_many :offset_m2m1, -> { offset(2).reorder("s2s.id desc") }, class_name: "OffThroughS2", through: :m1, source: :m2 194 | end 195 | 196 | class OffThroughS1 < ActiveRecord::Base 197 | self.table_name = "s1s" 198 | has_many :m2, class_name: "OffThroughS2", foreign_key: "s1_id" 199 | end 200 | 201 | class OffThroughS2 < ActiveRecord::Base 202 | self.table_name = "s2s" 203 | end 204 | 205 | it "_* ignores offset from has_many :through's scope" do 206 | s0 = OffThroughS0.create! 207 | s1 = s0.m1.create! 208 | s1.m2.create! 209 | without_manual_wa_test do # Different handling of offset on :through associations 210 | assert_wa_from(OffThroughS0, 1, :offset_m2m1) 211 | end 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /test/tests/wa_null_relation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { S0.create_default! } 7 | 8 | it "handles NullRelation in block on has_many" do 9 | s0 10 | assert_wa(0, :m1) 11 | assert_wa(0, :m1) { |scope| scope.none } 12 | 13 | s0.create_assoc!(:m1, :S0_m1) 14 | 15 | assert_wa(1, :m1) 16 | assert_wa(0, :m1) { |scope| scope.none } 17 | end 18 | 19 | it "handles NullRelation as condition on has_many association" do 20 | s0 21 | assert_wa(0, :m1_none) 22 | 23 | s0.create_assoc!(:m1, :S0_m1_none) 24 | 25 | assert_wa(0, :m1_none) 26 | 27 | # Just double checking that the create_assoc actually did something 28 | expected_m1_none_value = S1.test_condition_value_for(:default_scope) * S0.test_condition_value_for(:m1_none) 29 | assert S1.unscoped.where(s1s_column: expected_m1_none_value).exists? 30 | end 31 | 32 | it "handles NullRelation in block on has_one" do 33 | # MySQL doesn't support has_one 34 | next if Test::SelectedDBHelper == Test::MySQL 35 | 36 | s0 37 | assert_wa(0, :o1) 38 | assert_wa(0, :o1) { |scope| scope.none } 39 | 40 | s0.create_assoc!(:o1, :S0_o1) 41 | 42 | assert_wa(1, :o1) 43 | assert_wa(0, :o1) { |scope| scope.none } 44 | end 45 | 46 | it "handles NullRelation as condition on has_one association" do 47 | # MySQL doesn't support has_one 48 | next if Test::SelectedDBHelper == Test::MySQL 49 | 50 | s0 51 | assert_wa(0, :o1_none) 52 | 53 | s0.create_assoc!(:o1, :S0_o1_none) 54 | 55 | assert_wa(0, :o1_none) 56 | 57 | # Just double checking that the create_assoc actually did something 58 | expected_m1_none_value = S1.test_condition_value_for(:default_scope) * S0.test_condition_value_for(:o1_none) 59 | assert S1.unscoped.where(s1s_column: expected_m1_none_value).exists? 60 | end 61 | 62 | it "handles NullRelation in block on belongs_to" do 63 | s0 64 | assert_wa(0, :b1) 65 | assert_wa(0, :b1) { |scope| scope.none } 66 | 67 | s0.create_assoc!(:b1, :S0_b1) 68 | 69 | assert_wa(1, :b1) 70 | assert_wa(0, :b1) { |scope| scope.none } 71 | end 72 | 73 | it "handles NullRelation as condition on belongs_to association" do 74 | s0 75 | assert_wa(0, :b1_none) 76 | 77 | s0.create_assoc!(:b1, :S0_b1_none) 78 | 79 | assert_wa(0, :b1_none) 80 | 81 | # Just double checking that the create_assoc actually did something 82 | expected_m1_none_value = S1.test_condition_value_for(:default_scope) * S0.test_condition_value_for(:b1_none) 83 | assert S1.unscoped.where(s1s_column: expected_m1_none_value).exists? 84 | end 85 | 86 | it "handles NullRelation in block on has_and_belongs_to_many" do 87 | s0 88 | assert_wa(0, :z1) 89 | assert_wa(0, :z1) { |scope| scope.none } 90 | 91 | s0.create_assoc!(:z1, :S0_z1) 92 | 93 | assert_wa(1, :z1) 94 | assert_wa(0, :z1) { |scope| scope.none } 95 | end 96 | 97 | it "handles NullRelation as condition on has_and_belongs_to_many association" do 98 | s0 99 | assert_wa(0, :z1_none) 100 | 101 | s0.create_assoc!(:z1, :S0_z1_none) 102 | 103 | assert_wa(0, :z1_none) 104 | 105 | # Just double checking that the create_assoc actually did something 106 | expected_m1_none_value = S1.test_condition_value_for(:default_scope) * S0.test_condition_value_for(:z1_none) 107 | assert S1.unscoped.where(s1s_column: expected_m1_none_value).exists? 108 | end 109 | 110 | # ProfilePicture.where_assoc_exists(:profile, nil, poly_belongs_to: { PersonProfile => proc { |scope| scope.none } }) 111 | it "handles NullRelation in poly_belongs_to" do 112 | s0 113 | 114 | assert_wa(0, :bp1, nil, poly_belongs_to: { S1 => proc { |scope| scope.none } }) 115 | assert_wa(0, :bp1, nil, poly_belongs_to: [S1]) 116 | 117 | s0.create_assoc!(:bp1, :S0_bp1, target_model: S1) 118 | 119 | assert_wa(1, :bp1, nil, poly_belongs_to: [S1]) 120 | without_manual_wa_test do 121 | assert_wa(0, :bp1, nil, poly_belongs_to: { S1 => proc { |scope| scope.none } }) 122 | end 123 | end 124 | 125 | end 126 | -------------------------------------------------------------------------------- /test/tests/wa_options_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | it "doesn't use #from when options[:never_alias_limit]" do 7 | skip if Test::SelectedDBHelper == Test::MySQL 8 | 9 | sql = LimOffOrdS0.where_assoc_exists(:m1) { from("hello") }.to_sql 10 | assert !sql.include?("id") 11 | 12 | sql = LimOffOrdS0.where_assoc_exists(:m1, nil, never_alias_limit: true) { from("hello") }.to_sql 13 | assert sql.include?("id") 14 | 15 | with_wa_default_options(never_alias_limit: true) do 16 | sql = LimOffOrdS0.where_assoc_exists(:m1) { from("hello") }.to_sql 17 | assert sql.include?("id") 18 | end 19 | end 20 | 21 | it "raises an expection for invalid options" do 22 | assert_raises(ArgumentError) do 23 | S0.where_assoc_exists(:m1, nil, here_comes_a_bad_option: true).exists? 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/tests/wa_recursive_association_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | # Since the goal is to check only against the records that would be returned by the association, 6 | # we need to follow the expected behavior for limits, offset and order. 7 | 8 | describe "wa" do 9 | def check_recursive_association(association, nb_levels, &block) 10 | s0 11 | 12 | target_assoc = [association] * nb_levels 13 | assert_wa(0, target_assoc) 14 | 15 | current = s0 16 | nb_levels.times do |i| 17 | current = current.create_assoc!(association, nil, skip_attributes: true) 18 | assert_wa(0, target_assoc) unless i == nb_levels - 1 19 | end 20 | 21 | assert_wa(1, target_assoc) 22 | rescue Minitest::Assertion 23 | # Adding more of the backtrace to the message to make it easier to know where things failed. 24 | raise $!, "#{$!}\n#{Minitest.filter_backtrace($!.backtrace).join("\n")}", $!.backtrace 25 | end 26 | 27 | let(:s0) { RecursiveS.create! } 28 | let(:s0_from) { RecursiveS.where(id: RecursiveS.minimum(:id)) } 29 | 30 | (1..2).each do |nb_levels| 31 | it "_* handles #{nb_levels} levels of recursive belongs_to association(s) correctly" do 32 | check_recursive_association(:b1, nb_levels) 33 | end 34 | 35 | it "_* handles #{nb_levels} levels of recursive has_many association(s) correctly" do 36 | check_recursive_association(:m1, nb_levels) 37 | end 38 | 39 | it "_* handles #{nb_levels} levels of recursive has_one association(s) correctly" do 40 | skip if Test::SelectedDBHelper == Test::MySQL 41 | check_recursive_association(:o1, nb_levels) 42 | end 43 | 44 | it "_* handles #{nb_levels} levels of recursive has_and_belongs_to_many association(s) correctly" do 45 | check_recursive_association(:z1, nb_levels) 46 | end 47 | 48 | it "_* handles #{nb_levels} levels of recursive polymorphic has_many association(s) correctly" do 49 | check_recursive_association(:mp1, nb_levels) 50 | end 51 | 52 | it "_* handles #{nb_levels} levels of recursive polymorphic has_one association(s) correctly" do 53 | skip if Test::SelectedDBHelper == Test::MySQL 54 | check_recursive_association(:op1, nb_levels) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/tests/wa_sti_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | let(:s0) { STIS0.create! } 7 | 8 | it "belongs_to finds with STI type of same class" do 9 | s0.create_b1! 10 | s0.create_b1! 11 | s0.save! # Save the changed id 12 | 13 | assert_wa_from(STIS0, 1, :b1) 14 | end 15 | it "belongs_to finds with STI type of subclass" do 16 | s0.create_b1sub! 17 | s0.create_b1sub! 18 | s0.save! # Save the changed id 19 | 20 | assert_wa_from(STIS0, 1, :b1) 21 | end 22 | it "belongs_to doesn't find with STI type of superclass" do 23 | s0.create_b1! 24 | s0.create_b1! 25 | s0.save! # Save the changed id 26 | 27 | assert_wa_from(STIS0, 0, :b1sub) 28 | end 29 | 30 | it "has_one works with STI type of same class" do 31 | skip if Test::SelectedDBHelper == Test::MySQL 32 | s0.create_has_one!(:o1) 33 | s0.create_has_one!(:o1) 34 | 35 | assert_wa_from(STIS0, 1, :o1) 36 | end 37 | it "has_one works with STI type of subclass" do 38 | skip if Test::SelectedDBHelper == Test::MySQL 39 | s0.create_has_one!(:o1sub) 40 | s0.create_has_one!(:o1sub) 41 | 42 | assert_wa_from(STIS0, 1, :o1) 43 | end 44 | it "has_one works with STI type of superclass" do 45 | skip if Test::SelectedDBHelper == Test::MySQL 46 | s0.create_has_one!(:o1) 47 | s0.create_has_one!(:o1) 48 | 49 | assert_wa_from(STIS0, 0, :o1sub) 50 | end 51 | 52 | it "has_many works with STI type of same class" do 53 | s0.m1.create! 54 | s0.m1.create! 55 | 56 | assert_wa_from(STIS0, 2, :m1) 57 | end 58 | it "has_many works with STI type of subclass" do 59 | s0.m1sub.create! 60 | s0.m1sub.create! 61 | 62 | assert_wa_from(STIS0, 2, :m1) 63 | end 64 | it "has_many works with STI type of superclass" do 65 | s0.m1.create! 66 | s0.m1.create! 67 | 68 | assert_wa_from(STIS0, 0, :m1sub) 69 | end 70 | 71 | it "has_and_belongs_to_many works with STI type of same class" do 72 | s0.z1.create! 73 | s0.z1.create! 74 | 75 | assert_wa_from(STIS0, 2, :z1) 76 | end 77 | it "has_and_belongs_to_many works with STI type of subclass" do 78 | s0.z1sub.create! 79 | s0.z1sub.create! 80 | 81 | assert_wa_from(STIS0, 2, :z1) 82 | end 83 | it "has_and_belongs_to_many works with STI type of superclass" do 84 | s0.z1.create! 85 | s0.z1.create! 86 | 87 | assert_wa_from(STIS0, 0, :z1sub) 88 | end 89 | 90 | it "polymorphic has_many works when defined and used from a root STI class" do 91 | s0 92 | assert_wa_from(STIS0, 0, :mp1) 93 | s0.mp1.create! 94 | s0.mp1.create! 95 | assert_wa_from(STIS0, 2, :mp1) 96 | end 97 | it "polymorphic has_many works when defined on root STI class and used from a subclass" do 98 | s0 = STIS0Sub.create! 99 | assert_wa_from(STIS0, 0, :mp1) 100 | assert_wa_from(STIS0Sub, 0, :mp1) 101 | s0.mp1.create 102 | s0.mp1.create 103 | assert_wa_from(STIS0, 2, :mp1) 104 | assert_wa_from(STIS0Sub, 2, :mp1) 105 | end 106 | it "polymorphic has_many works when defined and used on the same STI subclass" do 107 | s0 = STIS0Sub.create! 108 | assert_wa_from(STIS0Sub, 0, :mp1_from_sub) 109 | s0.mp1_from_sub.create 110 | s0.mp1_from_sub.create 111 | assert_wa_from(STIS0Sub, 2, :mp1_from_sub) 112 | end 113 | it "polymorphic has_many works when defined on an STI subclass and used from a deeper subclass" do 114 | s0 = STIS0SubSub.create! 115 | assert_wa_from(STIS0SubSub, 0, :mp1_from_sub) 116 | s0.mp1_from_sub.create 117 | s0.mp1_from_sub.create 118 | assert_wa_from(STIS0SubSub, 2, :mp1_from_sub) 119 | end 120 | 121 | 122 | it "polymorphic has_one works when defined and used from a root STI class" do 123 | skip if Test::SelectedDBHelper == Test::MySQL 124 | s0 125 | assert_wa_from(STIS0, 0, :op1) 126 | s0.create_has_one!(:op1) 127 | s0.create_has_one!(:op1) 128 | assert_wa_from(STIS0, 1, :op1) 129 | end 130 | it "polymorphic has_one works when defined on root STI class and used from a subclass" do 131 | skip if Test::SelectedDBHelper == Test::MySQL 132 | s0 = STIS0Sub.create! 133 | assert_wa_from(STIS0, 0, :op1) 134 | assert_wa_from(STIS0Sub, 0, :op1) 135 | s0.create_has_one!(:op1) 136 | s0.create_has_one!(:op1) 137 | assert_wa_from(STIS0, 1, :op1) 138 | assert_wa_from(STIS0Sub, 1, :op1) 139 | end 140 | it "polymorphic has_one works when defined and used on the same STI subclass" do 141 | skip if Test::SelectedDBHelper == Test::MySQL 142 | s0 = STIS0Sub.create! 143 | assert_wa_from(STIS0Sub, 0, :op1_from_sub) 144 | s0.create_has_one!(:op1) 145 | s0.create_has_one!(:op1) 146 | assert_wa_from(STIS0Sub, 1, :op1_from_sub) 147 | end 148 | it "polymorphic has_one works when defined on an STI subclass and used from a deeper subclass" do 149 | skip if Test::SelectedDBHelper == Test::MySQL 150 | s0 = STIS0SubSub.create! 151 | assert_wa_from(STIS0SubSub, 0, :op1_from_sub) 152 | s0.create_has_one!(:op1) 153 | s0.create_has_one!(:op1) 154 | assert_wa_from(STIS0SubSub, 1, :op1_from_sub) 155 | end 156 | 157 | it "polymorphic belongs_to to a STI top class works the same as in ActiveRecord" do 158 | s0 159 | assert_wa_from(STIS0, 0, :bp1, nil, poly_belongs_to: :pluck) 160 | s1 = STIS1.create! 161 | s0.bp1 = s1 162 | s0.save! 163 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: :pluck) 164 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: [STIS1]) 165 | # Using such a subclass like that is basically a condition which we don't compare in manual testing 166 | without_manual_wa_test do 167 | assert_wa_from(STIS0, 0, :bp1, nil, poly_belongs_to: [STIS1Sub]) 168 | assert_wa_from(STIS0, 0, :bp1, nil, poly_belongs_to: [STIS1SubSub]) 169 | end 170 | end 171 | 172 | it "polymorphic belongs_to to a STI single subclass works the same as in ActiveRecord" do 173 | s0 174 | assert_wa_from(STIS0, 0, :bp1, nil, poly_belongs_to: :pluck) 175 | s1 = STIS1Sub.create! 176 | s0.bp1 = s1 177 | s0.save! 178 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: :pluck) 179 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: [STIS1]) 180 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: [STIS1Sub]) 181 | # Using such a subclass like that is basically a condition which we don't compare in manual testing 182 | without_manual_wa_test do 183 | assert_wa_from(STIS0, 0, :bp1, nil, poly_belongs_to: [STIS1SubSub]) 184 | end 185 | end 186 | 187 | it "polymorphic belongs_to to a STI double subclass works the same as in ActiveRecord" do 188 | s0 189 | assert_wa_from(STIS0, 0, :bp1, nil, poly_belongs_to: :pluck) 190 | s1 = STIS1SubSub.create! 191 | s0.bp1 = s1 192 | s0.save! 193 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: :pluck) 194 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: [STIS1]) 195 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: [STIS1Sub]) 196 | assert_wa_from(STIS0, 1, :bp1, nil, poly_belongs_to: [STIS1SubSub]) 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /test/tests/wa_table_name_with_schema_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | next if Test::SelectedDBHelper == Test::SQLite3 7 | describe "from a schema table" do 8 | let(:s0) { SchemaS0.create! } 9 | 10 | describe "to a schema table" do 11 | it "belongs_to works" do 12 | s0.create_schema_b1! 13 | s0.save! # Save the changed id 14 | 15 | assert_wa_from(SchemaS0, 1, :schema_b1) 16 | end 17 | 18 | it "has_one works" do 19 | skip if Test::SelectedDBHelper == Test::MySQL 20 | 21 | s0.create_has_one!(:schema_o1) 22 | 23 | assert_wa_from(SchemaS0, 1, :schema_o1) 24 | end 25 | 26 | it "has_many works" do 27 | s0.schema_m1.create! 28 | 29 | assert_wa_from(SchemaS0, 1, :schema_m1) 30 | end 31 | 32 | it "has_and_belongs_to_many works" do 33 | s0.schema_z1.create! 34 | 35 | assert_wa_from(SchemaS0, 1, :schema_z1) 36 | end 37 | end 38 | 39 | describe "to a schemaless table" do 40 | it "belongs_to works" do 41 | s0.create_assoc!(:b1, nil) 42 | 43 | assert_wa_from(SchemaS0, 1, :b1) 44 | end 45 | 46 | it "has_one works" do 47 | skip if Test::SelectedDBHelper == Test::MySQL 48 | s0.create_assoc!(:o1, nil) 49 | 50 | assert_wa_from(SchemaS0, 1, :o1) 51 | end 52 | 53 | it "has_many works" do 54 | s0.create_assoc!(:m1, nil) 55 | 56 | assert_wa_from(SchemaS0, 1, :m1) 57 | end 58 | end 59 | end 60 | 61 | describe "from a schemaless table to a schema table" do 62 | let(:s0) { S0.create_default! } 63 | it "belongs_to works" do 64 | s0.create_schema_b1! 65 | s0.save! # Save the changed id 66 | 67 | assert_wa_from(S0, 1, :schema_b1) 68 | end 69 | 70 | it "has_one works" do 71 | skip if Test::SelectedDBHelper == Test::MySQL 72 | 73 | s0.create_has_one!(:schema_o1) 74 | 75 | assert_wa_from(S0, 1, :schema_o1) 76 | end 77 | 78 | it "has_many works" do 79 | s0.schema_m1.create! 80 | 81 | assert_wa_from(S0, 1, :schema_m1) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/tests/wa_through_inter_macro_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | describe "wa" do 6 | # MySQL doesn't support has_one 7 | next if Test::SelectedDBHelper == Test::MySQL 8 | 9 | let(:s0) { S0.create_default! } 10 | 11 | it "has_one through: belongs_to, source: belongs_to doesn't use LIMIT" do 12 | scope = S0.where_assoc_exists(:ob2b1) 13 | sql = scope.to_sql 14 | scope.to_a # Make sure it doesn't fail 15 | refute_includes sql.upcase, "LIMIT" 16 | end 17 | 18 | it "has_one through: has_many, source: has_and_belongs_to_many respects the limit of the source" do 19 | without_manual_wa_test do 20 | s0 = LimOffOrdS0.create! 21 | 22 | s1 = s0.m1.create! # not enough to go above the offset of LimOffOrdS1's default scope 23 | 24 | s1.zl2.create! 25 | s1.zl2.create! 26 | s1.zl2.create! 27 | 28 | assert_wa_from(LimOffOrdS0, 0, :mzl2m1) 29 | 30 | s1 = s0.m1.create! # now it's enough to go above the offset of LimOffOrdS1's default scope 31 | 32 | assert_wa_from(LimOffOrdS0, 0, :mzl2m1) 33 | 34 | s1.zl2.create! 35 | s1.zl2.create! # not enough to go above zl2's offset (2) 36 | 37 | assert_wa_from(LimOffOrdS0, 0, :mzl2m1) 38 | 39 | s1.zl2.create! # it's now enough to go above zl2's offset (2) 40 | 41 | assert_wa_from(LimOffOrdS0, 1, :mzl2m1) 42 | end 43 | end 44 | end 45 | --------------------------------------------------------------------------------