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 +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile +1 -1
- data/lib/unreliable/build_order.rb +13 -2
- data/lib/unreliable/version.rb +1 -1
- data/spec/computed_order_spec.rb +63 -0
- data/spec/select_distinct_raw_spec.rb +58 -0
- metadata +4 -3
- data/spec/examples.txt +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03cacbb1f651eeac84c72fe0534624779978b5c954f9e0612c89784555c1b0bb
|
|
4
|
+
data.tar.gz: b2157f68e867d7ba3ced7b0bb4bc4d0dbad964cfdbe229d915a8080e84a2fd8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -37,13 +37,23 @@ module Unreliable
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def distinct_on_postgres?(adapter_name)
|
|
40
|
-
|
|
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
|
-
|
|
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
|
data/lib/unreliable/version.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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 |
|