diff options
Diffstat (limited to 'spec/lib/gitlab/graphql/pagination/keyset')
6 files changed, 261 insertions, 1212 deletions
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb deleted file mode 100644 index eecdaa3409f..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb +++ /dev/null @@ -1,115 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::Conditions::NotNullCondition do - describe '#build' do - let(:operators) { ['>', '>'] } - let(:before_or_after) { :after } - let(:condition) { described_class.new(arel_table, order_list, values, operators, before_or_after) } - - context 'when there is only one ordering field' do - let(:arel_table) { Issue.arel_table } - let(:order_list) { [double(named_function: nil, attribute_name: 'id')] } - let(:values) { [500] } - let(:operators) { ['>'] } - - it 'generates a single condition sql' do - expected_sql = <<~SQL - ("issues"."id" > 500) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when ordering by a column attribute' do - let(:arel_table) { Issue.arel_table } - let(:order_list) { [double(named_function: nil, attribute_name: 'relative_position'), double(named_function: nil, attribute_name: 'id')] } - let(:values) { [1500, 500] } - - shared_examples ':after condition' do - it 'generates :after sql' do - expected_sql = <<~SQL - ("issues"."relative_position" > 1500) - OR ( - "issues"."relative_position" = 1500 - AND - "issues"."id" > 500 - ) - OR ("issues"."relative_position" IS NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :after' do - it_behaves_like ':after condition' - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - ("issues"."relative_position" > 1500) - OR ( - "issues"."relative_position" = 1500 - AND - "issues"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :foo' do - let(:before_or_after) { :foo } - - it_behaves_like ':after condition' - end - end - - context 'when ordering by LOWER' do - let(:arel_table) { Project.arel_table } - let(:relation) { Project.order(arel_table['name'].lower.asc).order(:id) } - let(:order_list) { Gitlab::Graphql::Pagination::Keyset::OrderInfo.build_order_list(relation) } - let(:values) { ['Test', 500] } - - context 'when :after' do - it 'generates :after sql' do - expected_sql = <<~SQL - (LOWER("projects"."name") > 'test') - OR ( - LOWER("projects"."name") = 'test' - AND - "projects"."id" > 500 - ) - OR (LOWER("projects"."name") IS NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - (LOWER("projects"."name") > 'test') - OR ( - LOWER("projects"."name") = 'test' - AND - "projects"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb deleted file mode 100644 index 582f96299ec..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::Conditions::NullCondition do - describe '#build' do - let(:values) { [nil, 500] } - let(:operators) { [nil, '>'] } - let(:before_or_after) { :after } - let(:condition) { described_class.new(arel_table, order_list, values, operators, before_or_after) } - - context 'when ordering by a column attribute' do - let(:arel_table) { Issue.arel_table } - let(:order_list) { [double(named_function: nil, attribute_name: 'relative_position'), double(named_function: nil, attribute_name: 'id')] } - - shared_examples ':after condition' do - it 'generates sql' do - expected_sql = <<~SQL - ( - "issues"."relative_position" IS NULL - AND - "issues"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :after' do - it_behaves_like ':after condition' - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - ( - "issues"."relative_position" IS NULL - AND - "issues"."id" > 500 - ) - OR ("issues"."relative_position" IS NOT NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :foo' do - let(:before_or_after) { :foo } - - it_behaves_like ':after condition' - end - end - - context 'when ordering by LOWER' do - let(:arel_table) { Project.arel_table } - let(:relation) { Project.order(arel_table['name'].lower.asc).order(:id) } - let(:order_list) { Gitlab::Graphql::Pagination::Keyset::OrderInfo.build_order_list(relation) } - - context 'when :after' do - it 'generates sql' do - expected_sql = <<~SQL - ( - LOWER("projects"."name") IS NULL - AND - "projects"."id" > 500 - ) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates :before sql' do - expected_sql = <<~SQL - ( - LOWER("projects"."name") IS NULL - AND - "projects"."id" > 500 - ) - OR (LOWER("projects"."name") IS NOT NULL) - SQL - - expect(condition.build.squish).to eq expected_sql.squish - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb deleted file mode 100644 index 8a2b5ae0d38..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb +++ /dev/null @@ -1,415 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do - include GraphqlHelpers - - # https://gitlab.com/gitlab-org/gitlab/-/issues/334973 - # The spec will be merged with connection_spec.rb in the future. - let(:nodes) { Project.all.order(id: :asc) } - let(:arguments) { {} } - let(:context) { GraphQL::Query::Context.new(query: query_double, values: nil, object: nil) } - - let_it_be(:column_order_id) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'id', order_expression: Project.arel_table[:id].asc) } - let_it_be(:column_order_id_desc) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'id', order_expression: Project.arel_table[:id].desc) } - let_it_be(:column_order_updated_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'updated_at', order_expression: Project.arel_table[:updated_at].asc) } - let_it_be(:column_order_created_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'created_at', order_expression: Project.arel_table[:created_at].asc) } - let_it_be(:column_order_last_repo) do - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'last_repository_check_at', - column_expression: Project.arel_table[:last_repository_check_at], - order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, - reversed_order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, - order_direction: :asc, - nullable: :nulls_last, - distinct: false) - end - - let_it_be(:column_order_last_repo_desc) do - Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( - attribute_name: 'last_repository_check_at', - column_expression: Project.arel_table[:last_repository_check_at], - order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, - reversed_order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, - order_direction: :desc, - nullable: :nulls_last, - distinct: false) - end - - subject(:connection) do - described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments)) - end - - def encoded_cursor(node) - described_class.new(nodes, context: context).cursor_for(node) - end - - def decoded_cursor(cursor) - Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) - end - - describe "With generic keyset order support" do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - it_behaves_like 'a connection with collection methods' - - it_behaves_like 'a redactable connection' do - let_it_be(:projects) { create_list(:project, 2) } - let(:unwanted) { projects.second } - end - - describe '#cursor_for' do - let(:project) { create(:project) } - let(:cursor) { connection.cursor_for(project) } - - it 'returns an encoded ID' do - expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) - end - - context 'when an order is specified' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) - end - end - - context 'when multiple orders are specified' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end - end - end - - describe '#sliced_nodes' do - let(:projects) { create_list(:project, 4) } - - context 'when before is passed' do - let(:arguments) { { before: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end - end - end - - context 'when after is passed' do - let(:arguments) { { after: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end - - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) - end - end - end - - context 'when both before and after are passed' do - let(:arguments) do - { - after: encoded_cursor(projects[1]), - before: encoded_cursor(projects[3]) - } - end - - it 'returns the expected set' do - expect(subject.sliced_nodes).to contain_exactly(projects[2]) - end - end - - shared_examples 'nodes are in ascending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'returns projects in ascending order' do - expect(subject.sliced_nodes).to eq(ascending_nodes) - end - end - - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) - end - end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) - end - end - end - - shared_examples 'nodes are in descending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } - - it 'only returns projects in descending order' do - expect(subject.sliced_nodes).to eq(descending_nodes) - end - end - - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) - end - end - - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) - end - end - - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) - end - end - end - - context 'when multiple orders with nil values are defined' do - let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 - let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 - let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 - let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2 - let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4 - - context 'when ascending' do - let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) } - let_it_be(:nodes) { Project.order(order) } - let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] } - - it_behaves_like 'nodes are in ascending order' - - context 'when before cursor value is NULL' do - let(:arguments) { { before: encoded_cursor(project4) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project5, project1, project3, project2]) - end - end - - context 'when after cursor value is NULL' do - let(:arguments) { { after: encoded_cursor(project2) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project4]) - end - end - end - - context 'when descending' do - let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) } - let_it_be(:nodes) { Project.order(order) } - let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] } - - it_behaves_like 'nodes are in descending order' - - context 'when before cursor value is NULL' do - let(:arguments) { { before: encoded_cursor(project4) } } - - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq([project3, project1, project5, project2]) - end - end - - context 'when after cursor value is NULL' do - let(:arguments) { { after: encoded_cursor(project2) } } - - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq([project4]) - end - end - end - end - - context 'when ordering by similarity' do - let_it_be(:project1) { create(:project, name: 'test') } - let_it_be(:project2) { create(:project, name: 'testing') } - let_it_be(:project3) { create(:project, name: 'tests') } - let_it_be(:project4) { create(:project, name: 'testing stuff') } - let_it_be(:project5) { create(:project, name: 'test') } - - let_it_be(:nodes) do - # Note: sorted_by_similarity_desc scope internally supports the generic keyset order. - Project.sorted_by_similarity_desc('test', include_in_select: true) - end - - let_it_be(:descending_nodes) { nodes.to_a } - - it_behaves_like 'nodes are in descending order' - end - - context 'when an invalid cursor is provided' do - let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - - it 'raises an error' do - expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - end - - describe '#nodes' do - let_it_be(:all_nodes) { create_list(:project, 5) } - - let(:paged_nodes) { subject.nodes } - - it_behaves_like 'connection with paged nodes' do - let(:paged_nodes_size) { 3 } - end - - context 'when both are passed' do - let(:arguments) { { first: 2, last: 2 } } - - it 'raises an error' do - expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end - end - - context 'when primary key is not in original order' do - let(:nodes) { Project.order(last_repository_check_at: :desc) } - - it 'is added to end' do - sliced = subject.sliced_nodes - - order_sql = sliced.order_values.last.to_sql - - expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) - end - end - - context 'when there is no primary key' do - before do - stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) - NoPrimaryKey.class_eval do - self.table_name = 'no_primary_key' - self.primary_key = nil - end - end - - let(:nodes) { NoPrimaryKey.all } - - it 'raises an error' do - expect(NoPrimaryKey.primary_key).to be_nil - expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') - end - end - end - - describe '#has_previous_page and #has_next_page' do - # using a list of 5 items with a max_page of 3 - let_it_be(:project_list) { create_list(:project, 5) } - let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - - context 'when default query' do - let(:arguments) { {} } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before is first item' do - let(:arguments) { { before: encoded_cursor(project_list.first) } } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - describe 'using `before`' do - context 'when before is the last item' do - let(:arguments) { { before: encoded_cursor(project_list.last) } } - - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last specified' do - let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last does request all remaining nodes' do - let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - expect(subject.nodes).to eq [project_list[0]] - end - end - end - - describe 'using `after`' do - context 'when after is the first item' do - let(:arguments) { { after: encoded_cursor(project_list.first) } } - - it 'has a previous, and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when after and first specified' do - let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } - - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy - end - end - - context 'when before and last does request all remaining nodes' do - let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } - - it 'has a previous but no next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_falsey - end - end - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index 6574b3e3131..b54c618d8e0 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -5,10 +5,38 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do include GraphqlHelpers + # https://gitlab.com/gitlab-org/gitlab/-/issues/334973 + # The spec will be merged with connection_spec.rb in the future. let(:nodes) { Project.all.order(id: :asc) } let(:arguments) { {} } let(:context) { GraphQL::Query::Context.new(query: query_double, values: nil, object: nil) } + let_it_be(:column_order_id) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'id', order_expression: Project.arel_table[:id].asc) } + let_it_be(:column_order_id_desc) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'id', order_expression: Project.arel_table[:id].desc) } + let_it_be(:column_order_updated_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'updated_at', order_expression: Project.arel_table[:updated_at].asc) } + let_it_be(:column_order_created_at) { Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(attribute_name: 'created_at', order_expression: Project.arel_table[:created_at].asc) } + let_it_be(:column_order_last_repo) do + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'last_repository_check_at', + column_expression: Project.arel_table[:last_repository_check_at], + order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, + reversed_order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, + order_direction: :asc, + nullable: :nulls_last, + distinct: false) + end + + let_it_be(:column_order_last_repo_desc) do + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'last_repository_check_at', + column_expression: Project.arel_table[:last_repository_check_at], + order_expression: Project.arel_table[:last_repository_check_at].desc.nulls_last, + reversed_order_expression: Project.arel_table[:last_repository_check_at].asc.nulls_last, + order_direction: :desc, + nullable: :nulls_last, + distinct: false) + end + subject(:connection) do described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments)) end @@ -21,414 +49,293 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor)) end - # see: https://gitlab.com/gitlab-org/gitlab/-/issues/297358 - context 'the relation has been preloaded' do - let(:projects) { Project.all.preload(:issues) } - let(:nodes) { projects.first.issues } - - before do - project = create(:project) - create_list(:issue, 3, project: project) - end - - it 'is loaded' do - expect(nodes).to be_loaded - end - - it 'does not error when accessing pagination information' do - connection.first = 2 - - expect(connection).to have_attributes( - has_previous_page: false, - has_next_page: true - ) - end - - it 'can generate cursors' do - connection.send(:ordered_items) # necessary to generate the order-list - - expect(connection.cursor_for(nodes.first)).to be_a(String) - end - - it 'can read the next page' do - connection.send(:ordered_items) # necessary to generate the order-list - ordered = nodes.reorder(id: :desc) - next_page = described_class.new(nodes, - context: context, - max_page_size: 3, - after: connection.cursor_for(ordered.second)) - - expect(next_page.sliced_nodes).to contain_exactly(ordered.third) - end - end - - it_behaves_like 'a connection with collection methods' - - it_behaves_like 'a redactable connection' do - let_it_be(:projects) { create_list(:project, 2) } - let(:unwanted) { projects.second } - end - - describe '#cursor_for' do - let(:project) { create(:project) } - let(:cursor) { connection.cursor_for(project) } - - it 'returns an encoded ID' do - expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) - end - - context 'when SimpleOrderBuilder cannot build keyset paginated query' do - it 'increments the `old_keyset_pagination_usage` counter', :prometheus do - expect(Gitlab::Pagination::Keyset::SimpleOrderBuilder).to receive(:build).and_return([false, nil]) - - decoded_cursor(cursor) - - counter = Gitlab::Metrics.registry.get(:old_keyset_pagination_usage) - expect(counter.get(model: 'Project')).to eq(1) - end - end - - context 'when an order is specified' do - let(:nodes) { Project.order(:updated_at) } + describe "with generic keyset order support" do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end - - it 'includes the :id even when not specified in the order' do - expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) - end - end + it_behaves_like 'a connection with collection methods' - context 'when multiple orders are specified' do - let(:nodes) { Project.order(:updated_at).order(:created_at) } - - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) - end + it_behaves_like 'a redactable connection' do + let_it_be(:projects) { create_list(:project, 2) } + let(:unwanted) { projects.second } end - context 'when multiple orders with SQL are specified' do - let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } + describe '#cursor_for' do + let(:project) { create(:project) } + let(:cursor) { connection.cursor_for(project) } - it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) + it 'returns an encoded ID' do + expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s) end - end - end - describe '#sliced_nodes' do - let(:projects) { create_list(:project, 4) } + context 'when an order is specified' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - context 'when before is passed' do - let(:arguments) { { before: encoded_cursor(projects[1]) } } - - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(projects.first) + it 'returns the encoded value of the order' do + expect(decoded_cursor(cursor)).to include('id' => project.id.to_s) + end end - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(id: :desc) } + context 'when multiple orders are specified' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_updated_at, column_order_created_at, column_order_id])) } - it 'returns the correct nodes' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) + it 'returns the encoded value of the order' do + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s(:inspect)) end end end - context 'when after is passed' do - let(:arguments) { { after: encoded_cursor(projects[1]) } } + describe '#sliced_nodes' do + let(:projects) { create_list(:project, 4) } - it 'only returns the project before the selected one' do - expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) - end + context 'when before is passed' do + let(:arguments) { { before: encoded_cursor(projects[1]) } } - context 'when the sort order is descending' do - let(:nodes) { Project.all.order(id: :desc) } - - it 'returns the correct nodes' do + it 'only returns the project before the selected one' do expect(subject.sliced_nodes).to contain_exactly(projects.first) end - end - end - context 'when both before and after are passed' do - let(:arguments) do - { - after: encoded_cursor(projects[1]), - before: encoded_cursor(projects[3]) - } - end + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - it 'returns the expected set' do - expect(subject.sliced_nodes).to contain_exactly(projects[2]) + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) + end + end end - end - shared_examples 'nodes are in ascending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } + context 'when after is passed' do + let(:arguments) { { after: encoded_cursor(projects[1]) } } - it 'returns projects in ascending order' do - expect(subject.sliced_nodes).to eq(ascending_nodes) + it 'only returns the project before the selected one' do + expect(subject.sliced_nodes).to contain_exactly(*projects[2..]) end - end - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } + context 'when the sort order is descending' do + let(:nodes) { Project.all.order(Gitlab::Pagination::Keyset::Order.build([column_order_id_desc])) } - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) + it 'returns the correct nodes' do + expect(subject.sliced_nodes).to contain_exactly(projects.first) + end end end - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } + context 'when both before and after are passed' do + let(:arguments) do + { + after: encoded_cursor(projects[1]), + before: encoded_cursor(projects[3]) + } + end - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) + it 'returns the expected set' do + expect(subject.sliced_nodes).to contain_exactly(projects[2]) end end - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } + shared_examples 'nodes are in ascending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) + it 'returns projects in ascending order' do + expect(subject.sliced_nodes).to eq(ascending_nodes) + end end - end - end - shared_examples 'nodes are in descending order' do - context 'when no cursor is passed' do - let(:arguments) { {} } + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(ascending_nodes[2]) } } - it 'only returns projects in descending order' do - expect(subject.sliced_nodes).to eq(descending_nodes) + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.first(2)) + end end - end - context 'when before cursor value is not NULL' do - let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(ascending_nodes[1]) } } - it 'returns all projects before the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes.last(3)) + end end - end - context 'when after cursor value is not NULL' do - let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(ascending_nodes.last), after: encoded_cursor(ascending_nodes.first) } } - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(ascending_nodes[1..3]) + end end end - context 'when before and after cursor' do - let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } + shared_examples 'nodes are in descending order' do + context 'when no cursor is passed' do + let(:arguments) { {} } - it 'returns all projects after the cursor' do - expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) + it 'only returns projects in descending order' do + expect(subject.sliced_nodes).to eq(descending_nodes) + end end - end - end - context 'when ordering uses LOWER' do - let!(:project1) { create(:project, name: 'A') } # Asc: project1 Desc: project4 - let!(:project2) { create(:project, name: 'c') } # Asc: project5 Desc: project2 - let!(:project3) { create(:project, name: 'b') } # Asc: project3 Desc: project3 - let!(:project4) { create(:project, name: 'd') } # Asc: project2 Desc: project5 - let!(:project5) { create(:project, name: 'a') } # Asc: project4 Desc: project1 + context 'when before cursor value is not NULL' do + let(:arguments) { { before: encoded_cursor(descending_nodes[2]) } } - context 'when ascending' do - let(:nodes) do - Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(id: :asc) + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.first(2)) + end end - let(:ascending_nodes) { [project1, project5, project3, project2, project4] } + context 'when after cursor value is not NULL' do + let(:arguments) { { after: encoded_cursor(descending_nodes[1]) } } - it_behaves_like 'nodes are in ascending order' - end - - context 'when descending' do - let(:nodes) do - Project.order(Arel::Table.new(:projects)['name'].lower.desc).order(id: :desc) + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes.last(3)) + end end - let(:descending_nodes) { [project4, project2, project3, project5, project1] } + context 'when before and after cursor' do + let(:arguments) { { before: encoded_cursor(descending_nodes.last), after: encoded_cursor(descending_nodes.first) } } - it_behaves_like 'nodes are in descending order' + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq(descending_nodes[1..3]) + end + end end - end - context 'NULLS order' do - using RSpec::Parameterized::TableSyntax + context 'when multiple orders with nil values are defined' do + let_it_be(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3 + let_it_be(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1 + let_it_be(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5 + let_it_be(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2 + let_it_be(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4 - let_it_be(:issue1) { create(:issue, relative_position: nil) } - let_it_be(:issue2) { create(:issue, relative_position: 100) } - let_it_be(:issue3) { create(:issue, relative_position: 200) } - let_it_be(:issue4) { create(:issue, relative_position: nil) } - let_it_be(:issue5) { create(:issue, relative_position: 300) } + context 'when ascending' do + let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo, column_order_id]) } + let_it_be(:nodes) { Project.order(order) } + let_it_be(:ascending_nodes) { [project5, project1, project3, project2, project4] } - context 'when ascending NULLS LAST (ties broken by id DESC implicitly)' do - let(:ascending_nodes) { [issue2, issue3, issue5, issue4, issue1] } + it_behaves_like 'nodes are in ascending order' - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].asc.nulls_last) } - ] - end + context 'when before cursor value is NULL' do + let(:arguments) { { before: encoded_cursor(project4) } } - with_them do - it_behaves_like 'nodes are in ascending order' - end - end + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq([project5, project1, project3, project2]) + end + end - context 'when descending NULLS LAST (ties broken by id DESC implicitly)' do - let(:descending_nodes) { [issue5, issue3, issue2, issue4, issue1] } + context 'when after cursor value is NULL' do + let(:arguments) { { after: encoded_cursor(project2) } } - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].desc.nulls_last) } -] + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq([project4]) + end + end end - with_them do + context 'when descending' do + let_it_be(:order) { Gitlab::Pagination::Keyset::Order.build([column_order_last_repo_desc, column_order_id]) } + let_it_be(:nodes) { Project.order(order) } + let_it_be(:descending_nodes) { [project3, project1, project5, project2, project4] } + it_behaves_like 'nodes are in descending order' - end - end - context 'when ascending NULLS FIRST with a tie breaker' do - let(:ascending_nodes) { [issue1, issue4, issue2, issue3, issue5] } + context 'when before cursor value is NULL' do + let(:arguments) { { before: encoded_cursor(project4) } } - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].asc.nulls_first).order(id: :asc) } -] - end + it 'returns all projects before the cursor' do + expect(subject.sliced_nodes).to eq([project3, project1, project5, project2]) + end + end - with_them do - it_behaves_like 'nodes are in ascending order' + context 'when after cursor value is NULL' do + let(:arguments) { { after: encoded_cursor(project2) } } + + it 'returns all projects after the cursor' do + expect(subject.sliced_nodes).to eq([project4]) + end + end end end - context 'when descending NULLS FIRST with a tie breaker' do - let(:descending_nodes) { [issue1, issue4, issue5, issue3, issue2] } + context 'when ordering by similarity' do + let_it_be(:project1) { create(:project, name: 'test') } + let_it_be(:project2) { create(:project, name: 'testing') } + let_it_be(:project3) { create(:project, name: 'tests') } + let_it_be(:project4) { create(:project, name: 'testing stuff') } + let_it_be(:project5) { create(:project, name: 'test') } - where(:nodes) do - [ - lazy { Issue.order(Issue.arel_table[:relative_position].desc.nulls_first).order(id: :asc) } -] + let_it_be(:nodes) do + # Note: sorted_by_similarity_desc scope internally supports the generic keyset order. + Project.sorted_by_similarity_desc('test', include_in_select: true) end - with_them do - it_behaves_like 'nodes are in descending order' - end - end - end + let_it_be(:descending_nodes) { nodes.to_a } - context 'when ordering by similarity' do - let!(:project1) { create(:project, name: 'test') } - let!(:project2) { create(:project, name: 'testing') } - let!(:project3) { create(:project, name: 'tests') } - let!(:project4) { create(:project, name: 'testing stuff') } - let!(:project5) { create(:project, name: 'test') } - - let(:nodes) do - Project.sorted_by_similarity_desc('test', include_in_select: true) + it_behaves_like 'nodes are in descending order' end - let(:descending_nodes) { nodes.to_a } - - it_behaves_like 'nodes are in descending order' - end + context 'when an invalid cursor is provided' do + let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - context 'when an invalid cursor is provided' do - let(:arguments) { { before: Base64Bp.urlsafe_encode64('invalidcursor', padding: false) } } - - it 'raises an error' do - expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + it 'raises an error' do + expect { subject.sliced_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end end end - end - describe '#nodes' do - let_it_be(:all_nodes) { create_list(:project, 5) } + describe '#nodes' do + let_it_be(:all_nodes) { create_list(:project, 5) } - let(:paged_nodes) { subject.nodes } + let(:paged_nodes) { subject.nodes } - it_behaves_like 'connection with paged nodes' do - let(:paged_nodes_size) { 3 } - end - - context 'when both are passed' do - let(:arguments) { { first: 2, last: 2 } } - - it 'raises an error' do - expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + it_behaves_like 'connection with paged nodes' do + let(:paged_nodes_size) { 3 } end - end - context 'when primary key is not in original order' do - let(:nodes) { Project.order(last_repository_check_at: :desc) } + context 'when both are passed' do + let(:arguments) { { first: 2, last: 2 } } - before do - stub_feature_flags(new_graphql_keyset_pagination: false) + it 'raises an error' do + expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end end - it 'is added to end' do - sliced = subject.sliced_nodes + context 'when primary key is not in original order' do + let(:nodes) { Project.order(last_repository_check_at: :desc) } - order_sql = sliced.order_values.last.to_sql + it 'is added to end' do + sliced = subject.sliced_nodes - expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) - end - end + order_sql = sliced.order_values.last.to_sql - context 'when there is no primary key' do - before do - stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) - NoPrimaryKey.class_eval do - self.table_name = 'no_primary_key' - self.primary_key = nil + expect(order_sql).to end_with(Project.arel_table[:id].desc.to_sql) end end - let(:nodes) { NoPrimaryKey.all } - - it 'raises an error' do - expect(NoPrimaryKey.primary_key).to be_nil - expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') - end - end - end - - describe '#has_previous_page and #has_next_page' do - # using a list of 5 items with a max_page of 3 - let_it_be(:project_list) { create_list(:project, 5) } - let_it_be(:nodes) { Project.order(:id) } + context 'when there is no primary key' do + before do + stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base)) + NoPrimaryKey.class_eval do + self.table_name = 'no_primary_key' + self.primary_key = nil + end + end - context 'when default query' do - let(:arguments) { {} } + let(:nodes) { NoPrimaryKey.all } - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy + it 'raises an error' do + expect(NoPrimaryKey.primary_key).to be_nil + expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key') + end end end - context 'when before is first item' do - let(:arguments) { { before: encoded_cursor(project_list.first) } } + describe '#has_previous_page and #has_next_page' do + # using a list of 5 items with a max_page of 3 + let_it_be(:project_list) { create_list(:project, 5) } + let_it_be(:nodes) { Project.order(Gitlab::Pagination::Keyset::Order.build([column_order_id])) } - it 'has no previous, but a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - end - end - - describe 'using `before`' do - context 'when before is the last item' do - let(:arguments) { { before: encoded_cursor(project_list.last) } } + context 'when default query' do + let(:arguments) { {} } it 'has no previous, but a next' do expect(subject.has_previous_page).to be_falsey @@ -436,51 +343,71 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do end end - context 'when before and last specified' do - let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } + context 'when before is first item' do + let(:arguments) { { before: encoded_cursor(project_list.first) } } - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey expect(subject.has_next_page).to be_truthy end end - context 'when before and last does request all remaining nodes' do - let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } + describe 'using `before`' do + context 'when before is the last item' do + let(:arguments) { { before: encoded_cursor(project_list.last) } } - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_falsey - expect(subject.has_next_page).to be_truthy - expect(subject.nodes).to eq [project_list[0]] + it 'has no previous, but a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + end end - end - end - describe 'using `after`' do - context 'when after is the first item' do - let(:arguments) { { after: encoded_cursor(project_list.first) } } + context 'when before and last specified' do + let(:arguments) { { before: encoded_cursor(project_list.last), last: 2 } } - it 'has a previous, and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end end - end - context 'when after and first specified' do - let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } + context 'when before and last does request all remaining nodes' do + let(:arguments) { { before: encoded_cursor(project_list[1]), last: 3 } } - it 'has a previous and a next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_truthy + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_falsey + expect(subject.has_next_page).to be_truthy + expect(subject.nodes).to eq [project_list[0]] + end end end - context 'when before and last does request all remaining nodes' do - let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } + describe 'using `after`' do + context 'when after is the first item' do + let(:arguments) { { after: encoded_cursor(project_list.first) } } + + it 'has a previous, and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when after and first specified' do + let(:arguments) { { after: encoded_cursor(project_list.first), first: 2 } } + + it 'has a previous and a next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_truthy + end + end + + context 'when before and last does request all remaining nodes' do + let(:arguments) { { after: encoded_cursor(project_list[2]), last: 3 } } - it 'has a previous but no next' do - expect(subject.has_previous_page).to be_truthy - expect(subject.has_next_page).to be_falsey + it 'has a previous but no next' do + expect(subject.has_previous_page).to be_truthy + expect(subject.has_next_page).to be_falsey + end end end end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb deleted file mode 100644 index 40ee47ece49..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do - describe '#build_order_list' do - let(:order_list) { described_class.build_order_list(relation) } - - context 'when multiple orders with SQL is specified' do - let(:relation) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } - - it 'ignores the SQL order' do - expect(order_list.count).to eq 2 - expect(order_list.first.attribute_name).to eq 'updated_at' - expect(order_list.first.operator_for(:after)).to eq '>' - expect(order_list.last.attribute_name).to eq 'id' - expect(order_list.last.operator_for(:after)).to eq '>' - end - end - - context 'when order contains NULLS LAST' do - let(:relation) { Project.order(Arel.sql('projects.updated_at Asc Nulls Last')).order(:id) } - - it 'does not ignore the SQL order' do - expect(order_list.count).to eq 2 - expect(order_list.first.attribute_name).to eq 'projects.updated_at' - expect(order_list.first.operator_for(:after)).to eq '>' - expect(order_list.last.attribute_name).to eq 'id' - expect(order_list.last.operator_for(:after)).to eq '>' - end - end - - context 'when order contains invalid formatted NULLS LAST ' do - let(:relation) { Project.order(Arel.sql('projects.updated_at created_at Asc Nulls Last')).order(:id) } - - it 'ignores the SQL order' do - expect(order_list.count).to eq 1 - end - end - - context 'when order contains LOWER' do - let(:relation) { Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(:id) } - - it 'does not ignore the SQL order' do - expect(order_list.count).to eq 2 - expect(order_list.first.attribute_name).to eq 'name' - expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction) - expect(order_list.first.named_function.to_sql).to eq 'LOWER("projects"."name")' - expect(order_list.first.operator_for(:after)).to eq '>' - expect(order_list.last.attribute_name).to eq 'id' - expect(order_list.last.operator_for(:after)).to eq '>' - end - end - - context 'when ordering by CASE', :aggregate_failuers do - let(:relation) { Project.order(Arel::Nodes::Case.new(Project.arel_table[:pending_delete]).when(true).then(100).else(1000).asc) } - - it 'assigns the right attribute name, named function, and direction' do - expect(order_list.count).to eq 1 - expect(order_list.first.attribute_name).to eq 'case_order_value' - expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Case) - expect(order_list.first.sort_direction).to eq :asc - end - end - - context 'when ordering by ARRAY_POSITION', :aggregate_failuers do - let(:array_position) { Arel::Nodes::NamedFunction.new('ARRAY_POSITION', [Arel.sql("ARRAY[1,0]::smallint[]"), Project.arel_table[:auto_cancel_pending_pipelines]]) } - let(:relation) { Project.order(array_position.asc) } - - it 'assigns the right attribute name, named function, and direction' do - expect(order_list.count).to eq 1 - expect(order_list.first.attribute_name).to eq 'array_position' - expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction) - expect(order_list.first.sort_direction).to eq :asc - end - end - end - - describe '#validate_ordering' do - let(:order_list) { described_class.build_order_list(relation) } - - context 'when number of ordering fields is 0' do - let(:relation) { Project.all } - - it 'raises an error' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, 'A minimum of 1 ordering field is required') - end - end - - context 'when number of ordering fields is over 2' do - let(:relation) { Project.order(last_repository_check_at: :desc).order(updated_at: :desc).order(:id) } - - it 'raises an error' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, 'A maximum of 2 ordering fields are allowed') - end - end - - context 'when the second (or first) column is nullable' do - let(:relation) { Project.order(last_repository_check_at: :desc).order(updated_at: :desc) } - - it 'raises an error' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, "Column `updated_at` must not allow NULL") - end - end - - context 'for last ordering field' do - let(:relation) { Project.order(namespace_id: :desc) } - - it 'raises error if primary key is not last field' do - expect { described_class.validate_ordering(relation, order_list) } - .to raise_error(ArgumentError, "Last ordering field must be the primary key, `#{relation.primary_key}`") - end - end - end -end diff --git a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb deleted file mode 100644 index 31c02fd43e8..00000000000 --- a/spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Graphql::Pagination::Keyset::QueryBuilder do - context 'when number of ordering fields is 0' do - it 'raises an error' do - expect { described_class.new(Issue.arel_table, [], {}, :after) } - .to raise_error(ArgumentError, 'No ordering scopes have been supplied') - end - end - - describe '#conditions' do - let(:relation) { Issue.order(relative_position: :desc).order(:id) } - let(:order_list) { Gitlab::Graphql::Pagination::Keyset::OrderInfo.build_order_list(relation) } - let(:arel_table) { Issue.arel_table } - let(:builder) { described_class.new(arel_table, order_list, decoded_cursor, before_or_after) } - let(:before_or_after) { :after } - - context 'when only a single ordering' do - let(:relation) { Issue.order(id: :desc) } - - context 'when the value is nil' do - let(:decoded_cursor) { { 'id' => nil } } - - it 'raises an error' do - expect { builder.conditions } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Before/after cursor invalid: `nil` was provided as only sortable value') - end - end - - context 'when value is not nil' do - let(:decoded_cursor) { { 'id' => 100 } } - let(:conditions) { builder.conditions } - - context 'when :after' do - it 'generates the correct condition' do - expect(conditions.strip).to eq '("issues"."id" < 100)' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - expect(conditions.strip).to eq '("issues"."id" > 100)' - end - end - end - end - - context 'when two orderings' do - let(:decoded_cursor) { { 'relative_position' => 1500, 'id' => 100 } } - - context 'when no values are nil' do - context 'when :after' do - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '"issues"."relative_position" < 1500' - expect(conditions).to include '"issues"."id" > 100' - expect(conditions).to include 'OR ("issues"."relative_position" IS NULL)' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '("issues"."relative_position" > 1500)' - expect(conditions).to include '"issues"."id" < 100' - expect(conditions).to include '"issues"."relative_position" = 1500' - end - end - end - - context 'when first value is nil' do - let(:decoded_cursor) { { 'relative_position' => nil, 'id' => 100 } } - - context 'when :after' do - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '"issues"."relative_position" IS NULL' - expect(conditions).to include '"issues"."id" > 100' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '"issues"."relative_position" IS NULL' - expect(conditions).to include '"issues"."id" < 100' - expect(conditions).to include 'OR ("issues"."relative_position" IS NOT NULL)' - end - end - end - end - - context 'when sorting using LOWER' do - let(:relation) { Project.order(Arel::Table.new(:projects)['name'].lower.asc).order(:id) } - let(:arel_table) { Project.arel_table } - let(:decoded_cursor) { { 'name' => 'Test', 'id' => 100 } } - - context 'when no values are nil' do - context 'when :after' do - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '(LOWER("projects"."name") > \'test\')' - expect(conditions).to include '"projects"."id" > 100' - expect(conditions).to include 'OR (LOWER("projects"."name") IS NULL)' - end - end - - context 'when :before' do - let(:before_or_after) { :before } - - it 'generates the correct condition' do - conditions = builder.conditions - - expect(conditions).to include '(LOWER("projects"."name") < \'test\')' - expect(conditions).to include '"projects"."id" < 100' - expect(conditions).to include 'LOWER("projects"."name") = \'test\'' - end - end - end - end - end -end |