unreliable 0.9.1 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +42 -33
- data/Rakefile +2 -0
- data/lib/unreliable/build_order.rb +8 -2
- data/lib/unreliable/version.rb +1 -1
- data/spec/adapter_option_spec.rb +7 -0
- data/spec/env_spec.rb +2 -2
- data/spec/execute_queries_spec.rb +125 -0
- data/spec/execute_subqueries_spec.rb +46 -0
- data/spec/model_indexes_books_spec.rb +3 -3
- data/spec/model_indexes_cats_spec.rb +2 -2
- data/spec/model_indexes_dreams_spec.rb +2 -2
- data/spec/model_indexes_shelves_spec.rb +6 -6
- data/spec/model_joins_spec.rb +8 -8
- data/spec/model_select_distinct_spec.rb +48 -0
- data/spec/model_select_spec.rb +5 -5
- data/spec/model_subquery_spec.rb +1 -1
- data/spec/model_update_arel_10_spec.rb +69 -6
- data/spec/spec_helper.rb +77 -0
- data/spec/textual_order_raw_spec.rb +98 -0
- data/spec/textual_order_spec.rb +14 -10
- metadata +56 -10
- data/spec/examples.txt +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3cf18102610d7a8a8c380f39bad81db77ed33392ecc8dffcdc99ef7c60d222e
|
4
|
+
data.tar.gz: b99100338674b31fbb11036b56ce1443006d1281e5641a0b4f200bbe7a22e832
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96139a8994cdb3badf0f80e3bc96c94c375864f9973d9b85b56fa6ad97486001dbaa56e24b42cdb2d4ba7d051a22877851fac26a5a993b07773984521cb456e2
|
7
|
+
data.tar.gz: 890b786b597e60b980937d792555a3666082f7d38ab6e6b1113acdfb01adc5ec6596ef59ed2041a89215526c2ca8dacccef1184967db68356f178155617d3aaa
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
## Unreliable 0.10.0 (January 15, 2024) ##
|
2
|
+
|
3
|
+
### Changed
|
4
|
+
|
5
|
+
* Rails 5.0 and 5.1 compatibility removed due to bug in interaction with Arel 8. This is not a breaking change because it didn't work before. If you must run Rails < 5.2, do not use Unreliable.
|
6
|
+
* PostgreSQL bug with SELECT DISTINCT fixed (#10).
|
7
|
+
* Many tests added; CI dockerized; test suite runs against SQLite, PostgreSQL, and MySQL (#3).
|
8
|
+
|
1
9
|
## Unreliable 0.9.1 (November 21, 2022) ##
|
2
10
|
|
3
11
|
### Changed
|
data/README.md
CHANGED
@@ -15,7 +15,7 @@ Add `unreliable` to your `Gemfile`'s `test` group:
|
|
15
15
|
# Gemfile
|
16
16
|
|
17
17
|
group :test do
|
18
|
-
gem "unreliable", "~> 0.
|
18
|
+
gem "unreliable", "~> 0.10"
|
19
19
|
end
|
20
20
|
```
|
21
21
|
|
@@ -27,17 +27,26 @@ Here's an [open secret](#references): **relational databases do not guarantee th
|
|
27
27
|
|
28
28
|
If all your ActiveRecord ordering is already unambiguous, congratulations! `unreliable` will have no effect.
|
29
29
|
|
30
|
-
But sometimes we think we specified an unambiguous order, but didn't.
|
30
|
+
But sometimes... we think we specified an unambiguous order, but we didn't. For example, maybe we ordered on timestamps, which are usually unique but sometimes not.
|
31
31
|
|
32
|
-
|
32
|
+
The test suite will stay silent about that, as long as our database just happens to return the same order. That silence is a problem.
|
33
33
|
|
34
|
-
|
34
|
+
If ambiguous ordering is fine for your app's purposes, but your tests rely on a specific order, that's a **bug in your tests**. Your tests are incorrectly failing -- rarely -- which can be confusing and annoying.
|
35
|
+
|
36
|
+
Or, if your Rails code relies on that accidental ordering, that's a **bug in your app**. Your tests are passing when they should be failing.
|
35
37
|
|
36
38
|
In both cases, `unreliable` exposes the problem by making those tests fail most of the time.
|
37
39
|
|
38
40
|
## Fixing the new failures
|
39
41
|
|
40
|
-
When `unreliable` turns up a new test failure, you fix it in one of two ways.
|
42
|
+
When `unreliable` turns up a new test failure, you fix it in one of two ways.
|
43
|
+
|
44
|
+
Either:
|
45
|
+
|
46
|
+
* relax your test so it stops relying on order,
|
47
|
+
* or tighten up your app to specify order rigorously.
|
48
|
+
|
49
|
+
In my company's app, it was about 50/50.
|
41
50
|
|
42
51
|
### Relax a test
|
43
52
|
|
@@ -45,7 +54,7 @@ Take a look at what your test is checking. If you're testing a method or an endp
|
|
45
54
|
|
46
55
|
* Make your test accept all correct answers. For example, sort an array in the method's response before comparing.
|
47
56
|
|
48
|
-
* Help your test suite focus on what you're testing. If your fixtures' "latest" element could change because they don't specify a timestamp, that might be a distraction that's not relevant to how your app works, so you could assign timestamps to the fixtures.
|
57
|
+
* Help your test suite focus on what you're testing. If your fixtures' "latest" element could change because they don't specify a timestamp, that might be a distraction that's not relevant to how your app works, so you could assign unique timestamps to the fixtures.
|
49
58
|
|
50
59
|
This makes your test suite more robust.
|
51
60
|
|
@@ -53,7 +62,9 @@ If your test suite is checking generated `.to_sql` against known-good SQL text,
|
|
53
62
|
|
54
63
|
### Tighten the app
|
55
64
|
|
56
|
-
If your app should be returning results in a particular order, and now with `unreliable` it sometimes does not, your test is correct and your app is wrong.
|
65
|
+
If your app should be returning results in a particular order, and now with `unreliable` it sometimes does not, your test is correct and your app is wrong.
|
66
|
+
|
67
|
+
Specify order rigorously in your app.
|
57
68
|
|
58
69
|
Maybe you're testing `Book.reverse_chron.first`, and you've defined that ordering this way:
|
59
70
|
|
@@ -75,19 +86,23 @@ Or, if `title` is not unique:
|
|
75
86
|
scope :reverse_chron, -> { order(year_published: :desc, title: :desc, id: :desc) }
|
76
87
|
```
|
77
88
|
|
78
|
-
|
89
|
+
This example's problem is easy to see because many books are published each year.
|
90
|
+
|
91
|
+
But this error can occur at any granularity, in time or other data types.
|
79
92
|
|
80
93
|
## Requirements
|
81
94
|
|
82
|
-
`unreliable` is tested
|
95
|
+
`unreliable` is tested on every valid combination of:
|
83
96
|
|
84
|
-
|
97
|
+
* sqlite, postgresql, and mysql2 adapters
|
98
|
+
* Ruby 2.6 through 3.3
|
99
|
+
* Rails 5.2 through 7.1
|
85
100
|
|
86
101
|
`unreliable` depends only on ActiveRecord and Railties. If you have a non-Rails app that uses ActiveRecord, you can still use it.
|
87
102
|
|
88
103
|
## Implementation
|
89
104
|
|
90
|
-
`unreliable` does exactly nothing outside of test environments. There is intentionally no way to enable
|
105
|
+
`unreliable` does exactly nothing outside of test environments. There is intentionally no way to enable it in production, and there never will be.
|
91
106
|
|
92
107
|
In a Rails test environment, `unreliable` patches ActiveRecord to append a final `ORDER BY` clause, when necessary, that returns results in a random order.
|
93
108
|
|
@@ -101,47 +116,41 @@ This means that the `ORDER BY` applies to not just `SELECT` but e.g. `delete_all
|
|
101
116
|
|
102
117
|
The patch is only applied when `Rails.env.test?`, and that boolean is also checked on every invocation, just to make certain it has no effect in any other environment.
|
103
118
|
|
119
|
+
The gem has a large test suite that checks for correctness at several abstraction layers inside ActiveRecord. It ensures the correct SQL is generated and that it executes correctly.
|
120
|
+
|
104
121
|
### No dual-purpose environment please
|
105
122
|
|
106
|
-
Your test environment is just for running your test suite. If you've overloaded the test environment to do any actual work, you'
|
123
|
+
Your test environment is just for running your test suite. If you've overloaded the test environment to do any actual work, you'll be frustrated when `unreliable` slows it down and changes its behavior. Don't do that.
|
107
124
|
|
108
125
|
## Contributing
|
109
126
|
|
110
|
-
Thoughts and suggestions are welcome. Please read the code of conduct, then create an issue or pull request on GitHub. If you just have questions, go ahead and open an issue
|
127
|
+
Thoughts and suggestions are welcome. Please read the code of conduct, then create an issue or pull request on GitHub. If you just have questions, please go ahead and open an issue!
|
111
128
|
|
112
129
|
### Run the gem's tests
|
113
130
|
|
114
|
-
To test locally,
|
115
|
-
|
116
|
-
```
|
117
|
-
gem install bundler
|
118
|
-
bundle install
|
119
|
-
bundle exec appraisal install
|
120
|
-
```
|
131
|
+
To test locally, see the hint at the top of `compose.yaml` to spin up docker containers.
|
121
132
|
|
122
|
-
|
133
|
+
After you spin up the containers and open a shell in the app container, run `unreliable`'s linter with:
|
123
134
|
|
124
135
|
```
|
125
|
-
|
136
|
+
standardrb
|
126
137
|
```
|
127
138
|
|
128
|
-
|
139
|
+
Run its tests in three separate passes:
|
129
140
|
|
130
141
|
```
|
131
|
-
bundle exec
|
142
|
+
RSPEC_ADAPTER=sqlite bundle exec rake
|
143
|
+
RSPEC_ADAPTER=postgresql bundle exec rake
|
144
|
+
RSPEC_ADAPTER=mysql2 bundle exec rake
|
132
145
|
```
|
133
146
|
|
134
|
-
Appraisal ensures the tests run against every compatible minor version of ActiveRecord.
|
135
|
-
|
136
147
|
The GitHub CI workflow in `.github/` ensures those tests are also run against against every compatible minor version of Ruby. Your PR won't trigger my GitHub project's workflow, but you're welcome to run your own, or ask me to run mine manually.
|
137
148
|
|
138
|
-
Testing against ActiveRecord is done with [Combustion](https://github.com/pat/combustion), which stands up a local SQLite database and ActiveRecord-based models for it. This gives more reliable coverage than mocking unit tests within ActiveRecord itself, though I do some of that too.
|
139
|
-
|
140
149
|
### Experiment
|
141
150
|
|
142
151
|
If you'd like to see `unreliable` in action on a small but real Rails app locally, you can do this:
|
143
152
|
|
144
|
-
1. In a directory next to your `unreliable` working directory, create a `.ruby-version` of `2.7.
|
153
|
+
1. In a directory next to your `unreliable` working directory, create a `.ruby-version` of `2.7.8` and a 2-line `Gemfile`: `source "https://rubygems.org"`, `gem "rails", "~> 7.0"`
|
145
154
|
2. `bundle install && bundle exec rails new . --force`
|
146
155
|
3. `echo 'gem "unreliable", path: "../unreliable"' >> Gemfile`
|
147
156
|
4. `bundle install && bundle exec rails generate model post title:string body:text`
|
@@ -166,8 +175,8 @@ The most common ambiguous ordering is an ORDER BY one column that is not unique,
|
|
166
175
|
But there are other ways you can order a relation but still have your query be ambiguous:
|
167
176
|
|
168
177
|
* ORDER BY multiple columns, but with no subset which is unique
|
169
|
-
* ORDER BY a column
|
170
|
-
* ORDER BY values that are identical within the [prefix length limit](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_sort_length) examined for sorting
|
178
|
+
* ORDER BY a column your [pre-Rails-6.1](https://guides.rubyonrails.org/6_1_release_notes.html#active-record-notable-changes) application thought was unique, but currently isn't, due to your non-UNIQUE database column's accent- or case-insensitive [collation](https://dev.mysql.com/doc/refman/8.0/en/charset-general.html)
|
179
|
+
* ORDER BY values that are identical only within the [prefix length limit](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_sort_length) examined for sorting
|
171
180
|
|
172
181
|
`unreliable` ensures correct testing because it appends a random order to each of these cases.
|
173
182
|
|
@@ -181,11 +190,11 @@ MySQL ([5.6](https://dev.mysql.com/doc/refman/5.6/en/limit-optimization.html), [
|
|
181
190
|
|
182
191
|
> If multiple rows have identical values in the `ORDER BY` columns, the server is free to return those rows in any order, and may do so differently depending on the overall execution plan. In other words, the sort order of those rows is nondeterministic with respect to the nonordered columns.
|
183
192
|
|
184
|
-
Postgres ([
|
193
|
+
Postgres ([13](https://www.postgresql.org/docs/13/sql-select.html#SQL-ORDERBY), [14](https://www.postgresql.org/docs/14/sql-select.html#SQL-ORDERBY), [15](https://www.postgresql.org/docs/15/sql-select.html#SQL-ORDERBY), [16](https://www.postgresql.org/docs/16/sql-select.html#SQL-ORDERBY)):
|
185
194
|
|
186
195
|
> If two rows are equal according to the leftmost expression, they are compared according to the next expression and so on. If they are equal according to all specified expressions, they are returned in an implementation-dependent order.
|
187
196
|
|
188
|
-
SQLite ([3.
|
197
|
+
SQLite ([3.45](https://www.sqlite.org/lang_select.html#the_order_by_clause)):
|
189
198
|
|
190
199
|
> The order in which two rows for which all ORDER BY expressions evaluate to equal values are returned is undefined.
|
191
200
|
|
data/Rakefile
CHANGED
@@ -9,17 +9,19 @@ module Unreliable
|
|
9
9
|
def build_order(arel)
|
10
10
|
super(arel)
|
11
11
|
|
12
|
+
adapter_name = Arel::Table.engine.connection.adapter_name
|
12
13
|
return unless Unreliable::Config.enabled?
|
14
|
+
return if distinct_on_postgres?(adapter_name)
|
13
15
|
return if from_only_internal_metadata?(arel)
|
14
16
|
return if from_one_table_with_ordered_pk?(arel)
|
15
17
|
|
16
|
-
case
|
18
|
+
case adapter_name
|
17
19
|
when "Mysql2"
|
18
20
|
# https://dev.mysql.com/doc/refman/8.0/en/mathematical-functions.html#function_rand
|
19
21
|
arel.order("RAND()")
|
20
22
|
|
21
23
|
when "PostgreSQL", "SQLite"
|
22
|
-
# https://www.postgresql.org/docs/
|
24
|
+
# https://www.postgresql.org/docs/16/functions-math.html#FUNCTIONS-MATH-RANDOM-TABLE
|
23
25
|
# https://www.sqlite.org/lang_corefunc.html#random
|
24
26
|
arel.order("RANDOM()")
|
25
27
|
|
@@ -29,6 +31,10 @@ module Unreliable
|
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
34
|
+
def distinct_on_postgres?(adapter_name)
|
35
|
+
distinct_value && adapter_name == "PostgreSQL"
|
36
|
+
end
|
37
|
+
|
32
38
|
def from_only_internal_metadata?(arel)
|
33
39
|
# No need to randomize queries on ar_internal_metadata
|
34
40
|
arel.froms.map(&:name) == [ActiveRecord::Base.internal_metadata_table_name]
|
data/lib/unreliable/version.rb
CHANGED
data/spec/env_spec.rb
CHANGED
@@ -3,14 +3,14 @@
|
|
3
3
|
RSpec.describe Unreliable do
|
4
4
|
it "does nothing in prod" do
|
5
5
|
Rails.env = "production"
|
6
|
-
expect(Cat.where(word: "foo").to_sql).to end_with(%q(WHERE "cats"."word" = 'foo'))
|
6
|
+
expect(Cat.where(word: "foo").to_sql).to end_with(adapter_text(%q(WHERE "cats"."word" = 'foo')))
|
7
7
|
ensure
|
8
8
|
Rails.env = "test"
|
9
9
|
end
|
10
10
|
|
11
11
|
it "does nothing in dev" do
|
12
12
|
Rails.env = "development"
|
13
|
-
expect(Cat.where(word: "foo").to_sql).to end_with(%q(WHERE "cats"."word" = 'foo'))
|
13
|
+
expect(Cat.where(word: "foo").to_sql).to end_with(adapter_text(%q(WHERE "cats"."word" = 'foo')))
|
14
14
|
ensure
|
15
15
|
Rails.env = "test"
|
16
16
|
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CatTest < UnreliableTest
|
4
|
+
# 12 factorial is about half a billion possible shuffles
|
5
|
+
NAMES = %w[angus Rashad bertha harry moka bubbles Morty Tofu Purrito Neffy Zoe stinky].freeze
|
6
|
+
RESPONSE_COUNT = 10
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec.describe Cat do
|
10
|
+
it "adds and selects all cats" do
|
11
|
+
expect(Cat.count).to eq(0)
|
12
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
13
|
+
expect(Cat.all.to_a.size).to eq(CatTest::NAMES.size)
|
14
|
+
ensure
|
15
|
+
Cat.delete_all
|
16
|
+
end
|
17
|
+
|
18
|
+
it "adds, updates-via-instance, and selects some cats" do
|
19
|
+
expect(Cat.count).to eq(0)
|
20
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
21
|
+
expect(Cat.where(name: "Rashad").to_a.size).to eq(1)
|
22
|
+
expect(Cat.where("name LIKE '%a%'").to_a.size).to eq(5)
|
23
|
+
Cat.find_by(name: "harry").destroy!
|
24
|
+
expect(Cat.where("name LIKE '%a%'").to_a.size).to eq(4)
|
25
|
+
Cat.where("name NOT LIKE '%a%'").first.update!(name: "Mantissa")
|
26
|
+
expect(Cat.where("name LIKE '%a%'").to_a.size).to eq(5)
|
27
|
+
ensure
|
28
|
+
Cat.delete_all
|
29
|
+
end
|
30
|
+
|
31
|
+
it "adds, updates-via-class, and selects some cats" do
|
32
|
+
expect(Cat.count).to eq(0)
|
33
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
34
|
+
expect(Cat.where(name: "Rashad").to_a.size).to eq(1)
|
35
|
+
expect(Cat.where("name LIKE '%a%'").to_a.size).to eq(5)
|
36
|
+
Cat.find_by(name: "harry").destroy!
|
37
|
+
expect(Cat.where("name LIKE '%a%'").to_a.size).to eq(4)
|
38
|
+
Cat.update(Cat.where("name NOT LIKE '%a%'").pluck(:id).first, name: "Mantissa")
|
39
|
+
expect(Cat.where("name LIKE '%a%'").to_a.size).to eq(5)
|
40
|
+
ensure
|
41
|
+
Cat.delete_all
|
42
|
+
end
|
43
|
+
|
44
|
+
it "adds and selects all ordered data unpredictably" do
|
45
|
+
expect(Cat.count).to eq(0)
|
46
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
47
|
+
responses = (1..CatTest::RESPONSE_COUNT).map do
|
48
|
+
Cat.all.map(&:name).join(":")
|
49
|
+
end
|
50
|
+
# The chances that there's one repeat in 10 randomly-ordered SELECTs
|
51
|
+
# is about 1 in ten billion, and we allow for that. The chances that
|
52
|
+
# there's two and this test incorrectly fails is in the quintillionths.
|
53
|
+
expect(responses.uniq.size).to(satisfy { |v| v >= 9 })
|
54
|
+
ensure
|
55
|
+
Cat.delete_all
|
56
|
+
end
|
57
|
+
|
58
|
+
it "adds and selects some ordered data unpredictably" do
|
59
|
+
expect(Cat.count).to eq(0)
|
60
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
61
|
+
responses = (1..CatTest::RESPONSE_COUNT).map do
|
62
|
+
Cat.where.not(name: "bubbles").map(&:name).join(":")
|
63
|
+
end
|
64
|
+
expect(responses.uniq.size).to(satisfy { |v| v >= 8 })
|
65
|
+
ensure
|
66
|
+
Cat.delete_all
|
67
|
+
end
|
68
|
+
|
69
|
+
it "adds and selects all ordered data predictably with order by id" do
|
70
|
+
expect(Cat.count).to eq(0)
|
71
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
72
|
+
responses = (1..CatTest::RESPONSE_COUNT).map do
|
73
|
+
Cat.order(:id).map(&:name).join(":")
|
74
|
+
end
|
75
|
+
expect(responses.uniq.size).to eq(1)
|
76
|
+
ensure
|
77
|
+
Cat.delete_all
|
78
|
+
end
|
79
|
+
|
80
|
+
it "adds and selects all ordered data predictably with order by name" do
|
81
|
+
expect(Cat.count).to eq(0)
|
82
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
83
|
+
responses = (1..CatTest::RESPONSE_COUNT).map do
|
84
|
+
Cat.order(:name).map(&:name).join(":")
|
85
|
+
end
|
86
|
+
expect(responses.uniq.size).to eq(1)
|
87
|
+
ensure
|
88
|
+
Cat.delete_all
|
89
|
+
end
|
90
|
+
|
91
|
+
it "adds and selects some ordered data predictably with order" do
|
92
|
+
expect(Cat.count).to eq(0)
|
93
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
94
|
+
responses = (1..CatTest::RESPONSE_COUNT).map do
|
95
|
+
Cat.where.not(name: "Groovy").order(:id).map(&:name).join(":")
|
96
|
+
end
|
97
|
+
expect(responses.uniq.size).to eq(1)
|
98
|
+
ensure
|
99
|
+
Cat.delete_all
|
100
|
+
end
|
101
|
+
|
102
|
+
it "adds and selects all ordered data predictably with disable" do
|
103
|
+
expect(Cat.count).to eq(0)
|
104
|
+
CatTest::NAMES.shuffle.each { |name| Cat.new(name: name).save! }
|
105
|
+
responses =
|
106
|
+
Unreliable::Config.disable do
|
107
|
+
(1..CatTest::RESPONSE_COUNT).map do
|
108
|
+
Cat.all.map(&:name).join(":")
|
109
|
+
end
|
110
|
+
end
|
111
|
+
# This is testing the actual undefined database behavior that Unreliable
|
112
|
+
# was created to account for! In practice it's quite rare to observe
|
113
|
+
# differing results on sequential SELECTs. I can't quantify the chances
|
114
|
+
# of it like I can with expected-truly-random behavior above, but I'm
|
115
|
+
# making an educated guess here and saying if we see more than 3 of 10,
|
116
|
+
# something went wrong with the gem disabling its behavior. But because
|
117
|
+
# database and protocol documentation all says it can do whatever it
|
118
|
+
# wants, the number of unique responses might be up to RESPONSE_COUNT!
|
119
|
+
# If this test fails erroneously basically ever, I would think it
|
120
|
+
# should be rewritten or removed!
|
121
|
+
expect(responses.uniq.size).to(satisfy { |v| v <= 3 })
|
122
|
+
ensure
|
123
|
+
Cat.delete_all
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DreamTest < UnreliableTest
|
4
|
+
SUBJECTS = %w[fire air water earth life death].freeze
|
5
|
+
DREAMER_NAMES = %w[Morpheus Cluracan Mervyn Gilbert Nuala].freeze
|
6
|
+
raise ArgumentError, "DreamTest needs at least as many subjects as dreamers" if SUBJECTS.size < DREAMER_NAMES.size
|
7
|
+
end
|
8
|
+
|
9
|
+
RSpec.describe Dream do
|
10
|
+
it "adds and selects all dreams and dreamers" do
|
11
|
+
expect(Dream.count).to eq(0)
|
12
|
+
DreamTest::SUBJECTS.shuffle.each { |subject| Dream.new(subject: subject).save! }
|
13
|
+
expect(Dream.all.to_a.size).to eq(DreamTest::SUBJECTS.size)
|
14
|
+
DreamTest::DREAMER_NAMES.shuffle.each do |name|
|
15
|
+
dream = Dream.find_by(subject: DreamTest::SUBJECTS.sample)
|
16
|
+
Dreamer.new(name: name, dream: dream).save!
|
17
|
+
end
|
18
|
+
expect(Dreamer.all.to_a.size).to eq(DreamTest::DREAMER_NAMES.size)
|
19
|
+
ensure
|
20
|
+
Dreamer.delete_all
|
21
|
+
Dream.delete_all
|
22
|
+
end
|
23
|
+
|
24
|
+
it "deletes some dreams and dreamers" do
|
25
|
+
expect(Dream.count).to eq(0)
|
26
|
+
DreamTest::SUBJECTS.shuffle.each { |subject| Dream.new(subject: subject).save! }
|
27
|
+
expect(Dream.all.to_a.size).to eq(DreamTest::SUBJECTS.size)
|
28
|
+
|
29
|
+
one_dream_subject_per_dreamer = DreamTest::SUBJECTS.shuffle
|
30
|
+
DreamTest::DREAMER_NAMES.shuffle.each do |dreamer_name|
|
31
|
+
expect(one_dream_subject_per_dreamer.size).to(satisfy { |v| v > 0 })
|
32
|
+
dream = Dream.find_by(subject: one_dream_subject_per_dreamer.shift)
|
33
|
+
Dreamer.new(name: dreamer_name, dream: dream).save!
|
34
|
+
end
|
35
|
+
expect(Dreamer.all.to_a.size).to eq(DreamTest::DREAMER_NAMES.size)
|
36
|
+
expect(Dream.all.pluck(:dreamer_id).compact.size).to eq(DreamTest::DREAMER_NAMES.size)
|
37
|
+
|
38
|
+
sample_dreamer = Dreamer.find_by(name: DreamTest::DREAMER_NAMES.sample)
|
39
|
+
expect(Dream.where(dreamer: sample_dreamer).to_a.size).to eq(1)
|
40
|
+
Dream.where(dreamer: sample_dreamer).delete_all
|
41
|
+
expect(Dream.all.size).to eq(DreamTest::SUBJECTS.size - 1)
|
42
|
+
ensure
|
43
|
+
Dreamer.delete_all
|
44
|
+
Dream.delete_all
|
45
|
+
end
|
46
|
+
end
|
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
RSpec.describe "model_indexes_books" do
|
4
4
|
it "randomly selects from books with no order" do
|
5
|
-
expect(Book.all.to_sql).to end_with("ORDER BY RANDOM()")
|
5
|
+
expect(Book.all.to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
6
6
|
end
|
7
7
|
|
8
8
|
it "randomly selects from books ordered by nonindexed column" do
|
9
|
-
expect(Book.all.order(:subject).to_sql).to end_with('ORDER BY "books"."subject" ASC, RANDOM()')
|
9
|
+
expect(Book.all.order(:subject).to_sql).to end_with(adapter_text('ORDER BY "books"."subject" ASC, RANDOM()'))
|
10
10
|
end
|
11
11
|
|
12
12
|
it "randomly selects from books ordered by unique column" do
|
13
|
-
expect(Book.all.order(:isbn).to_sql).to end_with('ORDER BY "books"."isbn" ASC, RANDOM()')
|
13
|
+
expect(Book.all.order(:isbn).to_sql).to end_with(adapter_text('ORDER BY "books"."isbn" ASC, RANDOM()'))
|
14
14
|
end
|
15
15
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
RSpec.describe "model_indexes_cats" do
|
4
4
|
it "randomly selects from cats" do
|
5
|
-
expect(Cat.all.to_sql).to end_with("ORDER BY RANDOM()")
|
5
|
+
expect(Cat.all.to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
6
6
|
end
|
7
7
|
|
8
8
|
it "nonrandomly selects from cats by implied primary key descending" do
|
9
|
-
expect(Cat.all.order(id: :desc).to_sql).to end_with('ORDER BY "cats"."id" DESC')
|
9
|
+
expect(Cat.all.order(id: :desc).to_sql).to end_with(adapter_text('ORDER BY "cats"."id" DESC'))
|
10
10
|
end
|
11
11
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
RSpec.describe "model_indexes_dreams" do
|
4
4
|
it "randomly selects from dreams ordered by nonindexed column" do
|
5
|
-
expect(Dream.all.order(:subject).to_sql).to end_with('ORDER BY "dreams"."subject" ASC, RANDOM()')
|
5
|
+
expect(Dream.all.order(:subject).to_sql).to end_with(adapter_text('ORDER BY "dreams"."subject" ASC, RANDOM()'))
|
6
6
|
end
|
7
7
|
|
8
8
|
it "nonrandomly selects from dreams by explicit primary key" do
|
9
|
-
expect(Dream.all.order(:dream_id).to_sql).to end_with('ORDER BY "dreams"."dream_id" ASC')
|
9
|
+
expect(Dream.all.order(:dream_id).to_sql).to end_with(adapter_text('ORDER BY "dreams"."dream_id" ASC'))
|
10
10
|
end
|
11
11
|
end
|
@@ -2,32 +2,32 @@
|
|
2
2
|
|
3
3
|
RSpec.describe "model_indexes_shelves" do
|
4
4
|
it "randomly selects from shelves" do
|
5
|
-
expect(Shelf.all.to_sql).to end_with("ORDER BY RANDOM()")
|
5
|
+
expect(Shelf.all.to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
6
6
|
end
|
7
7
|
|
8
8
|
it "randomly selects from some shelves" do
|
9
|
-
expect(Shelf.where(contents: "foo").to_sql).to end_with("ORDER BY RANDOM()")
|
9
|
+
expect(Shelf.where(contents: "foo").to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
10
10
|
end
|
11
11
|
|
12
12
|
it "randomly selects from shelves ordered by id" do
|
13
|
-
expect(Shelf.order(:shelf_id).to_sql).to end_with('ORDER BY "shelves"."shelf_id" ASC, RANDOM()')
|
13
|
+
expect(Shelf.order(:shelf_id).to_sql).to end_with(adapter_text('ORDER BY "shelves"."shelf_id" ASC, RANDOM()'))
|
14
14
|
end
|
15
15
|
|
16
16
|
it "randomly selects from shelves ordered by position" do
|
17
17
|
expect(Shelf.order(:shelf_position).to_sql).to end_with(
|
18
|
-
'ORDER BY "shelves"."shelf_position" ASC, RANDOM()'
|
18
|
+
adapter_text('ORDER BY "shelves"."shelf_position" ASC, RANDOM()')
|
19
19
|
)
|
20
20
|
end
|
21
21
|
|
22
22
|
it "nonrandomly selects from shelves ordered by id and position" do
|
23
23
|
expect(Shelf.order(:shelf_id, :shelf_position).to_sql).to end_with(
|
24
|
-
'ORDER BY "shelves"."shelf_id" ASC, "shelves"."shelf_position" ASC'
|
24
|
+
adapter_text('ORDER BY "shelves"."shelf_id" ASC, "shelves"."shelf_position" ASC')
|
25
25
|
)
|
26
26
|
end
|
27
27
|
|
28
28
|
it "nonrandomly selects from some shelves ordered by id and position" do
|
29
29
|
expect(Shelf.where(contents: "bar").order(:shelf_id, :shelf_position).to_sql).to end_with(
|
30
|
-
'ORDER BY "shelves"."shelf_id" ASC, "shelves"."shelf_position" ASC'
|
30
|
+
adapter_text('ORDER BY "shelves"."shelf_id" ASC, "shelves"."shelf_position" ASC')
|
31
31
|
)
|
32
32
|
end
|
33
33
|
end
|
data/spec/model_joins_spec.rb
CHANGED
@@ -6,22 +6,22 @@
|
|
6
6
|
|
7
7
|
RSpec.describe "model_indexes_joins" do
|
8
8
|
it "randomly selects from owner has_many cats" do
|
9
|
-
expect(Owner.joins(:cats).all.to_sql).to end_with("ORDER BY RANDOM()")
|
9
|
+
expect(Owner.joins(:cats).all.to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
10
10
|
end
|
11
11
|
|
12
12
|
it "randomly selects from owner has_many ordered cats" do
|
13
|
-
expect(Owner.joins(:cats).order("owners.id": :asc).all.to_sql).to end_with(", RANDOM()")
|
14
|
-
expect(Owner.joins(:cats).order(:"cats.id").all.to_sql).to end_with(", RANDOM()")
|
15
|
-
expect(Owner.joins(:cats).order(:"owners.id", "cats.id": :desc).all.to_sql).to end_with(", RANDOM()")
|
16
|
-
expect(Owner.joins(:cats).order(:"owners.id", :"cats.name").all.to_sql).to end_with(", RANDOM()")
|
13
|
+
expect(Owner.joins(:cats).order("owners.id": :asc).all.to_sql).to end_with(adapter_text(", RANDOM()"))
|
14
|
+
expect(Owner.joins(:cats).order(:"cats.id").all.to_sql).to end_with(adapter_text(", RANDOM()"))
|
15
|
+
expect(Owner.joins(:cats).order(:"owners.id", "cats.id": :desc).all.to_sql).to end_with(adapter_text(", RANDOM()"))
|
16
|
+
expect(Owner.joins(:cats).order(:"owners.id", :"cats.name").all.to_sql).to end_with(adapter_text(", RANDOM()"))
|
17
17
|
end
|
18
18
|
|
19
19
|
it "randomly selects from dreamer has_one dream" do
|
20
|
-
expect(Dreamer.joins(:dream).all.to_sql).to end_with("ORDER BY RANDOM()")
|
20
|
+
expect(Dreamer.joins(:dream).all.to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
21
21
|
end
|
22
22
|
|
23
23
|
it "randomly selects from dreamer has_one ordered dream" do
|
24
|
-
expect(Dreamer.joins(:dream).order("dreamers.id": :desc).all.to_sql).to end_with(", RANDOM()")
|
25
|
-
expect(Dreamer.joins(:dream).order(:"dreams.id").all.to_sql).to end_with(", RANDOM()")
|
24
|
+
expect(Dreamer.joins(:dream).order("dreamers.id": :desc).all.to_sql).to end_with(adapter_text(", RANDOM()"))
|
25
|
+
expect(Dreamer.joins(:dream).order(:"dreams.id").all.to_sql).to end_with(adapter_text(", RANDOM()"))
|
26
26
|
end
|
27
27
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Cat do
|
4
|
+
it "randomly selects distinctly except on postgres" do
|
5
|
+
expect(Cat.distinct.all.to_sql).to end_with(
|
6
|
+
case UnreliableTest.find_adapter
|
7
|
+
when "postgresql"
|
8
|
+
' FROM "cats"'
|
9
|
+
else
|
10
|
+
adapter_text("ORDER BY RANDOM()")
|
11
|
+
end
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "randomly selects distinctly from some" do
|
16
|
+
expect(Cat.where(name: "foo").distinct.to_sql).to end_with(
|
17
|
+
case UnreliableTest.find_adapter
|
18
|
+
when "postgresql"
|
19
|
+
%q( "cats"."name" = 'foo')
|
20
|
+
else
|
21
|
+
adapter_text("ORDER BY RANDOM()")
|
22
|
+
end
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "adds randomness to existing distinct order" do
|
27
|
+
expect(Cat.order(:name).distinct.to_sql).to end_with(
|
28
|
+
case UnreliableTest.find_adapter
|
29
|
+
when "postgresql"
|
30
|
+
' ORDER BY "cats"."name" ASC'
|
31
|
+
else
|
32
|
+
adapter_text('ORDER BY "cats"."name" ASC, RANDOM()')
|
33
|
+
end
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "executes a distinct" do
|
38
|
+
expect(Cat.distinct.count).to eq(0)
|
39
|
+
Cat.new(name: "Chet").save!
|
40
|
+
Cat.new(name: "Cab").save!
|
41
|
+
Cat.new(name: "Oscar").save!
|
42
|
+
Cat.new(name: "Chet").save!
|
43
|
+
expect(Cat.select(:name).to_a.size).to eq(4)
|
44
|
+
expect(Cat.select(:name).distinct.to_a.size).to eq(3)
|
45
|
+
ensure
|
46
|
+
Cat.delete_all
|
47
|
+
end
|
48
|
+
end
|
data/spec/model_select_spec.rb
CHANGED
@@ -2,21 +2,21 @@
|
|
2
2
|
|
3
3
|
RSpec.describe Cat do
|
4
4
|
it "randomly selects from all" do
|
5
|
-
expect(Cat.all.to_sql).to end_with("ORDER BY RANDOM()")
|
5
|
+
expect(Cat.all.to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
6
6
|
end
|
7
7
|
|
8
8
|
it "randomly selects from some" do
|
9
|
-
expect(Cat.where(name: "foo").to_sql).to end_with("ORDER BY RANDOM()")
|
9
|
+
expect(Cat.where(name: "foo").to_sql).to end_with(adapter_text("ORDER BY RANDOM()"))
|
10
10
|
end
|
11
11
|
|
12
12
|
it "adds randomness to existing order" do
|
13
|
-
expect(Cat.order(:name).to_sql).to end_with('ORDER BY "cats"."name" ASC, RANDOM()')
|
13
|
+
expect(Cat.order(:name).to_sql).to end_with(adapter_text('ORDER BY "cats"."name" ASC, RANDOM()'))
|
14
14
|
end
|
15
15
|
|
16
16
|
it "respects a disable block" do
|
17
17
|
Unreliable::Config.disable do
|
18
|
-
expect(Cat.where(name: "foo").to_sql).to_not end_with("ORDER BY RANDOM()")
|
19
|
-
expect(Cat.where(name: "foo").to_sql).to end_with(%q("cats"."name" = 'foo'))
|
18
|
+
expect(Cat.where(name: "foo").to_sql).to_not end_with(adapter_text("ORDER BY RANDOM()"))
|
19
|
+
expect(Cat.where(name: "foo").to_sql).to end_with(adapter_text(%q("cats"."name" = 'foo')))
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
data/spec/model_subquery_spec.rb
CHANGED
@@ -4,7 +4,7 @@ RSpec.describe Cat do
|
|
4
4
|
it "randomly selects in main query and subquery" do
|
5
5
|
# rubocop:disable Layout/SpaceInsideParens,Layout/DotPosition
|
6
6
|
expect( Cat.where(name: Cat.where(name: "foo")).to_sql ).
|
7
|
-
to end_with( %q[WHERE "cats"."name" = 'foo' ORDER BY RANDOM()) ORDER BY RANDOM()] )
|
7
|
+
to end_with(adapter_text( %q[WHERE "cats"."name" = 'foo' ORDER BY RANDOM()) ORDER BY RANDOM()] ))
|
8
8
|
# rubocop:enable Layout/SpaceInsideParens,Layout/DotPosition
|
9
9
|
end
|
10
10
|
end
|
@@ -4,6 +4,15 @@
|
|
4
4
|
# by build_arel. It returns an Arel::UpdateManager. This makes sure that internal call
|
5
5
|
# assembles the update query correctly.
|
6
6
|
|
7
|
+
# For three of the four tests in this file, we special-case MySQL to check for the
|
8
|
+
# different query it produces. The reason MySQL is handled differently is described
|
9
|
+
# in Arel 10 here:
|
10
|
+
# https://github.com/rails/rails/blob/v7.1.2/activerecord/lib/arel/visitors/to_sql.rb#L924-L942
|
11
|
+
# As it says, MySQL forbids using the *same* table in a subquery UPDATE:
|
12
|
+
# https://dev.mysql.com/doc/refman/8.0/en/subquery-errors.html
|
13
|
+
# Different tables are allowed though, as in our "update cats where id in (select owners..."
|
14
|
+
# test below.
|
15
|
+
|
7
16
|
module Unreliable
|
8
17
|
class SqlTestingData
|
9
18
|
class_attribute :update_manager_sql
|
@@ -11,7 +20,7 @@ module Unreliable
|
|
11
20
|
end
|
12
21
|
|
13
22
|
RSpec.describe "update_manager" do
|
14
|
-
it "in ActiveRecord >= 7, updates by subquery with select
|
23
|
+
it "in ActiveRecord >= 7, updates by subquery with select",
|
15
24
|
skip: ((ActiveRecord::VERSION::MAJOR < 7) ? "test is for ActiveRecord >= 7 only" : false) do
|
16
25
|
module Arel
|
17
26
|
class SelectManager
|
@@ -24,12 +33,66 @@ RSpec.describe "update_manager" do
|
|
24
33
|
alias_method :compile_update, :testing_compile_update
|
25
34
|
end
|
26
35
|
end
|
27
|
-
|
28
|
-
|
36
|
+
|
37
|
+
# rubocop:disable Layout/SpaceInsideParens,Layout/DotPosition
|
38
|
+
|
39
|
+
# Single subquery for sqlite/postgresql:
|
40
|
+
# "update cats where id in (select cats where name=bar)"
|
41
|
+
# Direct update for mysql:
|
42
|
+
# "update cats where name=bar"
|
43
|
+
|
29
44
|
Cat.where(name: "foo").update_all(name: "bar")
|
30
|
-
expect(Unreliable::SqlTestingData.update_manager_sql).
|
31
|
-
|
32
|
-
|
45
|
+
expect(Unreliable::SqlTestingData.update_manager_sql).
|
46
|
+
to end_with(
|
47
|
+
case UnreliableTest.find_adapter
|
48
|
+
when "mysql2"
|
49
|
+
"ORDER BY RAND()"
|
50
|
+
else
|
51
|
+
adapter_text("ORDER BY RANDOM())")
|
52
|
+
end
|
53
|
+
)
|
54
|
+
|
55
|
+
# Double-nested subquery for sqlite/postgresql:
|
56
|
+
# "update cats where id in (select cats where id in (select owners where name=baz))"
|
57
|
+
# Single-nested for mysql:
|
58
|
+
# "update cats where id in (select owners where name=baz)"
|
59
|
+
|
60
|
+
Cat.where( id: Owner.where(name: "bar") ).update_all(name: "baz")
|
61
|
+
expect(Unreliable::SqlTestingData.update_manager_sql).
|
62
|
+
to end_with(
|
63
|
+
case UnreliableTest.find_adapter
|
64
|
+
when "mysql2"
|
65
|
+
"ORDER BY RAND()"
|
66
|
+
else
|
67
|
+
adapter_text("ORDER BY RANDOM()) ORDER BY RANDOM())")
|
68
|
+
end
|
69
|
+
)
|
70
|
+
|
71
|
+
# Single ordered subquery for sqlite/postgresql:
|
72
|
+
# "update cats where id in (select cats where name=bar limit ?)"
|
73
|
+
# Direct update for mysql:
|
74
|
+
# "update cats where name=baz"
|
75
|
+
|
76
|
+
Cat.where(name: "bar").limit(1).update_all(name: "baz")
|
77
|
+
expect(Unreliable::SqlTestingData.update_manager_sql).
|
78
|
+
to match(
|
79
|
+
case UnreliableTest.find_adapter
|
80
|
+
when "mysql2"
|
81
|
+
'ORDER BY RAND\(\) LIMIT '
|
82
|
+
else
|
83
|
+
adapter_text('ORDER BY RANDOM\(\) LIMIT ')
|
84
|
+
end
|
85
|
+
)
|
86
|
+
|
87
|
+
# Single ordered subquery:
|
88
|
+
# "update cats where id in (select cats where name=bar order by id limit ?)"
|
89
|
+
# The presence of the primary-key order means Unreliable does not apply its own order.
|
90
|
+
|
91
|
+
Cat.where(name: "bar").order(:id).limit(1).update_all(name: "baz")
|
92
|
+
expect(Unreliable::SqlTestingData.update_manager_sql).
|
93
|
+
to match(adapter_text('ORDER BY "cats"\."id" ASC LIMIT '))
|
94
|
+
|
95
|
+
# rubocop:enable Layout/SpaceInsideParens,Layout/DotPosition
|
33
96
|
ensure
|
34
97
|
module Arel
|
35
98
|
class SelectManager
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
class UnreliableTest
|
4
|
+
DEFAULT_ADAPTER = "sqlite"
|
5
|
+
VALID_ADAPTERS = %w[mysql2 postgresql sqlite].freeze
|
6
|
+
ORIG_EXTENSION = "orig"
|
7
|
+
DATABASE_YML_FILENAME = "spec/internal/config/database.yml"
|
8
|
+
|
9
|
+
def self.find_adapter
|
10
|
+
ENV["RSPEC_ADAPTER"].presence || ::UnreliableTest::DEFAULT_ADAPTER
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.assert_valid_adapter!(adapter)
|
14
|
+
raise "RSPEC_ADAPTER '#{adapter}' not valid" unless ::UnreliableTest::VALID_ADAPTERS.include? adapter
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.cp_adapter_file(adapter)
|
18
|
+
FileUtils.cp(
|
19
|
+
"#{::UnreliableTest::DATABASE_YML_FILENAME}.#{adapter}",
|
20
|
+
::UnreliableTest::DATABASE_YML_FILENAME
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.restore_adapter_file
|
25
|
+
cp_adapter_file(::UnreliableTest::ORIG_EXTENSION)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
3
29
|
require "bundler"
|
4
30
|
|
5
31
|
Bundler.require :default, :development
|
@@ -11,6 +37,52 @@ if ActiveRecord.gem_version >= Gem::Version.new("5.2") && ActiveRecord.gem_versi
|
|
11
37
|
ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
|
12
38
|
end
|
13
39
|
|
40
|
+
if ActiveRecord.gem_version >= Gem::Version.new("6.1") && ActiveRecord.gem_version < Gem::Version.new("7.1")
|
41
|
+
# This causes all Rails deprecation warnings to raise.
|
42
|
+
# We would like to use this feature all the time, but it was only introduced in 6.1,
|
43
|
+
# and combustion <= 1.3.7 throws a deprecation in Rails 7.1. The next release of
|
44
|
+
# combustion should fix it: https://github.com/pat/combustion/pull/131
|
45
|
+
ActiveSupport::Deprecation.disallowed_warnings = :all
|
46
|
+
end
|
47
|
+
|
48
|
+
if ActiveRecord.gem_version >= Gem::Version.new("5.2") && ActiveRecord.gem_version < Gem::Version.new("6.1")
|
49
|
+
# This setting was introduced in Rails 5.2, deprecated in Rails 6.1, and
|
50
|
+
# removed in Rails 7.0.
|
51
|
+
ActiveRecord::Base.allow_unsafe_raw_sql = :disabled
|
52
|
+
end
|
53
|
+
|
54
|
+
# Convert the sqlite3 version of the text that each test is `expect`ing to see,
|
55
|
+
# into the text that the adapter would produce.
|
56
|
+
|
57
|
+
def adapter_text(sql)
|
58
|
+
case ActiveRecord::Base.connection.adapter_name
|
59
|
+
when "Mysql2"
|
60
|
+
sql.tr('"', "`").gsub("RANDOM()", "RAND()")
|
61
|
+
when "pg"
|
62
|
+
sql.tr('"', "`")
|
63
|
+
else
|
64
|
+
sql
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# ActiveRecord checks textual .order() arguments to ensure they match the adapter.
|
69
|
+
# This converts our test's text to match. See spec/textual_order_spec.rb for more.
|
70
|
+
|
71
|
+
def order_text(sql)
|
72
|
+
case ActiveRecord::Base.connection.adapter_name
|
73
|
+
when "Mysql2"
|
74
|
+
sql.tr('"', "`")
|
75
|
+
else
|
76
|
+
sql
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set the adapter for this run by copying the appropriate file into place.
|
81
|
+
adapter = UnreliableTest.find_adapter
|
82
|
+
UnreliableTest.assert_valid_adapter!(adapter)
|
83
|
+
UnreliableTest.cp_adapter_file(adapter)
|
84
|
+
puts "Running RSpec for #{adapter} on ActiveRecord #{ActiveRecord.version} on ruby #{RUBY_VERSION}"
|
85
|
+
|
14
86
|
Combustion.initialize! :active_record
|
15
87
|
|
16
88
|
RSpec.configure do |config|
|
@@ -25,5 +97,10 @@ RSpec.configure do |config|
|
|
25
97
|
config.shared_context_metadata_behavior = :apply_to_host_groups
|
26
98
|
config.example_status_persistence_file_path = "spec/examples.txt"
|
27
99
|
config.warnings = true
|
100
|
+
config.raise_errors_for_deprecations!
|
28
101
|
config.default_formatter = "doc" if config.files_to_run.count == 1
|
102
|
+
|
103
|
+
config.after(:suite) do
|
104
|
+
UnreliableTest.restore_adapter_file
|
105
|
+
end
|
29
106
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# These are funny tests written in funny ways, using the utility function
|
4
|
+
# `order_text` -- which is used only here -- and here's why.
|
5
|
+
#
|
6
|
+
# Start by reading the docs for `order`, especially the "strings" and "Arel"
|
7
|
+
# sections:
|
8
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-order
|
9
|
+
#
|
10
|
+
# The docs claim "only strings composed of plain column names" are allowed,
|
11
|
+
# but "plain" (since at least Rails 5.2) has allowed a table prefix as well, e.g.:
|
12
|
+
# https://github.com/rails/rails/blob/v6.0.0/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb#L171-L194
|
13
|
+
#
|
14
|
+
# To test non-quoted column and/or table names, across all three supported
|
15
|
+
# databases, we have to use `order_text` to convert the string specifying
|
16
|
+
# the order into whichever format the current adapter is expecting. What
|
17
|
+
# this means is that in the tests where we quote table and/or column name,
|
18
|
+
# the MySQL adapter will require the table and column name quoted with
|
19
|
+
# `backticks` and the other two with "quotes".
|
20
|
+
#
|
21
|
+
# To make this work, `order_text` ensures we send the adapter the correct format.
|
22
|
+
# Then `adapter_text`, as usual, makes sure we check the result in the right way.
|
23
|
+
#
|
24
|
+
# This test is to ensure that unreliable works even when an app writes an order
|
25
|
+
# in this not-well-documented way. So here we want to send Shelf.order() a
|
26
|
+
# fully-qualified textual order, i.e., an order that (unnecessarily, as it happens)
|
27
|
+
# specifies table name along with column name.
|
28
|
+
#
|
29
|
+
# We do this with spec_helper.rb having set the strictest setting for raw sql,
|
30
|
+
# "ActiveRecord::Base.allow_unsafe_raw_sql = :disabled", in the ActiveRecord
|
31
|
+
# versions where that's available -- which is only 5.2 and 6.0. This may have
|
32
|
+
# been an ill-advised setting because it makes ordering by quoted column
|
33
|
+
# and/or table names raise an UnknownAttributeReference error only in 5.2.
|
34
|
+
# Maybe this unexpected change is why this feature was quickly deprecated and
|
35
|
+
# removed. We test 5.2 separately from other verions.
|
36
|
+
|
37
|
+
RSpec.describe "textual order raw" do
|
38
|
+
it "randomly selects from shelves ordered by Arel-escaped quoted table and column name" do
|
39
|
+
expect(Shelf.order(Arel.sql(order_text('"shelves"."shelf_id"'))).to_sql).to end_with(
|
40
|
+
adapter_text('ORDER BY "shelves"."shelf_id", RANDOM()')
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "randomly selects from shelves ordered by Arel-escaped quoted column name" do
|
45
|
+
expect(Shelf.order(Arel.sql(order_text('"shelf_id"'))).to_sql).to end_with(
|
46
|
+
adapter_text('ORDER BY "shelf_id", RANDOM()')
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "raises (in 5.2) on non-Arel-escaped quoted table and column name",
|
51
|
+
skip: (
|
52
|
+
(ActiveRecord.version < Gem::Version.new("5.2") || ActiveRecord.version >= Gem::Version.new("6.0")
|
53
|
+
) ? "test is for ActiveRecord 5.2 only" : false) do
|
54
|
+
# It actually raises ActiveRecord::UnknownAttributeReference, but since
|
55
|
+
# that's not defined in Rails < 5.2, and there's a chance we might
|
56
|
+
# restore Rails 5.0 and 5.1 compatibility at some point in the future,
|
57
|
+
# we don't want to reference that constant if it might not exist.
|
58
|
+
# So we name that class by its superclass here.
|
59
|
+
expect { Shelf.order(order_text('"shelves"."shelf_id"')).to_sql }.to raise_error(ActiveRecord::ActiveRecordError)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "raises (in 5.2) on non-Arel-escaped quoted column name",
|
63
|
+
skip: (
|
64
|
+
(ActiveRecord.version < Gem::Version.new("5.2") || ActiveRecord.version >= Gem::Version.new("6.0")
|
65
|
+
) ? "test is for ActiveRecord 5.2 only" : false) do
|
66
|
+
expect { Shelf.order(order_text('"shelf_id"')).to_sql }.to raise_error(ActiveRecord::ActiveRecordError)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "randomly selects (except in 5.2) on non-Arel-escaped quoted table and column name",
|
70
|
+
skip: (
|
71
|
+
(ActiveRecord.version >= Gem::Version.new("5.2") && ActiveRecord.version < Gem::Version.new("6.0")
|
72
|
+
) ? "test is not for ActiveRecord 5.2" : false) do
|
73
|
+
expect(Shelf.order(order_text('"shelves"."shelf_id"')).to_sql).to end_with(
|
74
|
+
adapter_text('ORDER BY "shelves"."shelf_id", RANDOM()')
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "randomly selects (except in 5.2) on non-Arel-escaped quoted column name",
|
79
|
+
skip: (
|
80
|
+
(ActiveRecord.version >= Gem::Version.new("5.2") && ActiveRecord.version < Gem::Version.new("6.0")
|
81
|
+
) ? "test is not for ActiveRecord 5.2" : false) do
|
82
|
+
expect(Shelf.order(order_text('"shelf_id"')).to_sql).to end_with(
|
83
|
+
adapter_text('ORDER BY "shelf_id", RANDOM()')
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "randomly selects from shelves ordered by non-Arel-escaped unquoted table and column name" do
|
88
|
+
expect(Shelf.order("shelves.shelf_id").to_sql).to end_with(
|
89
|
+
adapter_text("ORDER BY shelves.shelf_id, RANDOM()")
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "randomly selects from shelves ordered by non-Arel-escaped unquoted column name" do
|
94
|
+
expect(Shelf.order("shelf_id").to_sql).to end_with(
|
95
|
+
adapter_text("ORDER BY shelf_id, RANDOM()")
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
data/spec/textual_order_spec.rb
CHANGED
@@ -2,34 +2,38 @@
|
|
2
2
|
|
3
3
|
RSpec.describe "textual order" do
|
4
4
|
it "randomly selects from shelves ordered by textual id asc" do
|
5
|
-
expect(Shelf.order("shelf_id ASC").to_sql).to end_with(
|
6
|
-
|
7
|
-
|
8
|
-
it "randomly selects from shelves ordered by fully qualified textual id asc" do
|
9
|
-
expect(Shelf.order('"shelves"."shelf_id"').to_sql).to end_with('ORDER BY "shelves"."shelf_id", RANDOM()')
|
5
|
+
expect(Shelf.order("shelf_id ASC").to_sql).to end_with(
|
6
|
+
adapter_text("ORDER BY shelf_id ASC, RANDOM()")
|
7
|
+
)
|
10
8
|
end
|
11
9
|
|
12
10
|
it "randomly selects from shelves ordered by textual position asc" do
|
13
|
-
expect(Shelf.order("shelf_position ASC").to_sql).to end_with(
|
11
|
+
expect(Shelf.order("shelf_position ASC").to_sql).to end_with(
|
12
|
+
adapter_text("ORDER BY shelf_position ASC, RANDOM()")
|
13
|
+
)
|
14
14
|
end
|
15
15
|
|
16
16
|
it "randomly selects from shelves ordered by textual id desc" do
|
17
|
-
expect(Shelf.order("shelf_id DESC").to_sql).to end_with(
|
17
|
+
expect(Shelf.order("shelf_id DESC").to_sql).to end_with(
|
18
|
+
adapter_text("ORDER BY shelf_id DESC, RANDOM()")
|
19
|
+
)
|
18
20
|
end
|
19
21
|
|
20
22
|
it "randomly selects from shelves ordered by textual position desc" do
|
21
|
-
expect(Shelf.order("shelf_position DESC").to_sql).to end_with(
|
23
|
+
expect(Shelf.order("shelf_position DESC").to_sql).to end_with(
|
24
|
+
adapter_text("ORDER BY shelf_position DESC, RANDOM()")
|
25
|
+
)
|
22
26
|
end
|
23
27
|
|
24
28
|
it "randomly selects from shelves ordered by textual id and position asc" do
|
25
29
|
expect(Shelf.order("shelf_id ASC, shelf_position ASC").to_sql).to end_with(
|
26
|
-
"ORDER BY shelf_id ASC, shelf_position ASC, RANDOM()"
|
30
|
+
adapter_text("ORDER BY shelf_id ASC, shelf_position ASC, RANDOM()")
|
27
31
|
)
|
28
32
|
end
|
29
33
|
|
30
34
|
it "randomly selects from shelves ordered by textual id and position desc" do
|
31
35
|
expect(Shelf.order("shelf_id DESC, shelf_position DESC").to_sql).to end_with(
|
32
|
-
"ORDER BY shelf_id DESC, shelf_position DESC, RANDOM()"
|
36
|
+
adapter_text("ORDER BY shelf_id DESC, shelf_position DESC, RANDOM()")
|
33
37
|
)
|
34
38
|
end
|
35
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unreliable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- James McCarthy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,7 +16,7 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '5.
|
19
|
+
version: '5.2'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
22
|
version: '8.0'
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '5.
|
29
|
+
version: '5.2'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: '8.0'
|
@@ -36,7 +36,7 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '5.
|
39
|
+
version: '5.2'
|
40
40
|
- - "<"
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
version: '8.0'
|
@@ -46,7 +46,7 @@ dependencies:
|
|
46
46
|
requirements:
|
47
47
|
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
|
-
version: '5.
|
49
|
+
version: '5.2'
|
50
50
|
- - "<"
|
51
51
|
- !ruby/object:Gem::Version
|
52
52
|
version: '8.0'
|
@@ -92,6 +92,34 @@ dependencies:
|
|
92
92
|
- - "~>"
|
93
93
|
- !ruby/object:Gem::Version
|
94
94
|
version: '1.3'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: mysql2
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0.5'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0.5'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: pg
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - "~>"
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '1.5'
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - "~>"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '1.5'
|
95
123
|
- !ruby/object:Gem::Dependency
|
96
124
|
name: rake
|
97
125
|
requirement: !ruby/object:Gem::Requirement
|
@@ -126,14 +154,14 @@ dependencies:
|
|
126
154
|
requirements:
|
127
155
|
- - "~>"
|
128
156
|
- !ruby/object:Gem::Version
|
129
|
-
version:
|
157
|
+
version: 1.6.9
|
130
158
|
type: :development
|
131
159
|
prerelease: false
|
132
160
|
version_requirements: !ruby/object:Gem::Requirement
|
133
161
|
requirements:
|
134
162
|
- - "~>"
|
135
163
|
- !ruby/object:Gem::Version
|
136
|
-
version:
|
164
|
+
version: 1.6.9
|
137
165
|
- !ruby/object:Gem::Dependency
|
138
166
|
name: standard
|
139
167
|
requirement: !ruby/object:Gem::Requirement
|
@@ -148,6 +176,20 @@ dependencies:
|
|
148
176
|
- - "~>"
|
149
177
|
- !ruby/object:Gem::Version
|
150
178
|
version: '1.17'
|
179
|
+
- !ruby/object:Gem::Dependency
|
180
|
+
name: yamllint
|
181
|
+
requirement: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - "~>"
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: 0.0.9
|
186
|
+
type: :development
|
187
|
+
prerelease: false
|
188
|
+
version_requirements: !ruby/object:Gem::Requirement
|
189
|
+
requirements:
|
190
|
+
- - "~>"
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: 0.0.9
|
151
193
|
description: |
|
152
194
|
Unreliable helps uncover bugs in Rails apps that rely on ambiguous database ordering.
|
153
195
|
Installing it makes both your app and your test suite more robust.
|
@@ -167,19 +209,23 @@ files:
|
|
167
209
|
- lib/unreliable/config.rb
|
168
210
|
- lib/unreliable/railtie.rb
|
169
211
|
- lib/unreliable/version.rb
|
212
|
+
- spec/adapter_option_spec.rb
|
170
213
|
- spec/env_spec.rb
|
171
|
-
- spec/
|
214
|
+
- spec/execute_queries_spec.rb
|
215
|
+
- spec/execute_subqueries_spec.rb
|
172
216
|
- spec/model_cache_versioning_spec.rb
|
173
217
|
- spec/model_indexes_books_spec.rb
|
174
218
|
- spec/model_indexes_cats_spec.rb
|
175
219
|
- spec/model_indexes_dreams_spec.rb
|
176
220
|
- spec/model_indexes_shelves_spec.rb
|
177
221
|
- spec/model_joins_spec.rb
|
222
|
+
- spec/model_select_distinct_spec.rb
|
178
223
|
- spec/model_select_spec.rb
|
179
224
|
- spec/model_subquery_spec.rb
|
180
225
|
- spec/model_update_arel_10_spec.rb
|
181
226
|
- spec/railtie_spec.rb
|
182
227
|
- spec/spec_helper.rb
|
228
|
+
- spec/textual_order_raw_spec.rb
|
183
229
|
- spec/textual_order_spec.rb
|
184
230
|
- spec/version_spec.rb
|
185
231
|
homepage: https://github.com/jamiemccarthy/unreliable
|
@@ -203,7 +249,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
249
|
- !ruby/object:Gem::Version
|
204
250
|
version: '0'
|
205
251
|
requirements: []
|
206
|
-
rubygems_version: 3.
|
252
|
+
rubygems_version: 3.5.3
|
207
253
|
signing_key:
|
208
254
|
specification_version: 4
|
209
255
|
summary: For ActiveRecord tests, surface ambiguous-ordering bugs
|
data/spec/examples.txt
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
example_id | status | run_time |
|
2
|
-
------------------------------------------ | ------ | --------------- |
|
3
|
-
./spec/env_spec.rb[1:1] | passed | 0.00346 seconds |
|
4
|
-
./spec/env_spec.rb[1:2] | passed | 0.00049 seconds |
|
5
|
-
./spec/model_cache_versioning_spec.rb[1:1] | passed | 0.03983 seconds |
|
6
|
-
./spec/model_indexes_books_spec.rb[1:1] | passed | 0.00152 seconds |
|
7
|
-
./spec/model_indexes_books_spec.rb[1:2] | passed | 0.00132 seconds |
|
8
|
-
./spec/model_indexes_books_spec.rb[1:3] | passed | 0.00108 seconds |
|
9
|
-
./spec/model_indexes_cats_spec.rb[1:1] | passed | 0.00105 seconds |
|
10
|
-
./spec/model_indexes_cats_spec.rb[1:2] | passed | 0.0015 seconds |
|
11
|
-
./spec/model_indexes_dreams_spec.rb[1:1] | passed | 0.00313 seconds |
|
12
|
-
./spec/model_indexes_dreams_spec.rb[1:2] | passed | 0.00129 seconds |
|
13
|
-
./spec/model_indexes_shelves_spec.rb[1:1] | passed | 0.00178 seconds |
|
14
|
-
./spec/model_indexes_shelves_spec.rb[1:2] | passed | 0.00174 seconds |
|
15
|
-
./spec/model_indexes_shelves_spec.rb[1:3] | passed | 0.00076 seconds |
|
16
|
-
./spec/model_indexes_shelves_spec.rb[1:4] | passed | 0.00087 seconds |
|
17
|
-
./spec/model_indexes_shelves_spec.rb[1:5] | passed | 0.00079 seconds |
|
18
|
-
./spec/model_indexes_shelves_spec.rb[1:6] | passed | 0.00101 seconds |
|
19
|
-
./spec/model_joins_spec.rb[1:1] | passed | 0.01262 seconds |
|
20
|
-
./spec/model_joins_spec.rb[1:2] | passed | 0.00359 seconds |
|
21
|
-
./spec/model_joins_spec.rb[1:3] | passed | 0.01319 seconds |
|
22
|
-
./spec/model_joins_spec.rb[1:4] | passed | 0.00217 seconds |
|
23
|
-
./spec/model_select_spec.rb[1:1] | passed | 0.0007 seconds |
|
24
|
-
./spec/model_select_spec.rb[1:2] | passed | 0.00093 seconds |
|
25
|
-
./spec/model_select_spec.rb[1:3] | passed | 0.00091 seconds |
|
26
|
-
./spec/model_select_spec.rb[1:4] | passed | 0.00127 seconds |
|
27
|
-
./spec/model_subquery_spec.rb[1:1] | passed | 0.00155 seconds |
|
28
|
-
./spec/model_update_arel_10_spec.rb[1:1] | passed | 0.00536 seconds |
|
29
|
-
./spec/railtie_spec.rb[1:1] | passed | 0.00013 seconds |
|
30
|
-
./spec/railtie_spec.rb[1:2] | passed | 0.00008 seconds |
|
31
|
-
./spec/textual_order_spec.rb[1:1] | passed | 0.00108 seconds |
|
32
|
-
./spec/textual_order_spec.rb[1:2] | passed | 0.00067 seconds |
|
33
|
-
./spec/textual_order_spec.rb[1:3] | passed | 0.00078 seconds |
|
34
|
-
./spec/textual_order_spec.rb[1:4] | passed | 0.00087 seconds |
|
35
|
-
./spec/textual_order_spec.rb[1:5] | passed | 0.00069 seconds |
|
36
|
-
./spec/textual_order_spec.rb[1:6] | passed | 0.00079 seconds |
|
37
|
-
./spec/textual_order_spec.rb[1:7] | passed | 0.00088 seconds |
|
38
|
-
./spec/version_spec.rb[1:1] | passed | 0.00151 seconds |
|
39
|
-
./spec/version_spec.rb[1:2] | passed | 0.0002 seconds |
|