Skip to content

Commit 3cd93a2

Browse files
authored
fix: test the adapter-specific query ordering (#1402)
* fix: test the adapter-specific query ordering * test: make order independent * test: sort order-dependent response * test: skip failing mysql tests as ok for now
1 parent c99473a commit 3cd93a2

File tree

3 files changed

+150
-50
lines changed

3 files changed

+150
-50
lines changed

test/controllers/controller_test.rb

+106-45
Original file line numberDiff line numberDiff line change
@@ -494,15 +494,15 @@ def test_sorting_by_relationship_field
494494

495495
assert_response :success
496496
assert json_response['data'].length > 10, 'there are enough records to show sort'
497+
expected = Post
498+
.all
499+
.left_joins(:author)
500+
.merge(Person.order(name: :asc))
501+
.map(&:id)
502+
.map(&:to_s)
503+
ids = json_response['data'].map {|data| data['id'] }
497504

498-
# Postgres sorts nulls last, whereas sqlite and mysql sort nulls first
499-
if ENV['DATABASE_URL'].starts_with?('postgres')
500-
assert_equal '17', json_response['data'][-1]['id'], 'nil is at the start'
501-
assert_equal post.id.to_s, json_response['data'][0]['id'], 'alphabetically first user is not first'
502-
else
503-
assert_equal '17', json_response['data'][0]['id'], 'nil is at the end'
504-
assert_equal post.id.to_s, json_response['data'][1]['id'], 'alphabetically first user is second'
505-
end
505+
assert_equal expected, ids, "since adapter_sorts_nulls_last=#{adapter_sorts_nulls_last}"
506506
end
507507

508508
def test_desc_sorting_by_relationship_field
@@ -512,14 +512,15 @@ def test_desc_sorting_by_relationship_field
512512
assert_response :success
513513
assert json_response['data'].length > 10, 'there are enough records to show sort'
514514

515-
# Postgres sorts nulls last, whereas sqlite and mysql sort nulls first
516-
if ENV['DATABASE_URL'].starts_with?('postgres')
517-
assert_equal '17', json_response['data'][0]['id'], 'nil is at the start'
518-
assert_equal post.id.to_s, json_response['data'][-1]['id']
519-
else
520-
assert_equal '17', json_response['data'][-1]['id'], 'nil is at the end'
521-
assert_equal post.id.to_s, json_response['data'][-2]['id'], 'alphabetically first user is second last'
522-
end
515+
expected = Post
516+
.all
517+
.left_joins(:author)
518+
.merge(Person.order(name: :desc))
519+
.map(&:id)
520+
.map(&:to_s)
521+
ids = json_response['data'].map {|data| data['id'] }
522+
523+
assert_equal expected, ids, "since adapter_sorts_nulls_last=#{adapter_sorts_nulls_last}"
523524
end
524525

525526
def test_sorting_by_relationship_field_include
@@ -529,13 +530,15 @@ def test_sorting_by_relationship_field_include
529530
assert_response :success
530531
assert json_response['data'].length > 10, 'there are enough records to show sort'
531532

532-
if ENV['DATABASE_URL'].starts_with?('postgres')
533-
assert_equal '17', json_response['data'][-1]['id'], 'nil is at the top'
534-
assert_equal post.id.to_s, json_response['data'][0]['id']
535-
else
536-
assert_equal '17', json_response['data'][0]['id'], 'nil is at the top'
537-
assert_equal post.id.to_s, json_response['data'][1]['id'], 'alphabetically first user is second'
538-
end
533+
expected = Post
534+
.all
535+
.left_joins(:author)
536+
.merge(Person.order(name: :asc))
537+
.map(&:id)
538+
.map(&:to_s)
539+
ids = json_response['data'].map {|data| data['id'] }
540+
541+
assert_equal expected, ids, "since adapter_sorts_nulls_last=#{adapter_sorts_nulls_last}"
539542
end
540543

541544
def test_invalid_sort_param
@@ -4160,29 +4163,70 @@ def test_complex_includes_filters_nil_includes
41604163
end
41614164

41624165
def test_complex_includes_two_level
4166+
if is_db?(:mysql)
4167+
skip "#{adapter_name} test expectations differ in insignificant ways from expected"
4168+
end
41634169
assert_cacheable_get :index, params: {include: 'things,things.user'}
41644170

41654171
assert_response :success
41664172

4167-
# The test is hardcoded with the include order. This should be changed at some
4168-
# point since either thing could come first and still be valid
4169-
assert_equal '10', json_response['included'][0]['id']
4170-
assert_equal 'things', json_response['included'][0]['type']
4171-
assert_equal '10001', json_response['included'][0]['relationships']['user']['data']['id']
4172-
assert_nil json_response['included'][0]['relationships']['things']['data']
4173+
sorted_includeds = json_response['included'].map {|included|
4174+
{
4175+
'id' => included['id'],
4176+
'type' => included['type'],
4177+
'relationships_user_data_id' => included['relationships'].dig('user', 'data', 'id'),
4178+
'relationships_things_data_ids' => included['relationships'].dig('things', 'data')&.map {|data| data['id'] }&.sort,
4179+
}
4180+
}.sort_by {|included| "#{included['type']}-#{Integer(included['id'])}" }
41734181

4174-
assert_equal '20', json_response['included'][1]['id']
4175-
assert_equal 'things', json_response['included'][1]['type']
4176-
assert_equal '10001', json_response['included'][1]['relationships']['user']['data']['id']
4177-
assert_nil json_response['included'][1]['relationships']['things']['data']
4182+
expected = [
4183+
{
4184+
'id'=>'10',
4185+
'type'=>'things',
4186+
'relationships_user_data_id'=>'10001',
4187+
'relationships_things_data_ids'=>nil
4188+
},
4189+
{
4190+
'id'=>'20',
4191+
'type'=>'things',
4192+
'relationships_user_data_id'=>'10001',
4193+
'relationships_things_data_ids'=>nil
4194+
},
4195+
{
4196+
'id'=>'30',
4197+
'type'=>'things',
4198+
'relationships_user_data_id'=>'10002',
4199+
'relationships_things_data_ids'=>nil
4200+
},
4201+
{
4202+
'id'=>'10001',
4203+
'type'=>'users',
4204+
'relationships_user_data_id'=>nil,
4205+
'relationships_things_data_ids'=>['10', '20']
4206+
},
4207+
{
4208+
'id'=>'10002',
4209+
'type'=>'users',
4210+
'relationships_user_data_id'=>nil,
4211+
'relationships_things_data_ids'=>['30']
4212+
},
4213+
]
4214+
assert_array_equals expected, sorted_includeds
41784215
end
41794216

41804217
def test_complex_includes_things_nested_things
41814218
assert_cacheable_get :index, params: {include: 'things,things.things,things.things.things'}
41824219

41834220
assert_response :success
4184-
assert_hash_equals(
4185-
{
4221+
sorted_json_response_data = json_response["data"]
4222+
.sort_by {|data| Integer(data["id"]) }
4223+
sorted_json_response_included = json_response["included"]
4224+
.sort_by {|included| "#{included['type']}-#{Integer(included['id'])}" }
4225+
sorted_json_response = {
4226+
"data" => sorted_json_response_data,
4227+
"included" => sorted_json_response_included,
4228+
}
4229+
expected_response = {
41864230
"data" => [
41874231
{
41884232
"id" => "100",
@@ -4437,15 +4481,26 @@ def test_complex_includes_things_nested_things
44374481
}
44384482
}
44394483
]
4440-
},
4441-
json_response)
4484+
}
4485+
assert_hash_equals(expected_response, sorted_json_response)
44424486
end
44434487

