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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b11225a3c1dad62d2a283c3eb36fef7b69fda61815df13a860ce9c1970016eb
4
- data.tar.gz: 1994dd9ea21ba01dc839c56cc08ca91742465dc9b850c0b6a5eebbc81c7558a2
3
+ metadata.gz: f3cf18102610d7a8a8c380f39bad81db77ed33392ecc8dffcdc99ef7c60d222e
4
+ data.tar.gz: b99100338674b31fbb11036b56ce1443006d1281e5641a0b4f200bbe7a22e832
5
5
  SHA512:
6
- metadata.gz: 145d5bbe8790fe4ee33bf4f0ea874350edf9655c2ebed35bfb3f18acb123d03efbe86c826dd0d9d4ca7ff12938c368e5cc4ebdc7afba224e0a8c12669a92e816
7
- data.tar.gz: 2a3ca2dfa53cbf67a0f8a6b7128df829874409a698586c15d10746624a71a18eddc77914768ec5da361541aa54706bd1899f473aa1f6093ec839f689428b36ed
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.1"
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. Maybe we ordered on timestamps, which are usually unique but sometimes not. And the test suite will stay silent as long as our database just happens to return the same order.
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
- 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.
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
- 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.
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. Either relax your test so it stops relying on order, or tighten up your app to specify order rigorously. (In my company's app, it was about 50/50.)
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. Specify order rigorously in your app.
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
- The problem in this example is easy to see because many books are published each year. But this error can occur at any time granularity.
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 to support Ruby 2.6 through 3.1, and Rails 5.0 through 7.0.
95
+ `unreliable` is tested on every valid combination of:
83
96
 
84
- As of November 2022, this is all released versions of both that are currently supported, plus several older releases.
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 `unreliable` in production, and there never will be.
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'd be frustrated when `unreliable` slows it down and changes its behavior, so don't install it (yet).
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, I'm pretty friendly.
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, against the different versions of ActiveRecord, use Ruby 2.7, the only version currently compatible with all the ActiveRecord versions supported. Install the required gems with:
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
- Run `unreliable`'s linter with:
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
- bundle exec standardrb
136
+ standardrb
126
137
  ```
127
138
 
128
- Then you can run `unreliable`'s tests with:
139
+ Run its tests in three separate passes:
129
140
 
130
141
  ```
131
- bundle exec appraisal rake
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.6` and a 2-line `Gemfile`: `source "https://rubygems.org"`, `gem "rails", "~> 7.0"`
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 with values that differ only by [character case](https://dev.mysql.com/doc/refman/8.0/en/sorting-rows.html)
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 ([12](https://www.postgresql.org/docs/12/sql-select.html#SQL-ORDERBY), [13](https://www.postgresql.org/docs/13/sql-select.html#SQL-ORDERBY), [14](https://www.postgresql.org/docs/14/sql-select.html#SQL-ORDERBY)):
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.39](https://www.sqlite.org/lang_select.html#the_order_by_clause)):
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/gem_tasks"
4
+
3
5
  begin
4
6
  require "rspec/core/rake_task"
5
7
 
@@ -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 Arel::Table.engine.connection.adapter_name
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/13/functions-math.html#FUNCTIONS-MATH-RANDOM-TABLE
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]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unreliable
4
- VERSION = "0.9.1"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "rspec adapter" do
4
+ it "is as specified" do
5
+ expect(ActiveRecord::Base.connection.adapter_name.downcase).to eq(UnreliableTest.find_adapter)
6
+ end
7
+ end
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
@@ -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
@@ -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
@@ -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 in random order",
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
- Cat.update_all(name: "bar")
28
- expect(Unreliable::SqlTestingData.update_manager_sql).to end_with("ORDER BY RANDOM())")
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).to end_with("ORDER BY RANDOM())")
31
- Cat.where(name: "bar").order(:id).update_all(name: "baz")
32
- expect(Unreliable::SqlTestingData.update_manager_sql).to end_with("ORDER BY \"cats\".\"id\" ASC)")
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
@@ -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("ORDER BY shelf_id ASC, RANDOM()")
6
- end
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("ORDER BY shelf_position ASC, RANDOM()")
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("ORDER BY shelf_id DESC, RANDOM()")
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("ORDER BY shelf_position DESC, RANDOM()")
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.9.1
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: 2022-11-21 00:00:00.000000000 Z
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.0'
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.0'
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.0'
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.0'
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: '1.4'
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: '1.4'
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/examples.txt
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.1.6
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 |