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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3cf18102610d7a8a8c380f39bad81db77ed33392ecc8dffcdc99ef7c60d222e
4
- data.tar.gz: b99100338674b31fbb11036b56ce1443006d1281e5641a0b4f200bbe7a22e832
3
+ metadata.gz: 17d0865e8524f14bc5d1ddc01106ccf63adb25f044a7ea3ca9109d44c6f31f3f
4
+ data.tar.gz: c3b5ff4213d9fb54bd3a48838ad0642c4a132e80715e45500ecf3c6884cd6f47
5
5
  SHA512:
6
- metadata.gz: 96139a8994cdb3badf0f80e3bc96c94c375864f9973d9b85b56fa6ad97486001dbaa56e24b42cdb2d4ba7d051a22877851fac26a5a993b07773984521cb456e2
7
- data.tar.gz: 890b786b597e60b980937d792555a3666082f7d38ab6e6b1113acdfb01adc5ec6596ef59ed2041a89215526c2ca8dacccef1184967db68356f178155617d3aaa
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
- The next time your test suite runs, it may emit new errors and failures. If so, great!
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 mysql2 adapters
98
- * Ruby 2.6 through 3.3
99
- * Rails 5.2 through 7.1
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::QueryMethods#build_arel`, the point where an Arel is converted for use, to append an order 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, Postgres, and SQLite.
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
- This means that the `ORDER BY` applies to not just `SELECT` but e.g. `delete_all` and `update_all`. It also applies within subqueries.
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?`, and that boolean is also checked on every invocation, just to make certain it has no effect in any other environment.
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 three separate passes:
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 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.
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(arel)
10
+ super
11
11
 
12
- adapter_name = Arel::Table.engine.connection.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 Arel::Table.engine"
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
- [ActiveRecord::Base.connection.schema_cache.primary_keys(arel.froms.first.name)].flatten
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
- .select { |order| order.is_a? Arel::Nodes::Ordering } # Don't try to parse textual orders
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)
@@ -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
- prev_enabled = @enabled
15
- @enabled = false
14
+ was_disabled = Thread.current[:unreliable_disabled]
15
+ Thread.current[:unreliable_disabled] = true
16
16
  yield
17
17
  ensure
18
- @enabled = prev_enabled
18
+ Thread.current[:unreliable_disabled] = was_disabled
19
19
  end
20
20
  end
21
21
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unreliable
4
- VERSION = "0.10.0"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -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(adapter_text(%q(WHERE "cats"."word" = 'foo')))
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(adapter_text(%q(WHERE "cats"."word" = 'foo')))
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[Morpheus Cluracan Mervyn Gilbert Nuala].freeze
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(adapter_text("ORDER BY RANDOM()"))
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(adapter_text('ORDER BY "books"."subject" ASC, RANDOM()'))
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(adapter_text('ORDER BY "books"."isbn" ASC, RANDOM()'))
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