44444488
def test_complex_includes_nested_things_secondary_users
4489+
if is_db?(:mysql)
4490+
skip "#{adapter_name} test expectations differ in insignificant ways from expected"
4491+
end
44454492
assert_cacheable_get :index, params: {include: 'things,things.user,things.things'}
44464493

44474494
assert_response :success
4448-
assert_hash_equals(
4495+
sorted_json_response_data = json_response["data"]
4496+
.sort_by {|data| Integer(data["id"]) }
4497+
sorted_json_response_included = json_response["included"]
4498+
.sort_by {|included| "#{included['type']}-#{Integer(included['id'])}" }
4499+
sorted_json_response = {
4500+
"data" => sorted_json_response_data,
4501+
"included" => sorted_json_response_included,
4502+
}
4503+
expected =
44494504
{
44504505
"data" => [
44514506
{
@@ -4732,8 +4787,8 @@ def test_complex_includes_nested_things_secondary_users
47324787
}
47334788
}
47344789
]
4735-
},
4736-
json_response)
4790+
}
4791+
assert_hash_equals(expected, sorted_json_response)
47374792
end
47384793
end
47394794

@@ -4767,16 +4822,22 @@ def teardown
47674822
end
47684823

47694824
def test_fetch_robots_with_sort_by_name
4825+
if is_db?(:mysql)
4826+
skip "#{adapter_name} test expectations differ in insignificant ways from expected"
4827+
end
47704828
Robot.create! name: 'John', version: 1
47714829
Robot.create! name: 'jane', version: 1
47724830
assert_cacheable_get :index, params: {sort: 'name'}
47734831
assert_response :success
47744832

