unreliable 1.0.0 → 1.0.2

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: 17d0865e8524f14bc5d1ddc01106ccf63adb25f044a7ea3ca9109d44c6f31f3f
4
- data.tar.gz: c3b5ff4213d9fb54bd3a48838ad0642c4a132e80715e45500ecf3c6884cd6f47
3
+ metadata.gz: 03cacbb1f651eeac84c72fe0534624779978b5c954f9e0612c89784555c1b0bb
4
+ data.tar.gz: b2157f68e867d7ba3ced7b0bb4bc4d0dbad964cfdbe229d915a8080e84a2fd8c
5
5
  SHA512:
6
- metadata.gz: 461eb8a9445d48f09ff0b6e89f097fba52685f55e93637c1e2c6bc582104af44d3e30d8c503f10e145f1a3ef052f9fdaf2b4c9cb2544f3e71080f87ea2d9854a
7
- data.tar.gz: 892d721b2ff0b45430625c1feae890ce0dbdc194dcab3118f8d20251af0eb0ee2c2d45bc4550bfd5ddbff334524c42c67b2d642a53709efa9c289e27a866f956
6
+ metadata.gz: e037706c73288b463555844acbcf70ba73c03416d28252c32c2ceb26280bda10898d55270fbde8c650fc60bad1cf21f1a5ef049d6cad8d7c0cd15a4ca01c7cfa
7
+ data.tar.gz: f9fd9946c411f6691e645626d186ec79a856f7007aedd2becfed8d6bfac25ad04f1e319fd00b26ad1ecb3c3b67870a88c3bd4b102ff8b05b323fcfe0fc620d1a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## Unreliable 1.0.2 (July 22, 2026) ##
2
+
3
+ ### Fixed
4
+
5
+ * Postgres bugfix when using textual DISTINCT
6
+ * Postgres bugfix when using computed columns
7
+
8
+ ## Unreliable 1.0.1 (June 4, 2026) ##
9
+
10
+ ### Changed
11
+
12
+ * Administrative changes to how the gem is built; no changes to the gem itself.
13
+
1
14
  ## Unreliable 1.0.0 (April 18, 2026) ##
2
15
 
3
16
  ### Fixed
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  gem "appraisal", "~> 2.4"
8
- gem "bundler", "~> 2.1"
8
+ gem "bundler", ">= 2.1"
9
9
  gem "combustion", "~> 1.5"
10
10
  gem "mysql2", "~> 0.5"
11
11
  gem "ostruct"
@@ -37,13 +37,23 @@ module Unreliable
37
37
  end
38
38
 
39
39
  def distinct_on_postgres?(adapter_name)
40
- distinct_value && adapter_name == "PostgreSQL"
40
+ distinct_query? && adapter_name == "PostgreSQL"
41
41
  end
42
42
 
43
43
  def distinct_on_sqlserver?(adapter_name)
44
44
  # SQL Server rejects ORDER BY expressions not in the select list when
45
45
  # DISTINCT is used, so we don't append NEWID() to DISTINCT queries.
46
- distinct_value && adapter_name == "SQLServer"
46
+ distinct_query? && adapter_name == "SQLServer"
47
+ end
48
+
49
+ def distinct_query?
50
+ # `.distinct` sets distinct_value, but a raw "DISTINCT ..." / "DISTINCT ON (...)"
51
+ # written into `.select` does not. SQL only allows a statement-level DISTINCT as
52
+ # the leading keyword of the select clause, so we detect it at the start of a
53
+ # select fragment (an aggregate's inner `count(DISTINCT x)` is never at the start).
54
+ # A false positive only forgoes randomization; a miss would append an ORDER BY that
55
+ # PostgreSQL and SQL Server reject for DISTINCT queries.
56
+ distinct_value || select_values.any? { |value| value.to_s.match?(/\A\s*DISTINCT\b/i) }
47
57
  end
48
58
 
49
59
  def from_only_internal_metadata?(arel)
@@ -83,6 +93,7 @@ module Unreliable
83
93
  arel.orders
84
94
  .grep(Arel::Nodes::Ordering) # Don't try to parse textual orders
85
95
  .map(&:expr)
