unreliable 0.10.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile +12 -0
- data/README.md +15 -9
- data/lib/unreliable/build_order.rb +24 -6
- data/lib/unreliable/config.rb +4 -4
- data/lib/unreliable/version.rb +1 -1
- data/spec/config_disable_nesting_spec.rb +24 -0
- data/spec/config_thread_safety_spec.rb +27 -0
- data/spec/env_spec.rb +2 -2
- data/spec/examples.txt +62 -0
- data/spec/execute_eager_load_spec.rb +109 -0
- data/spec/execute_find_each_spec.rb +49 -0
- data/spec/execute_pluck_exists_aggregate_spec.rb +48 -0
- data/spec/execute_subqueries_spec.rb +1 -1
- data/spec/model_first_last_spec.rb +28 -0
- data/spec/model_group_spec.rb +35 -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 +12 -6
- data/spec/model_internal_metadata_spec.rb +18 -0
- data/spec/model_joins_spec.rb +8 -8
- data/spec/model_limit_spec.rb +18 -0
- data/spec/model_or_spec.rb +31 -0
- data/spec/model_reorder_spec.rb +20 -0
- data/spec/model_select_distinct_spec.rb +10 -10
- data/spec/model_select_spec.rb +5 -5
- data/spec/model_subquery_spec.rb +7 -4
- data/spec/model_update_arel_10_spec.rb +22 -7
- data/spec/pluck_exists_aggregate_spec.rb +48 -0
- data/spec/spec_helper.rb +77 -14
- data/spec/textual_order_raw_spec.rb +18 -16
- data/spec/textual_order_spec.rb +6 -6
- data/spec/version_spec.rb +1 -1
- metadata +20 -150
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 17d0865e8524f14bc5d1ddc01106ccf63adb25f044a7ea3ca9109d44c6f31f3f
|
|
4
|
+
data.tar.gz: c3b5ff4213d9fb54bd3a48838ad0642c4a132e80715e45500ecf3c6884cd6f47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 461eb8a9445d48f09ff0b6e89f097fba52685f55e93637c1e2c6bc582104af44d3e30d8c503f10e145f1a3ef052f9fdaf2b4c9cb2544f3e71080f87ea2d9854a
|
|
7
|
+
data.tar.gz: 892d721b2ff0b45430625c1feae890ce0dbdc194dcab3118f8d20251af0eb0ee2c2d45bc4550bfd5ddbff334524c42c67b2d642a53709efa9c289e27a866f956
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## Unreliable 1.0.0 (April 18, 2026) ##
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
* Fixed crash on Rails 7.2+ caused by removal of `Arel::Table.engine` — replaced with `lease_connection` helper.
|
|
6
|
+
* `Config.disable` is now thread-safe — uses `Thread.current` instead of toggling a global flag, so parallel test runners don't interfere with each other.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
* SQL Server support — queries get `ORDER BY NEWID()`; DISTINCT queries are skipped (same rationale as PostgreSQL).
|
|
11
|
+
* Trilogy MySQL client support alongside `mysql2`.
|
|
12
|
+
* Rails 7.2, 8.0, and 8.1 compatibility; upper version bound raised to `< 9.0`.
|
|
13
|
+
* Ruby 3.4 and 4.0 added to the CI matrix.
|
|
14
|
+
* Many new specs: eager loading, `first`/`last`, `limit`, `reorder`, `group`/`having`, `or` queries, `pluck`/`exists?`/aggregates, `find_each`, and thread safety.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
* Development dependencies moved from gemspec to Gemfile.
|
|
19
|
+
* Suppressed C-extension deprecation warnings from sqlite3 1.3.x on Ruby 2.7 (#4).
|
|
20
|
+
|
|
1
21
|
## Unreliable 0.10.0 (January 15, 2024) ##
|
|
2
22
|
|
|
3
23
|
### Changed
|
data/Gemfile
CHANGED
|
@@ -3,3 +3,15 @@
|
|
|
3
3
|
source "https://rubygems.org"
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
|
+
|
|
7
|
+
gem "appraisal", "~> 2.4"
|
|
8
|
+
gem "bundler", "~> 2.1"
|
|
9
|
+
gem "combustion", "~> 1.5"
|
|
10
|
+
gem "mysql2", "~> 0.5"
|
|
11
|
+
gem "ostruct"
|
|
12
|
+
gem "pg", "~> 1.5"
|
|
13
|
+
gem "rake", "~> 13.0"
|
|
14
|
+
gem "rspec", "~> 3.0"
|
|
15
|
+
gem "sqlite3", (RUBY_VERSION >= "3.2") ? "~> 1.6.9" : "~> 1.5.4"
|
|
16
|
+
gem "standard", "~> 1.17"
|
|
17
|
+
gem "yamllint", "~> 0.0.9"
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@ group :test do
|
|
|
19
19
|
end
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
And run `bundle install`. Then try running your test suite. If it emits new errors and failures, great!
|
|
23
23
|
|
|
24
24
|
## The problem with orders
|
|
25
25
|
|
|
@@ -94,9 +94,11 @@ But this error can occur at any granularity, in time or other data types.
|
|
|
94
94
|
|
|
95
95
|
`unreliable` is tested on every valid combination of:
|
|
96
96
|
|
|
97
|
-
* sqlite, postgresql, and
|
|
98
|
-
* Ruby 2.6 through
|
|
99
|
-
* Rails 5.2 through
|
|
97
|
+
* sqlite, postgresql, mysql2, trilogy, and sqlserver adapters
|
|
98
|
+
* Ruby 2.6 through 4.0
|
|
99
|
+
* Rails 5.2 through 8.1
|
|
100
|
+
|
|
101
|
+
SQL Server tests run in CI (x86\_64) only; the SQL Server 2022 Docker image has no arm64 variant and is unstable under QEMU on Apple Silicon.
|
|
100
102
|
|
|
101
103
|
`unreliable` depends only on ActiveRecord and Railties. If you have a non-Rails app that uses ActiveRecord, you can still use it.
|
|
102
104
|
|
|
@@ -110,11 +112,11 @@ Because it's appended, the existing ordering is not affected unless it is ambigu
|
|
|
110
112
|
|
|
111
113
|
With `unreliable` installed, every ActiveRecord relation invoked by the test suite will have any ambiguity replaced with randomness. Tests that rely on the ordering of two records will break half the time. Tests with three or more break most of the time.
|
|
112
114
|
|
|
113
|
-
`unreliable` patches `ActiveRecord::
|
|
115
|
+
`unreliable` patches `ActiveRecord::Relation#build_order`, the point where ordering is assembled, to append a random-order term to the existing order chain. (The patch is applied after ActiveRecord loads, using `ActiveSupport.on_load`, the standard interface since Rails 4.0.) It works with MySQL, PostgreSQL, SQLite, and SQL Server.
|
|
114
116
|
|
|
115
|
-
|
|
117
|
+
Patching `build_order` ensures that the `ORDER BY` applies to not just `SELECT` but e.g. `delete_all` and `update_all`. It also applies within subqueries.
|
|
116
118
|
|
|
117
|
-
The patch is only applied when `Rails.env.test?`,
|
|
119
|
+
The patch is only applied when `Rails.env.test?`, so it has zero effect on production. Just to be sure, the environment is also checked on every invocation, to make absolutely certain this code can run only in `test`.
|
|
118
120
|
|
|
119
121
|
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
122
|
|
|
@@ -136,15 +138,19 @@ After you spin up the containers and open a shell in the app container, run `unr
|
|
|
136
138
|
standardrb
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
Run its tests in
|
|
141
|
+
Run its tests in separate passes:
|
|
140
142
|
|
|
141
143
|
```
|
|
142
144
|
RSPEC_ADAPTER=sqlite bundle exec rake
|
|
143
145
|
RSPEC_ADAPTER=postgresql bundle exec rake
|
|
144
146
|
RSPEC_ADAPTER=mysql2 bundle exec rake
|
|
147
|
+
RSPEC_ADAPTER=trilogy bundle exec rake
|
|
148
|
+
RSPEC_ADAPTER=sqlserver bundle exec rake
|
|
145
149
|
```
|
|
146
150
|
|
|
147
|
-
The
|
|
151
|
+
The SQL Server pass requires an x86\_64 host; the SQL Server 2022 Docker image does not run on Apple Silicon.
|
|
152
|
+
|
|
153
|
+
The GitHub CI workflow in `.github/` ensures those tests are also run 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.
|
|
148
154
|
|
|
149
155
|
### Experiment
|
|
150
156
|
|
|
@@ -7,26 +7,31 @@ require "active_record/connection_adapters/abstract_adapter"
|
|
|
7
7
|
module Unreliable
|
|
8
8
|
module BuildOrder
|
|
9
9
|
def build_order(arel)
|
|
10
|
-
super
|
|
10
|
+
super
|
|
11
11
|
|
|
12
|
-
adapter_name =
|
|
12
|
+
adapter_name = unreliable_connection.adapter_name
|
|
13
13
|
return unless Unreliable::Config.enabled?
|
|
14
14
|
return if distinct_on_postgres?(adapter_name)
|
|
15
|
+
return if distinct_on_sqlserver?(adapter_name)
|
|
15
16
|
return if from_only_internal_metadata?(arel)
|
|
16
17
|
return if from_one_table_with_ordered_pk?(arel)
|
|
17
18
|
|
|
18
19
|
case adapter_name
|
|
19
|
-
when "Mysql2"
|
|
20
|
+
when "Mysql2", "Trilogy"
|
|
20
21
|
# https://dev.mysql.com/doc/refman/8.0/en/mathematical-functions.html#function_rand
|
|
21
22
|
arel.order("RAND()")
|
|
22
23
|
|
|
24
|
+
when "SQLServer"
|
|
25
|
+
# https://learn.microsoft.com/en-us/sql/t-sql/functions/newid-transact-sql
|
|
26
|
+
arel.order("NEWID()")
|
|
27
|
+
|
|
23
28
|
when "PostgreSQL", "SQLite"
|
|
24
29
|
# https://www.postgresql.org/docs/16/functions-math.html#FUNCTIONS-MATH-RANDOM-TABLE
|
|
25
30
|
# https://www.sqlite.org/lang_corefunc.html#random
|
|
26
31
|
arel.order("RANDOM()")
|
|
27
32
|
|
|
28
33
|
else
|
|
29
|
-
raise ArgumentError, "unknown
|
|
34
|
+
raise ArgumentError, "unreliable: unknown adapter #{adapter_name.inspect}"
|
|
30
35
|
|
|
31
36
|
end
|
|
32
37
|
end
|
|
@@ -35,6 +40,12 @@ module Unreliable
|
|
|
35
40
|
distinct_value && adapter_name == "PostgreSQL"
|
|
36
41
|
end
|
|
37
42
|
|
|
43
|
+
def distinct_on_sqlserver?(adapter_name)
|
|
44
|
+
# SQL Server rejects ORDER BY expressions not in the select list when
|
|
45
|
+
# DISTINCT is used, so we don't append NEWID() to DISTINCT queries.
|
|
46
|
+
distinct_value && adapter_name == "SQLServer"
|
|
47
|
+
end
|
|
48
|
+
|
|
38
49
|
def from_only_internal_metadata?(arel)
|
|
39
50
|
# No need to randomize queries on ar_internal_metadata
|
|
40
51
|
arel.froms.map(&:name) == [ActiveRecord::Base.internal_metadata_table_name]
|
|
@@ -57,13 +68,20 @@ module Unreliable
|
|
|
57
68
|
# Using the SchemaCache minimizes the number of times we have to, e.g. in MySQL,
|
|
58
69
|
# SELECT column_name FROM information_schema.statistics
|
|
59
70
|
# (or in Rails < 6, SELECT column_name FROM information_schema.key_column_usage)
|
|
60
|
-
[
|
|
71
|
+
[unreliable_connection.schema_cache.primary_keys(arel.froms.first.name)].flatten
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def unreliable_connection
|
|
75
|
+
# Rails 7.2+ soft-deprecated `connection` in favor of `lease_connection`.
|
|
76
|
+
# We check for it with klass (not self) because it works on Rails 7.2+ due to
|
|
77
|
+
# delegation, and it doesn't break Rails 5.2 due to a respond_to_missing bug.
|
|
78
|
+
klass.respond_to?(:lease_connection) ? lease_connection : connection
|
|
61
79
|
end
|
|
62
80
|
|
|
63
81
|
def order_columns(arel)
|
|
64
82
|
from_table_name = arel.froms.first.name
|
|
65
83
|
arel.orders
|
|
66
|
-
.
|
|
84
|
+
.grep(Arel::Nodes::Ordering) # Don't try to parse textual orders
|
|
67
85
|
.map(&:expr)
|
|
68
86
|
.select { |expr| expr.relation.name == from_table_name }
|
|
69
87
|
.map(&:name)
|
data/lib/unreliable/config.rb
CHANGED
|
@@ -7,15 +7,15 @@ module Unreliable
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def self.enabled?
|
|
10
|
-
@enabled && Rails.env.test?
|
|
10
|
+
@enabled && !Thread.current[:unreliable_disabled] && Rails.env.test?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def self.disable
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
was_disabled = Thread.current[:unreliable_disabled]
|
|
15
|
+
Thread.current[:unreliable_disabled] = true
|
|
16
16
|
yield
|
|
17
17
|
ensure
|
|
18
|
-
|
|
18
|
+
Thread.current[:unreliable_disabled] = was_disabled
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
data/lib/unreliable/version.rb
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Unreliable::Config do
|
|
4
|
+
it "restores state after nested disable blocks" do
|
|
5
|
+
expect(Cat.all.to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
6
|
+
|
|
7
|
+
Unreliable::Config.disable do
|
|
8
|
+
expect(Cat.all.to_sql).to_not include("RANDOM()")
|
|
9
|
+
expect(Cat.all.to_sql).to_not include("RAND()")
|
|
10
|
+
|
|
11
|
+
Unreliable::Config.disable do
|
|
12
|
+
expect(Cat.all.to_sql).to_not include("RANDOM()")
|
|
13
|
+
expect(Cat.all.to_sql).to_not include("RAND()")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Still disabled after inner block
|
|
17
|
+
expect(Cat.all.to_sql).to_not include("RANDOM()")
|
|
18
|
+
expect(Cat.all.to_sql).to_not include("RAND()")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Re-enabled after outer block
|
|
22
|
+
expect(Cat.all.to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Config.disable must only suppress randomization in the calling thread.
|
|
4
|
+
# Before the fix, @enabled was a class-level variable shared across threads,
|
|
5
|
+
# so one thread's disable block would suppress randomization in all threads.
|
|
6
|
+
|
|
7
|
+
RSpec.describe Unreliable::Config, "thread safety" do
|
|
8
|
+
it "does not affect other threads when disabled" do
|
|
9
|
+
other_thread_sql = nil
|
|
10
|
+
barrier = Queue.new
|
|
11
|
+
|
|
12
|
+
Unreliable::Config.disable do
|
|
13
|
+
# Our thread has randomization disabled
|
|
14
|
+
expect(Cat.all.to_sql).to_not include("RANDOM()")
|
|
15
|
+
expect(Cat.all.to_sql).to_not include("RAND()")
|
|
16
|
+
|
|
17
|
+
Thread.new do
|
|
18
|
+
other_thread_sql = Cat.all.to_sql
|
|
19
|
+
barrier.push(:done)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
barrier.pop # wait for the other thread
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
expect(other_thread_sql).to include(adapter_rand("RANDOM()"))
|
|
26
|
+
end
|
|
27
|
+
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(
|
|
6
|
+
expect(Cat.where(word: "foo").to_sql).to end_with(adapter_rand(%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(
|
|
13
|
+
expect(Cat.where(word: "foo").to_sql).to end_with(adapter_rand(%q(WHERE "cats"."word" = 'foo')))
|
|
14
14
|
ensure
|
|
15
15
|
Rails.env = "test"
|
|
16
16
|
end
|
data/spec/examples.txt
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
example_id | status | run_time |
|
|
2
|
+
------------------------------------------ | ------- | --------------- |
|
|
3
|
+
./spec/adapter_option_spec.rb[1:1] | passed | 0.00045 seconds |
|
|
4
|
+
./spec/env_spec.rb[1:1] | passed | 0.00516 seconds |
|
|
5
|
+
./spec/env_spec.rb[1:2] | passed | 0.00012 seconds |
|
|
6
|
+
./spec/execute_queries_spec.rb[1:1] | passed | 0.00479 seconds |
|
|
7
|
+
./spec/execute_queries_spec.rb[1:2] | passed | 0.00774 seconds |
|
|
8
|
+
./spec/execute_queries_spec.rb[1:3] | passed | 0.00722 seconds |
|
|
9
|
+
./spec/execute_queries_spec.rb[1:4] | passed | 0.00989 seconds |
|
|
10
|
+
./spec/execute_queries_spec.rb[1:5] | passed | 0.01507 seconds |
|
|
11
|
+
./spec/execute_queries_spec.rb[1:6] | passed | 0.006 seconds |
|
|
12
|
+
./spec/execute_queries_spec.rb[1:7] | passed | 0.00368 seconds |
|
|
13
|
+
./spec/execute_queries_spec.rb[1:8] | passed | 0.00372 seconds |
|
|
14
|
+
./spec/execute_queries_spec.rb[1:9] | passed | 0.00329 seconds |
|
|
15
|
+
./spec/execute_subqueries_spec.rb[1:1] | passed | 0.00974 seconds |
|
|
16
|
+
./spec/execute_subqueries_spec.rb[1:2] | passed | 0.00419 seconds |
|
|
17
|
+
./spec/model_cache_versioning_spec.rb[1:1] | passed | 0.00092 seconds |
|
|
18
|
+
./spec/model_indexes_books_spec.rb[1:1] | passed | 0.00058 seconds |
|
|
19
|
+
./spec/model_indexes_books_spec.rb[1:2] | passed | 0.0002 seconds |
|
|
20
|
+
./spec/model_indexes_books_spec.rb[1:3] | passed | 0.00007 seconds |
|
|
21
|
+
./spec/model_indexes_cats_spec.rb[1:1] | passed | 0.00006 seconds |
|
|
22
|
+
./spec/model_indexes_cats_spec.rb[1:2] | passed | 0.00009 seconds |
|
|
23
|
+
./spec/model_indexes_dreams_spec.rb[1:1] | passed | 0.00008 seconds |
|
|
24
|
+
./spec/model_indexes_dreams_spec.rb[1:2] | passed | 0.00007 seconds |
|
|
25
|
+
./spec/model_indexes_shelves_spec.rb[1:1] | passed | 0.00053 seconds |
|
|
26
|
+
./spec/model_indexes_shelves_spec.rb[1:2] | passed | 0.00022 seconds |
|
|
27
|
+
./spec/model_indexes_shelves_spec.rb[1:3] | passed | 0.00008 seconds |
|
|
28
|
+
./spec/model_indexes_shelves_spec.rb[1:4] | passed | 0.00006 seconds |
|
|
29
|
+
./spec/model_indexes_shelves_spec.rb[1:5] | passed | 0.00007 seconds |
|
|
30
|
+
./spec/model_indexes_shelves_spec.rb[1:6] | passed | 0.0001 seconds |
|
|
31
|
+
./spec/model_joins_spec.rb[1:1] | passed | 0.00405 seconds |
|
|
32
|
+
./spec/model_joins_spec.rb[1:2] | passed | 0.0006 seconds |
|
|
33
|
+
./spec/model_joins_spec.rb[1:3] | passed | 0.00015 seconds |
|
|
34
|
+
./spec/model_joins_spec.rb[1:4] | passed | 0.00024 seconds |
|
|
35
|
+
./spec/model_select_distinct_spec.rb[1:1] | passed | 0.0001 seconds |
|
|
36
|
+
./spec/model_select_distinct_spec.rb[1:2] | passed | 0.00009 seconds |
|
|
37
|
+
./spec/model_select_distinct_spec.rb[1:3] | passed | 0.00007 seconds |
|
|
38
|
+
./spec/model_select_distinct_spec.rb[1:4] | passed | 0.00123 seconds |
|
|
39
|
+
./spec/model_select_spec.rb[1:1] | passed | 0.00007 seconds |
|
|
40
|
+
./spec/model_select_spec.rb[1:2] | passed | 0.00008 seconds |
|
|
41
|
+
./spec/model_select_spec.rb[1:3] | passed | 0.00007 seconds |
|
|
42
|
+
./spec/model_select_spec.rb[1:4] | passed | 0.00013 seconds |
|
|
43
|
+
./spec/model_subquery_spec.rb[1:1] | passed | 0.00014 seconds |
|
|
44
|
+
./spec/model_update_arel_10_spec.rb[1:1] | passed | 0.00123 seconds |
|
|
45
|
+
./spec/railtie_spec.rb[1:1] | passed | 0.00003 seconds |
|
|
46
|
+
./spec/railtie_spec.rb[1:2] | passed | 0.00003 seconds |
|
|
47
|
+
./spec/textual_order_raw_spec.rb[1:1] | passed | 0.00009 seconds |
|
|
48
|
+
./spec/textual_order_raw_spec.rb[1:2] | passed | 0.00007 seconds |
|
|
49
|
+
./spec/textual_order_raw_spec.rb[1:3] | pending | 0.00001 seconds |
|
|
50
|
+
./spec/textual_order_raw_spec.rb[1:4] | pending | 0 seconds |
|
|
51
|
+
./spec/textual_order_raw_spec.rb[1:5] | passed | 0.00007 seconds |
|
|
52
|
+
./spec/textual_order_raw_spec.rb[1:6] | passed | 0.00006 seconds |
|
|
53
|
+
./spec/textual_order_raw_spec.rb[1:7] | passed | 0.00006 seconds |
|
|
54
|
+
./spec/textual_order_raw_spec.rb[1:8] | passed | 0.00006 seconds |
|
|
55
|
+
./spec/textual_order_spec.rb[1:1] | passed | 0.00006 seconds |
|
|
56
|
+
./spec/textual_order_spec.rb[1:2] | passed | 0.00006 seconds |
|
|
57
|
+
./spec/textual_order_spec.rb[1:3] | passed | 0.00006 seconds |
|
|
58
|
+
./spec/textual_order_spec.rb[1:4] | passed | 0.00006 seconds |
|
|
59
|
+
./spec/textual_order_spec.rb[1:5] | passed | 0.00006 seconds |
|
|
60
|
+
./spec/textual_order_spec.rb[1:6] | passed | 0.00007 seconds |
|
|
61
|
+
./spec/version_spec.rb[1:1] | passed | 0.00054 seconds |
|
|
62
|
+
./spec/version_spec.rb[1:2] | passed | 0.00004 seconds |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# eager_load fires a single LEFT OUTER JOIN query that goes through build_order
|
|
4
|
+
# and gets ORDER BY RANDOM() appended.
|
|
5
|
+
#
|
|
6
|
+
# preload fires the main query (ORDER BY RANDOM()) then a separate IN-list query
|
|
7
|
+
# for the association (also ORDER BY RANDOM(), since it's its own build_order call).
|
|
8
|
+
#
|
|
9
|
+
# includes delegates to preload when no association conditions are present, and
|
|
10
|
+
# to eager_load when conditions reference the association table.
|
|
11
|
+
#
|
|
12
|
+
# All three strategies are tested for correct SQL shape (via to_sql) and correct
|
|
13
|
+
# association loading (via execution).
|
|
14
|
+
|
|
15
|
+
RSpec.describe "eager_load, preload, includes" do
|
|
16
|
+
# SQL shape — eager_load produces a single joinable query; to_sql reflects it fully.
|
|
17
|
+
# preload/includes (no conditions) to_sql shows only the main query.
|
|
18
|
+
|
|
19
|
+
it "eager_load appends random order on has_one" do
|
|
20
|
+
expect(Dreamer.eager_load(:dream).to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "eager_load appends random order on has_many" do
|
|
24
|
+
expect(Owner.eager_load(:cats).to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "preload main query appends random order" do
|
|
28
|
+
expect(Dreamer.preload(:dream).to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "preload main query does not append random order when ordered by primary key" do
|
|
32
|
+
expect(Dreamer.preload(:dream).order(:dreamer_id).to_sql).to end_with(
|
|
33
|
+
adapter_rand('ORDER BY "dreamers"."dreamer_id" ASC')
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "includes (no conditions) main query appends random order" do
|
|
38
|
+
expect(Owner.includes(:cats).to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "includes (no conditions) main query does not append random order when ordered by primary key" do
|
|
42
|
+
expect(Owner.includes(:cats).order(:id).to_sql).to end_with(
|
|
43
|
+
adapter_rand('ORDER BY "owners"."id" ASC')
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "includes (with association conditions) appends random order" do
|
|
48
|
+
expect(Owner.includes(:cats).where(cats: {name: "foo"}).to_sql).to end_with(
|
|
49
|
+
adapter_rand("ORDER BY RANDOM()")
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Execution — verify associations load correctly despite the appended ORDER BY.
|
|
54
|
+
|
|
55
|
+
context "with data" do
|
|
56
|
+
before do
|
|
57
|
+
@chuangmu = Owner.create!(name: "Chuangmu")
|
|
58
|
+
@oisin = Owner.create!(name: "Oisin")
|
|
59
|
+
Cat.create!(name: "Baku", owner: @chuangmu)
|
|
60
|
+
Cat.create!(name: "Mara", owner: @chuangmu)
|
|
61
|
+
Cat.create!(name: "Khidr", owner: @oisin)
|
|
62
|
+
|
|
63
|
+
@gilgamesh = Dreamer.create!(name: "Gilgamesh")
|
|
64
|
+
@penelope = Dreamer.create!(name: "Penelope")
|
|
65
|
+
Dream.create!(subject: "cedar forest", dreamer: @gilgamesh)
|
|
66
|
+
Dream.create!(subject: "eagle and goose", dreamer: @penelope)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
after do
|
|
70
|
+
Dream.delete_all
|
|
71
|
+
Dreamer.delete_all
|
|
72
|
+
Cat.delete_all
|
|
73
|
+
Owner.delete_all
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "eager_load loads has_many associations correctly" do
|
|
77
|
+
owners = Owner.eager_load(:cats).order(:name).to_a
|
|
78
|
+
chuangmu = owners.find { |o| o.name == "Chuangmu" }
|
|
79
|
+
oisin = owners.find { |o| o.name == "Oisin" }
|
|
80
|
+
expect(chuangmu.cats.map(&:name).sort).to eq(%w[Baku Mara])
|
|
81
|
+
expect(oisin.cats.map(&:name)).to eq(["Khidr"])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "eager_load loads has_one associations correctly" do
|
|
85
|
+
dreamers = Dreamer.eager_load(:dream).order(:name).to_a
|
|
86
|
+
gilgamesh = dreamers.find { |d| d.name == "Gilgamesh" }
|
|
87
|
+
penelope = dreamers.find { |d| d.name == "Penelope" }
|
|
88
|
+
expect(gilgamesh.dream.subject).to eq("cedar forest")
|
|
89
|
+
expect(penelope.dream.subject).to eq("eagle and goose")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "preload loads associations correctly" do
|
|
93
|
+
owners = Owner.preload(:cats).order(:name).to_a
|
|
94
|
+
chuangmu = owners.find { |o| o.name == "Chuangmu" }
|
|
95
|
+
expect(chuangmu.cats.map(&:name).sort).to eq(%w[Baku Mara])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "includes loads associations correctly" do
|
|
99
|
+
dreamers = Dreamer.includes(:dream).order(:name).to_a
|
|
100
|
+
penelope = dreamers.find { |d| d.name == "Penelope" }
|
|
101
|
+
expect(penelope.dream.subject).to eq("eagle and goose")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "includes with association conditions filters and loads correctly" do
|
|
105
|
+
owners = Owner.includes(:cats).where(cats: {name: "Baku"}).to_a
|
|
106
|
+
expect(owners.map(&:name)).to eq(["Chuangmu"])
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# find_each and in_batches add ORDER BY id ASC internally for cursor-based
|
|
4
|
+
# iteration. The gem detects that the primary key is fully covered and correctly
|
|
5
|
+
# suppresses ORDER BY RANDOM(), leaving the batch queries intact.
|
|
6
|
+
|
|
7
|
+
RSpec.describe "find_each and in_batches" do
|
|
8
|
+
before do
|
|
9
|
+
%w[Baku Caer Chuangmu Gilgamesh Penelope].each { |name| Cat.create!(name: name) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
after { Cat.delete_all }
|
|
13
|
+
|
|
14
|
+
it "find_each yields all records without error" do
|
|
15
|
+
names = []
|
|
16
|
+
Cat.find_each(batch_size: 2) { |cat| names << cat.name }
|
|
17
|
+
expect(names.sort).to eq(%w[Baku Caer Chuangmu Gilgamesh Penelope])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "in_batches iterates all records without error" do
|
|
21
|
+
names = []
|
|
22
|
+
Cat.in_batches(of: 2) { |batch| names.concat(batch.pluck(:name)) }
|
|
23
|
+
expect(names.sort).to eq(%w[Baku Caer Chuangmu Gilgamesh Penelope])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "find_each does not append random order" do
|
|
27
|
+
sqls = []
|
|
28
|
+
subscription = ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
|
|
29
|
+
sqls << payload[:sql] if payload[:sql].include?("cats")
|
|
30
|
+
end
|
|
31
|
+
Cat.find_each(batch_size: 100) { next }
|
|
32
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
33
|
+
expect(sqls).to all(satisfy do |sql|
|
|
34
|
+
!sql.include?("RANDOM()") && !sql.include?("RAND()") && !sql.include?("NEWID()")
|
|
35
|
+
end)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "in_batches does not append random order" do
|
|
39
|
+
sqls = []
|
|
40
|
+
subscription = ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
|
|
41
|
+
sqls << payload[:sql] if payload[:sql].include?("cats")
|
|
42
|
+
end
|
|
43
|
+
Cat.in_batches(of: 100) { next }
|
|
44
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
45
|
+
expect(sqls).to all(satisfy do |sql|
|
|
46
|
+
!sql.include?("RANDOM()") && !sql.include?("RAND()") && !sql.include?("NEWID()")
|
|
47
|
+
end)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# pluck, exists?, and aggregate methods (count, sum, minimum, maximum) all go
|
|
4
|
+
# through build_arel and get ORDER BY RANDOM() appended. The ordering is harmless
|
|
5
|
+
# for aggregates (result is the same regardless) and silently discarded for pluck.
|
|
6
|
+
# These tests verify the methods return correct values despite the appended ORDER BY.
|
|
7
|
+
|
|
8
|
+
RSpec.describe "pluck, exists?, and aggregates" do
|
|
9
|
+
before do
|
|
10
|
+
Cat.new(name: "Alfie").save!
|
|
11
|
+
Cat.new(name: "Bella").save!
|
|
12
|
+
Cat.new(name: "Cleo").save!
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
after { Cat.delete_all }
|
|
16
|
+
|
|
17
|
+
it "pluck returns correct values" do
|
|
18
|
+
expect(Cat.pluck(:name).sort).to eq(%w[Alfie Bella Cleo])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "pluck with where returns correct values" do
|
|
22
|
+
expect(Cat.where(name: %w[Alfie Bella]).pluck(:name).sort).to eq(%w[Alfie Bella])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "exists? returns true when matching rows exist" do
|
|
26
|
+
expect(Cat.where(name: "Alfie").exists?).to be true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "exists? returns false when no matching rows exist" do
|
|
30
|
+
expect(Cat.where(name: "Nobody").exists?).to be false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "count returns correct value" do
|
|
34
|
+
expect(Cat.count).to eq(3)
|
|
35
|
+
expect(Cat.where(name: %w[Alfie Bella]).count).to eq(2)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "sum returns correct value" do
|
|
39
|
+
ids = Cat.pluck(:id)
|
|
40
|
+
expect(Cat.sum(:id)).to eq(ids.sum)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "minimum and maximum return correct values" do
|
|
44
|
+
ids = Cat.pluck(:id).sort
|
|
45
|
+
expect(Cat.minimum(:id)).to eq(ids.first)
|
|
46
|
+
expect(Cat.maximum(:id)).to eq(ids.last)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
class DreamTest < UnreliableTest
|
|
4
4
|
SUBJECTS = %w[fire air water earth life death].freeze
|
|
5
|
-
DREAMER_NAMES = %w[
|
|
5
|
+
DREAMER_NAMES = %w[Baku Caer Chuangmu Penelope Zhuangzi].freeze
|
|
6
6
|
raise ArgumentError, "DreamTest needs at least as many subjects as dreamers" if SUBJECTS.size < DREAMER_NAMES.size
|
|
7
7
|
end
|
|
8
8
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Cat.first internally does order(:id).limit(1). Since the PK is fully covered
|
|
4
|
+
# by the order, unreliable should NOT add RANDOM(). We can't call .to_sql on
|
|
5
|
+
# first/last (they return records), so we test the equivalent Relation.
|
|
6
|
+
|
|
7
|
+
RSpec.describe "first and last" do
|
|
8
|
+
it "does not randomize the query equivalent to .first" do
|
|
9
|
+
expect(Cat.order(:id).limit(1).to_sql).to end_with(
|
|
10
|
+
"#{adapter_rand('ORDER BY "cats"."id" ASC')} #{adapter_limit(1)}"
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "does not randomize the query equivalent to .last" do
|
|
15
|
+
expect(Cat.order(id: :desc).limit(1).to_sql).to end_with(
|
|
16
|
+
"#{adapter_rand('ORDER BY "cats"."id" DESC')} #{adapter_limit(1)}"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "executes first and last without error" do
|
|
21
|
+
Cat.new(name: "Alfa").save!
|
|
22
|
+
Cat.new(name: "Bravo").save!
|
|
23
|
+
expect(Cat.first.name).to be_a(String)
|
|
24
|
+
expect(Cat.last.name).to be_a(String)
|
|
25
|
+
ensure
|
|
26
|
+
Cat.delete_all
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# GROUP BY queries get ORDER BY RANDOM() appended after the HAVING clause.
|
|
4
|
+
# This is semantically harmless -- the DB groups first, then sorts.
|
|
5
|
+
# Aggregate queries like .count use a separate code path and don't get ORDER BY.
|
|
6
|
+
|
|
7
|
+
RSpec.describe "group and having" do
|
|
8
|
+
it "randomly selects with group" do
|
|
9
|
+
expect(Cat.group(:name).to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "randomly selects with group and having" do
|
|
13
|
+
expect(Cat.group(:name).having("count(*) > 0").to_sql).to end_with(adapter_rand("ORDER BY RANDOM()"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "executes group with count correctly" do
|
|
17
|
+
Cat.new(name: "Jinx").save!
|
|
18
|
+
Cat.new(name: "Jinx").save!
|
|
19
|
+
Cat.new(name: "Pixel").save!
|
|
20
|
+
result = Cat.group(:name).count
|
|
21
|
+
expect(result).to eq({"Jinx" => 2, "Pixel" => 1})
|
|
22
|
+
ensure
|
|
23
|
+
Cat.delete_all
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "executes group with having correctly" do
|
|
27
|
+
Cat.new(name: "Jinx").save!
|
|
28
|
+
Cat.new(name: "Jinx").save!
|
|
29
|
+
Cat.new(name: "Pixel").save!
|
|
30
|
+
result = Cat.group(:name).having("count(*) > 1").count
|
|
31
|
+
expect(result).to eq({"Jinx" => 2})
|
|
32
|
+
ensure
|
|
33
|
+
Cat.delete_all
|
|
34
|
+
end
|
|
35
|
+
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(
|
|
5
|
+
expect(Book.all.to_sql).to end_with(adapter_rand("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(
|
|
9
|
+
expect(Book.all.order(:subject).to_sql).to end_with(adapter_rand('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(
|
|
13
|
+
expect(Book.all.order(:isbn).to_sql).to end_with(adapter_rand('ORDER BY "books"."isbn" ASC, RANDOM()'))
|
|
14
14
|
end
|
|
15
15
|
end
|