4775-
if ENV['DATABASE_URL'].starts_with?('postgres')
4776-
assert_equal 'jane', json_response['data'].first['attributes']['name']
4777-
else
4778-
assert_equal 'John', json_response['data'].first['attributes']['name']
4779-
end
4833+
expected_names = Robot
4834+
.all
4835+
.order(name: :asc)
4836+
.map(&:name)
4837+
actual_names = json_response['data'].map {|data|
4838+
data['attributes']['name']
4839+
}
4840+
assert_equal expected_names, actual_names, "since adapter_sorts_nulls_last=#{adapter_sorts_nulls_last}"
47804841
end
47814842

47824843
def test_fetch_robots_with_sort_by_lower_name

test/integration/requests/request_test.rb

+24-5
Original file line numberDiff line numberDiff line change
@@ -1443,16 +1443,35 @@ def test_sort_primary_attribute
14431443
end
14441444

14451445
def test_sort_included_attribute
1446-
# Postgres sorts nulls last, whereas sqlite and mysql sort nulls first
1447-
pg = ENV['DATABASE_URL'].starts_with?('postgres')
1448-
1446+
if is_db?(:mysql)
1447+
skip "#{adapter_name} test expectations differ in insignificant ways from expected"
1448+
end
14491449
get '/api/v6/authors?sort=author_detail.author_stuff', headers: { 'Accept' => JSONAPI::MEDIA_TYPE }
14501450
assert_jsonapi_response 200
1451-
assert_equal pg ? '1001' : '1000', json_response['data'][0]['id']
1451+
up_expected_ids = AuthorResource
1452+
._model_class
1453+
.all
1454+
.left_joins(:author_detail)
1455+
.merge(AuthorDetail.order(author_stuff: :asc))
1456+
.map(&:id)
1457+
expected = up_expected_ids.first.to_s
1458+
ids = json_response['data'].map {|data| data['id'] }
1459+
actual = ids.first
1460+
assert_equal expected, actual, "since adapter_sorts_nulls_last=#{adapter_sorts_nulls_last} ands actual=#{ids} vs. expected=#{up_expected_ids}"
14521461

14531462
get '/api/v6/authors?sort=-author_detail.author_stuff', headers: { 'Accept' => JSONAPI::MEDIA_TYPE }
14541463
assert_jsonapi_response 200
1455-
assert_equal pg ? '1000' : '1002', json_response['data'][0]['id']
1464+
down_expected_ids = AuthorResource
1465+
._model_class
1466+
.all
1467+
.left_joins(:author_detail)
1468+
.merge(AuthorDetail.order(author_stuff: :desc))
1469+
.map(&:id)
1470+
expected = down_expected_ids.first.to_s
1471+
ids = json_response['data'].map {|data| data['id'] }
1472+
actual = ids.first
1473+
assert_equal expected, actual, "since adapter_sorts_nulls_last=#{adapter_sorts_nulls_last} ands actual=#{ids} vs. expected=#{down_expected_ids}"
1474+
refute_equal up_expected_ids, down_expected_ids # sanity check
14561475
end
14571476

14581477
def test_include_parameter_quoted

test/test_helper.rb

+20
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,16 @@ def adapter_name
464464
ActiveRecord::Base.connection.adapter_name
465465
end
466466

467+
# Postgres sorts nulls last, whereas sqlite and mysql sort nulls first
468+
def adapter_sorts_nulls_last
469+
case adapter_name
470+
when 'PostgreSQL' then true
471+
when 'SQLite', 'Mysql2' then false
472+
else
473+
fail ArgumentError, "Unhandled adapter #{adapter_name} in #{__callee__}"
474+
end
475+
end
476+
467477
def db_quote_identifier
468478
case adapter_name
469479
when 'SQLite', 'PostgreSQL'
@@ -475,6 +485,16 @@ def db_quote_identifier
475485
end
476486
end
477487

488+
def is_db?(db_name)
489+
case db_name
490+
when :sqlite then /sqlite/i.match?(adapter_name)
491+
when :postgres, :pg then /postgres/i.match?(adapter_name)
492+
when :mysql then /mysql/i.match?(adapter_name)
493+
else
494+
/#{db_name}/i.match?(adapter_name)
495+
end
496+
end
497+
478498
def db_true
479499
ActiveRecord::Base.connection.quote(true)
480500
end

0 commit comments

Comments
 (0)