├── .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 |
32 | ☰
33 |
34 |
35 |
36 |
37 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
85 |
86 |
87 | module ActiveRecordWhereAssoc
88 |
89 |
90 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
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 |
32 | ☰
33 |
34 |
35 |
36 |
37 |
66 |
67 |
68 |
69 |
70 |
71 |
Class and Module Index
72 |
73 |
80 |
81 |
82 |
87 |
88 |
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 |
32 | ☰
33 |
34 |
35 |
36 |
37 |
38 |
49 |
50 |
51 |
65 |
66 |
67 |
68 |
73 |
74 |
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 |
--------------------------------------------------------------------------------