96
+ .grep(Arel::Attributes::Attribute) # Skip computed expressions
86
97
  .select { |expr| expr.relation.name == from_table_name }
87
98
  .map(&:name)
88
99
  .map(&:to_s) # In Rails < 5.2, the order column names are symbols; >= 5.2, strings
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unreliable
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
5
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An order can be a directional node (Ascending/Descending) wrapping a computed
4
+ # expression rather than a plain column. Those wrappers survive the
5
+ # Arel::Nodes::Ordering grep in order_columns, but their .expr is not an
6
+ # Arel::Attributes::Attribute and has no .relation. The wrapped expr can be any
7
+ # of several node classes -- a SqlLiteral, a NamedFunction, an aggregate, etc.
8
+ # None of them can cover a primary key, so the query must be randomized (and
9
+ # must not raise).
10
+ #
11
+ # Note: a plain string order like .order("name DESC") does NOT reach this code;
12
+ # it becomes a bare SqlLiteral that the Ordering grep drops (see
13
+ # textual_order_spec.rb). Only a *directional* node wrapping a non-column does.
14
+
15
+ RSpec.describe "computed order" do
16
+ it "randomly selects when ordered by a string-key hash alias" do
17
+ # No explicit Arel -- the idiomatic hash form. "name_len" isn't a real column,
18
+ # so it becomes a quoted SqlLiteral identifier wrapped in a Descending node.
19
+ expect(Cat.order("name_len" => :desc).to_sql).to end_with(
20
+ adapter_rand('ORDER BY "name_len" DESC, RANDOM()')
21
+ )
22
+ end
23
+
24
+ it "randomly selects when ordered by a SqlLiteral with a direction" do
25
+ expect(Cat.order(Arel.sql("name_len").desc).to_sql).to end_with(
26
+ adapter_rand("ORDER BY name_len DESC, RANDOM()")
27
+ )
28
+ end
29
+
30
+ it "randomly selects when ordered by a function expression (NamedFunction)" do
31
+ expect(Cat.order(Cat.arel_table[:name].lower.desc).to_sql).to end_with(
32
+ adapter_rand('ORDER BY LOWER("cats"."name") DESC, RANDOM()')
33
+ )
34
+ end
35
+
36
+ it "randomly selects when ordered by an aggregate expression (Count)" do
37
+ expect(Cat.order(Cat.arel_table[:id].count.desc).to_sql).to end_with(
38
+ adapter_rand('ORDER BY COUNT("cats"."id") DESC, RANDOM()')
39
+ )
40
+ end
41
+
42
+ it "randomly selects when ordered by an arithmetic expression (Grouping)" do
43
+ # (attr + 1) is an Arel::Nodes::Grouping, which does not mix in .desc on
44
+ # ActiveRecord 5.2, so wrap it in a Descending node directly.
45
+ expect(Cat.order(Arel::Nodes::Descending.new(Cat.arel_table[:id] + 1)).to_sql).to end_with(
46
+ adapter_rand('ORDER BY ("cats"."id" + 1) DESC, RANDOM()')
47
+ )
48
+ end
49
+
50
+ it "randomly selects on a composite-PK table ordered by a computed expression" do
51
+ expect(Shelf.order(Arel.sql("LENGTH(contents)").asc).to_sql).to end_with(
52
+ adapter_rand("ORDER BY LENGTH(contents) ASC, RANDOM()")
53
+ )
54
+ end
55
+
56
+ it "does not randomize when the PK is covered even though a computed order is also present" do
57
+ # order_columns must still recognize the real "id" attribute after skipping
58
+ # the computed expression, so the covered-PK short-circuit still applies.
59
+ expect(Cat.order(:id).order(Arel.sql("LENGTH(name)").desc).to_sql).to end_with(
60
+ adapter_rand('ORDER BY "cats"."id" ASC, LENGTH(name) DESC')
61
+ )
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DISTINCT written as a raw string in .select (rather than via the .distinct
4
+ # relation method) does not set distinct_value. Without detecting it, the gem
5
+ # appends ORDER BY RANDOM(), which PostgreSQL and SQL Server reject:
6
+ #
7
+ # for SELECT DISTINCT, ORDER BY expressions must appear in select list
8
+ # SELECT DISTINCT ON expressions must match initial ORDER BY expressions
9
+ #
10
+ # On MySQL and SQLite the appended random order is accepted, so -- as with the
11
+ # .distinct relation method -- those adapters still get randomized.
12
+
13
+ RSpec.describe "raw DISTINCT in select" do
14
+ it "does not append a randomizing order to a raw DISTINCT select on postgres/sqlserver" do
15
+ expect(Cat.select("DISTINCT name").to_sql).to end_with(
16
+ case UnreliableTest.find_adapter
17
+ when "postgresql", "sqlserver"
18
+ adapter_quotes('DISTINCT name FROM "cats"')
19
+ else
20
+ adapter_rand("ORDER BY RANDOM()")
21
+ end
22
+ )
23
+ end
24
+
25
+ it "does not append a randomizing order to a raw DISTINCT ON select on postgres",
26
+ skip: ((UnreliableTest.find_adapter == "postgresql") ? false : "DISTINCT ON is PostgreSQL-only syntax") do
27
+ expect(Cat.select("DISTINCT ON (name) *").to_sql).to end_with(
28
+ adapter_rand('SELECT DISTINCT ON (name) * FROM "cats"')
29
+ )
30
+ end
31
+
32
+ it "still detects DISTINCT written in lowercase" do
33
+ expect(Cat.select("distinct name").to_sql).to end_with(
34
+ case UnreliableTest.find_adapter
35
+ when "postgresql", "sqlserver"
36
+ adapter_quotes('distinct name FROM "cats"')
37
+ else
38
+ adapter_rand("ORDER BY RANDOM()")
39
+ end
40
+ )
41
+ end
42
+
43
+ it "does not treat an aggregate's inner DISTINCT as a statement-level DISTINCT" do
44
+ # count(DISTINCT ...) is not a SELECT DISTINCT, so the query must still be randomized.
45
+ expect(Cat.select("count(DISTINCT name) AS n").to_sql).to end_with(
46
+ adapter_rand("ORDER BY RANDOM()")
47
+ )
48
+ end
49
+
50
+ it "executes a raw DISTINCT select without a server error" do
51
+ expect { Cat.select("DISTINCT name").load }.not_to raise_error
52
+ end
53
+
54
+ it "executes a raw DISTINCT ON select without a server error",
55
+ skip: ((UnreliableTest.find_adapter == "postgresql") ? false : "DISTINCT ON is PostgreSQL-only syntax") do
56
+ expect { Cat.select("DISTINCT ON (name) *").load }.not_to raise_error
57
+ end
58
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unreliable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - James McCarthy
@@ -69,10 +69,10 @@ files:
69
69
  - lib/unreliable/railtie.rb
70
70
  - lib/unreliable/version.rb
71
71
  - spec/adapter_option_spec.rb
72
+ - spec/computed_order_spec.rb
72
73
  - spec/config_disable_nesting_spec.rb
73
74
  - spec/config_thread_safety_spec.rb
74
75
  - spec/env_spec.rb
75
- - spec/examples.txt
76
76
  - spec/execute_eager_load_spec.rb
77
77
  - spec/execute_find_each_spec.rb
78
78
  - spec/execute_pluck_exists_aggregate_spec.rb
@@ -96,6 +96,7 @@ files:
96
96
  - spec/model_update_arel_10_spec.rb
97
97
  - spec/pluck_exists_aggregate_spec.rb
98
98
  - spec/railtie_spec.rb
99
+ - spec/select_distinct_raw_spec.rb
99
100
  - spec/spec_helper.rb
100
101
  - spec/textual_order_raw_spec.rb
101
102
  - spec/textual_order_spec.rb
@@ -120,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
121
  - !ruby/object:Gem::Version
121
122
  version: '0'
122
123
  requirements: []
123
- rubygems_version: 4.0.6
124
+ rubygems_version: 4.0.16
124
125
  specification_version: 4
125
126
  summary: For ActiveRecord tests, surface ambiguous-ordering bugs
126
127
  test_files: []
data/spec/examples.txt DELETED
@@ -1,62 +0,0 @@
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 |