unreliable 1.0.1 → 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: 4e32845f793b8cf8d2200fad74d279a8a49a9d9cd2667628dec2be952f9853f7
4
- data.tar.gz: b3d7922e822f5c81d816c6860efdec1e7375921f33c37ca80428587f0179aa19
3
+ metadata.gz: 03cacbb1f651eeac84c72fe0534624779978b5c954f9e0612c89784555c1b0bb
4
+ data.tar.gz: b2157f68e867d7ba3ced7b0bb4bc4d0dbad964cfdbe229d915a8080e84a2fd8c
5
5
  SHA512:
6
- metadata.gz: 847f0641314a2f5c7270fb8550a5326a2912a8a58b3dedee183853cfa0fb5a8b4af6f8dffbb898fd9c183a22a3976aaca2a99c9fb215a73b1464b2f5cf60f65b
7
- data.tar.gz: 68f3009ddaf290b01cfb1ec04d9012eb95d9bc5069f7c5ef6e4d0cc5a1a04b5b366ad67235f48ee3193cea798828cc353ec35bb7b93ebee3cb4bddc3a4b5581f
6
+ metadata.gz: e037706c73288b463555844acbcf70ba73c03416d28252c32c2ceb26280bda10898d55270fbde8c650fc60bad1cf21f1a5ef049d6cad8d7c0cd15a4ca01c7cfa
7
+ data.tar.gz: f9fd9946c411f6691e645626d186ec79a856f7007aedd2becfed8d6bfac25ad04f1e319fd00b26ad1ecb3c3b67870a88c3bd4b102ff8b05b323fcfe0fc620d1a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## Unreliable 1.0.1 (June 4, 2026) ##
2
9
 
3
10
  ### Changed
@@ -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.1"
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.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - James McCarthy
@@ -69,6 +69,7 @@ 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
@@ -95,6 +96,7 @@ files:
95
96
  - spec/model_update_arel_10_spec.rb
96
97
  - spec/pluck_exists_aggregate_spec.rb
97
98
  - spec/railtie_spec.rb
99
+ - spec/select_distinct_raw_spec.rb
98
100
  - spec/spec_helper.rb
99
101
  - spec/textual_order_raw_spec.rb
100
102
  - spec/textual_order_spec.rb
@@ -119,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
121
  - !ruby/object:Gem::Version
120
122
  version: '0'
121
123
  requirements: []
122
- rubygems_version: 4.0.10
124
+ rubygems_version: 4.0.16
123
125
  specification_version: 4
124
126
  summary: For ActiveRecord tests, surface ambiguous-ordering bugs
125
127
  test_files: []