Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/graphql/pagination/keyset')
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition_spec.rb115
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/conditions/null_condition_spec.rb95
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_generic_keyset_spec.rb415
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb595
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb118
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/query_builder_spec.rb135
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