2021-04-17 20:07:23 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require 'spec_helper'
|
|
|
|
|
|
|
|
RSpec.describe Gitlab::Pagination::Keyset::Order do
|
2021-06-08 01:23:25 +05:30
|
|
|
describe 'paginate over items correctly' do
|
|
|
|
let(:table) { Arel::Table.new(:my_table) }
|
|
|
|
let(:order) { nil }
|
2021-10-27 15:23:28 +05:30
|
|
|
let(:default_limit) { 999 }
|
|
|
|
let(:query_building_method) { :build_query }
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
def run_query(query)
|
2021-10-27 15:23:28 +05:30
|
|
|
ApplicationRecord.connection.execute(query).to_a
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-10-27 15:23:28 +05:30
|
|
|
def where_conditions_as_sql(where_conditions)
|
|
|
|
"WHERE #{Array(where_conditions).map(&:to_sql).join(' OR ')}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_query(order:, where_conditions: [], limit: nil)
|
|
|
|
where_string = where_conditions_as_sql(where_conditions)
|
|
|
|
|
|
|
|
<<-SQL
|
|
|
|
SELECT id, year, month
|
|
|
|
FROM (#{table_data}) my_table (id, year, month)
|
|
|
|
#{where_string if where_conditions.present?}
|
|
|
|
ORDER BY #{order}
|
|
|
|
LIMIT #{limit || default_limit};
|
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
|
|
|
def build_union_query(order:, where_conditions: [], limit: nil)
|
|
|
|
return build_query(order: order, where_conditions: where_conditions, limit: limit) if where_conditions.blank?
|
|
|
|
|
|
|
|
union_queries = Array(where_conditions).map do |where_condition|
|
|
|
|
<<-SQL
|
|
|
|
(SELECT id, year, month
|
|
|
|
FROM (#{table_data}) my_table (id, year, month)
|
|
|
|
WHERE #{where_condition.to_sql}
|
|
|
|
ORDER BY #{order}
|
|
|
|
LIMIT #{limit || default_limit})
|
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
|
|
|
union_query = union_queries.join(" UNION ALL ")
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
<<-SQL
|
2021-10-27 15:23:28 +05:30
|
|
|
SELECT id, year, month
|
|
|
|
FROM (#{union_query}) as my_table
|
|
|
|
ORDER BY #{order}
|
|
|
|
LIMIT #{limit || default_limit};
|
2021-06-08 01:23:25 +05:30
|
|
|
SQL
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
2021-10-27 15:23:28 +05:30
|
|
|
def cursor_attributes_for_node(node)
|
|
|
|
order.cursor_attributes_for_node(node)
|
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
def iterate_and_collect(order:, page_size:, where_conditions: nil)
|
|
|
|
all_items = []
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
loop do
|
2021-10-27 15:23:28 +05:30
|
|
|
paginated_items = run_query(send(query_building_method, order: order, where_conditions: where_conditions, limit: page_size))
|
2021-06-08 01:23:25 +05:30
|
|
|
break if paginated_items.empty?
|
|
|
|
|
|
|
|
all_items.concat(paginated_items)
|
|
|
|
last_item = paginated_items.last
|
2021-10-27 15:23:28 +05:30
|
|
|
cursor_attributes = cursor_attributes_for_node(last_item)
|
|
|
|
where_conditions = order.build_where_values(cursor_attributes)
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
all_items
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
subject do
|
|
|
|
run_query(build_query(order: order))
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
shared_examples 'order examples' do
|
2021-04-17 20:07:23 +05:30
|
|
|
it { expect(subject).to eq(expected) }
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when paginating forwards' do
|
|
|
|
subject { iterate_and_collect(order: order, page_size: 2) }
|
2021-04-17 20:07:23 +05:30
|
|
|
|
|
|
|
it { expect(subject).to eq(expected) }
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'with different page size' do
|
|
|
|
subject { iterate_and_collect(order: order, page_size: 5) }
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it { expect(subject).to eq(expected) }
|
|
|
|
end
|
2021-10-27 15:23:28 +05:30
|
|
|
|
|
|
|
context 'when using the conditions in an UNION query' do
|
|
|
|
let(:query_building_method) { :build_union_query }
|
|
|
|
|
|
|
|
it { expect(subject).to eq(expected) }
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the cursor attributes are SQL literals' do
|
|
|
|
def cursor_attributes_for_node(node)
|
|
|
|
# Simulate the scenario where the cursor attributes are SQL literals
|
|
|
|
order.cursor_attributes_for_node(node).transform_values.each_with_index do |value, i|
|
|
|
|
index = i + 1
|
|
|
|
value_sql = value.nil? ? 'NULL::integer' : value
|
|
|
|
values = [value_sql] * index
|
|
|
|
Arel.sql("(ARRAY[#{values.join(',')}])[#{index}]") # example: ARRAY[cursor_value][1] will return cursor_value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
it { expect(subject).to eq(expected) }
|
|
|
|
|
|
|
|
context 'when using the conditions in an UNION query' do
|
|
|
|
let(:query_building_method) { :build_union_query }
|
|
|
|
|
|
|
|
it { expect(subject).to eq(expected) }
|
|
|
|
end
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when paginating backwards' do
|
|
|
|
subject do
|
|
|
|
last_item = expected.last
|
|
|
|
cursor_attributes = order.cursor_attributes_for_node(last_item)
|
2021-10-27 15:23:28 +05:30
|
|
|
where_conditions = order.reversed_order.build_where_values(cursor_attributes)
|
2021-06-08 01:23:25 +05:30
|
|
|
|
2021-10-27 15:23:28 +05:30
|
|
|
iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions)
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
it do
|
2022-01-26 12:08:38 +05:30
|
|
|
expect(subject).to eq(expected.reverse[1..]) # removing one item because we used it to calculate cursor data for the "last" page in subject
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when ordering by a distinct column' do
|
|
|
|
let(:table_data) do
|
|
|
|
<<-SQL
|
2021-04-17 20:07:23 +05:30
|
|
|
VALUES (1, 0, 0),
|
|
|
|
(2, 0, 0),
|
|
|
|
(3, 0, 0),
|
|
|
|
(4, 0, 0),
|
|
|
|
(5, 0, 0),
|
|
|
|
(6, 0, 0),
|
|
|
|
(7, 0, 0),
|
|
|
|
(8, 0, 0),
|
|
|
|
(9, 0, 0)
|
2021-06-08 01:23:25 +05:30
|
|
|
SQL
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:expected) do
|
|
|
|
[
|
|
|
|
{ "id" => 9, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 8, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 7, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 6, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 5, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 4, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 3, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 2, "year" => 0, "month" => 0 },
|
|
|
|
{ "id" => 1, "year" => 0, "month" => 0 }
|
|
|
|
]
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it_behaves_like 'order examples'
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when ordering by two non-nullable columns and a distinct column' do
|
|
|
|
let(:table_data) do
|
|
|
|
<<-SQL
|
2021-04-17 20:07:23 +05:30
|
|
|
VALUES (1, 2010, 2),
|
|
|
|
(2, 2011, 1),
|
|
|
|
(3, 2009, 2),
|
|
|
|
(4, 2011, 1),
|
|
|
|
(5, 2011, 1),
|
|
|
|
(6, 2009, 2),
|
|
|
|
(7, 2010, 3),
|
|
|
|
(8, 2012, 4),
|
|
|
|
(9, 2013, 5)
|
2021-06-08 01:23:25 +05:30
|
|
|
SQL
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'year',
|
|
|
|
column_expression: table['year'],
|
|
|
|
order_expression: table['year'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'month',
|
|
|
|
column_expression: table['month'],
|
|
|
|
order_expression: table['month'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:expected) do
|
|
|
|
[
|
|
|
|
{ 'year' => 2009, 'month' => 2, 'id' => 3 },
|
|
|
|
{ 'year' => 2009, 'month' => 2, 'id' => 6 },
|
|
|
|
{ 'year' => 2010, 'month' => 2, 'id' => 1 },
|
|
|
|
{ 'year' => 2010, 'month' => 3, 'id' => 7 },
|
|
|
|
{ 'year' => 2011, 'month' => 1, 'id' => 2 },
|
|
|
|
{ 'year' => 2011, 'month' => 1, 'id' => 4 },
|
|
|
|
{ 'year' => 2011, 'month' => 1, 'id' => 5 },
|
|
|
|
{ 'year' => 2012, 'month' => 4, 'id' => 8 },
|
|
|
|
{ 'year' => 2013, 'month' => 5, 'id' => 9 }
|
|
|
|
]
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it_behaves_like 'order examples'
|
2021-09-30 23:02:18 +05:30
|
|
|
|
|
|
|
it 'uses the row comparison method' do
|
|
|
|
sql = order.where_values_with_or_query({ year: 2010, month: 5, id: 1 }).to_sql
|
|
|
|
|
|
|
|
expect(sql).to eq('(("my_table"."year", "my_table"."month", "my_table"."id") > (2010, 5, 1))')
|
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when ordering by nullable columns and a distinct column' do
|
|
|
|
let(:table_data) do
|
|
|
|
<<-SQL
|
2021-04-17 20:07:23 +05:30
|
|
|
VALUES (1, 2010, null),
|
|
|
|
(2, 2011, 2),
|
|
|
|
(3, null, null),
|
|
|
|
(4, null, 5),
|
|
|
|
(5, 2010, null),
|
|
|
|
(6, 2011, 2),
|
|
|
|
(7, 2010, 2),
|
|
|
|
(8, 2012, 2),
|
|
|
|
(9, null, 2),
|
|
|
|
(10, null, null),
|
|
|
|
(11, 2010, 2)
|
2021-06-08 01:23:25 +05:30
|
|
|
SQL
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'year',
|
|
|
|
column_expression: table['year'],
|
|
|
|
order_expression: table[:year].asc.nulls_last,
|
|
|
|
reversed_order_expression: table[:year].desc.nulls_first,
|
|
|
|
order_direction: :asc,
|
|
|
|
nullable: :nulls_last,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'month',
|
|
|
|
column_expression: table['month'],
|
|
|
|
order_expression: table[:month].asc.nulls_last,
|
|
|
|
reversed_order_expression: table[:month].desc.nulls_first,
|
|
|
|
order_direction: :asc,
|
|
|
|
nullable: :nulls_last,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:expected) do
|
|
|
|
[
|
|
|
|
{ "id" => 7, "year" => 2010, "month" => 2 },
|
|
|
|
{ "id" => 11, "year" => 2010, "month" => 2 },
|
|
|
|
{ "id" => 1, "year" => 2010, "month" => nil },
|
|
|
|
{ "id" => 5, "year" => 2010, "month" => nil },
|
|
|
|
{ "id" => 2, "year" => 2011, "month" => 2 },
|
|
|
|
{ "id" => 6, "year" => 2011, "month" => 2 },
|
|
|
|
{ "id" => 8, "year" => 2012, "month" => 2 },
|
|
|
|
{ "id" => 9, "year" => nil, "month" => 2 },
|
|
|
|
{ "id" => 4, "year" => nil, "month" => 5 },
|
|
|
|
{ "id" => 3, "year" => nil, "month" => nil },
|
|
|
|
{ "id" => 10, "year" => nil, "month" => nil }
|
|
|
|
]
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it_behaves_like 'order examples'
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when ordering by nullable columns with nulls first ordering and a distinct column' do
|
|
|
|
let(:table_data) do
|
|
|
|
<<-SQL
|
2021-04-17 20:07:23 +05:30
|
|
|
VALUES (1, 2010, null),
|
|
|
|
(2, 2011, 2),
|
|
|
|
(3, null, null),
|
|
|
|
(4, null, 5),
|
|
|
|
(5, 2010, null),
|
|
|
|
(6, 2011, 2),
|
|
|
|
(7, 2010, 2),
|
|
|
|
(8, 2012, 2),
|
|
|
|
(9, null, 2),
|
|
|
|
(10, null, null),
|
|
|
|
(11, 2010, 2)
|
2021-06-08 01:23:25 +05:30
|
|
|
SQL
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'year',
|
|
|
|
column_expression: table['year'],
|
|
|
|
order_expression: table[:year].asc.nulls_first,
|
|
|
|
reversed_order_expression: table[:year].desc.nulls_last,
|
|
|
|
order_direction: :asc,
|
|
|
|
nullable: :nulls_first,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'month',
|
|
|
|
column_expression: table['month'],
|
|
|
|
order_expression: table[:month].asc.nulls_first,
|
|
|
|
order_direction: :asc,
|
|
|
|
reversed_order_expression: table[:month].desc.nulls_last,
|
|
|
|
nullable: :nulls_first,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:expected) do
|
|
|
|
[
|
|
|
|
{ "id" => 3, "year" => nil, "month" => nil },
|
|
|
|
{ "id" => 10, "year" => nil, "month" => nil },
|
|
|
|
{ "id" => 9, "year" => nil, "month" => 2 },
|
|
|
|
{ "id" => 4, "year" => nil, "month" => 5 },
|
|
|
|
{ "id" => 1, "year" => 2010, "month" => nil },
|
|
|
|
{ "id" => 5, "year" => 2010, "month" => nil },
|
|
|
|
{ "id" => 7, "year" => 2010, "month" => 2 },
|
|
|
|
{ "id" => 11, "year" => 2010, "month" => 2 },
|
|
|
|
{ "id" => 2, "year" => 2011, "month" => 2 },
|
|
|
|
{ "id" => 6, "year" => 2011, "month" => 2 },
|
|
|
|
{ "id" => 8, "year" => 2012, "month" => 2 }
|
|
|
|
]
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it_behaves_like 'order examples'
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when ordering by non-nullable columns with mixed directions and a distinct column' do
|
|
|
|
let(:table_data) do
|
|
|
|
<<-SQL
|
2021-04-17 20:07:23 +05:30
|
|
|
VALUES (1, 2010, 0),
|
|
|
|
(2, 2011, 0),
|
|
|
|
(3, 2010, 0),
|
|
|
|
(4, 2010, 0),
|
|
|
|
(5, 2012, 0),
|
|
|
|
(6, 2012, 0),
|
|
|
|
(7, 2010, 0),
|
|
|
|
(8, 2011, 0),
|
|
|
|
(9, 2013, 0),
|
|
|
|
(10, 2014, 0),
|
|
|
|
(11, 2013, 0)
|
2021-06-08 01:23:25 +05:30
|
|
|
SQL
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'year',
|
|
|
|
column_expression: table['year'],
|
|
|
|
order_expression: table['year'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
let(:expected) do
|
|
|
|
[
|
|
|
|
{ "id" => 7, "year" => 2010, "month" => 0 },
|
|
|
|
{ "id" => 4, "year" => 2010, "month" => 0 },
|
|
|
|
{ "id" => 3, "year" => 2010, "month" => 0 },
|
|
|
|
{ "id" => 1, "year" => 2010, "month" => 0 },
|
|
|
|
{ "id" => 8, "year" => 2011, "month" => 0 },
|
|
|
|
{ "id" => 2, "year" => 2011, "month" => 0 },
|
|
|
|
{ "id" => 6, "year" => 2012, "month" => 0 },
|
|
|
|
{ "id" => 5, "year" => 2012, "month" => 0 },
|
|
|
|
{ "id" => 11, "year" => 2013, "month" => 0 },
|
|
|
|
{ "id" => 9, "year" => 2013, "month" => 0 },
|
|
|
|
{ "id" => 10, "year" => 2014, "month" => 0 }
|
|
|
|
]
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it 'takes out a slice between two cursors' do
|
|
|
|
after_cursor = { "id" => 8, "year" => 2011 }
|
|
|
|
before_cursor = { "id" => 5, "year" => 2012 }
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
after_conditions = order.where_values_with_or_query(after_cursor)
|
|
|
|
reversed = order.reversed_order
|
|
|
|
before_conditions = reversed.where_values_with_or_query(before_cursor)
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
query = build_query(
|
|
|
|
order: order,
|
|
|
|
where_conditions: [Arel::Nodes::And.new([after_conditions, before_conditions])],
|
|
|
|
limit: 100)
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2022-11-25 23:54:43 +05:30
|
|
|
expect(run_query(query)).to eq(
|
|
|
|
[
|
|
|
|
{ "id" => 2, "year" => 2011, "month" => 0 },
|
|
|
|
{ "id" => 6, "year" => 2012, "month" => 0 }
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
2022-06-21 17:19:12 +05:30
|
|
|
context 'when ordering by the named function LOWER' do
|
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'title',
|
|
|
|
column_expression: Arel::Nodes::NamedFunction.new("LOWER", [table['title'].desc]),
|
|
|
|
order_expression: table['title'].lower.desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2022-06-21 17:19:12 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
let(:table_data) do
|
|
|
|
<<-SQL
|
|
|
|
VALUES (1, 'A')
|
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:query) do
|
|
|
|
<<-SQL
|
|
|
|
SELECT id, title
|
|
|
|
FROM (#{table_data}) my_table (id, title)
|
|
|
|
ORDER BY #{order};
|
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
|
|
|
subject { run_query(query) }
|
|
|
|
|
|
|
|
it "uses downcased value for encoding and decoding a cursor" do
|
|
|
|
expect(order.cursor_attributes_for_node(subject.first)['title']).to eq("a")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when the passed cursor values do not match with the order definition' do
|
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'year',
|
|
|
|
column_expression: table['year'],
|
|
|
|
order_expression: table['year'].asc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: false
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
column_expression: table['id'],
|
|
|
|
order_expression: table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when values are missing' do
|
|
|
|
it 'raises error' do
|
|
|
|
expect { order.build_where_values(id: 1) }.to raise_error(/Missing items: year/)
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when extra values are present' do
|
|
|
|
it 'raises error' do
|
|
|
|
expect { order.build_where_values(id: 1, year: 2, foo: 3) }.to raise_error(/Extra items: foo/)
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when values are missing and extra values are present' do
|
|
|
|
it 'raises error' do
|
|
|
|
expect { order.build_where_values(year: 2, foo: 3) }.to raise_error(/Extra items: foo\. Missing items: id/)
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when no values are passed' do
|
|
|
|
it 'returns empty array' do
|
|
|
|
expect(order.build_where_values({})).to eq([])
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'extract and apply cursor attributes' do
|
|
|
|
let(:model) { Project.new(id: 100) }
|
|
|
|
let(:scope) { Project.all }
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
shared_examples 'cursor attribute examples' do
|
|
|
|
describe '#cursor_attributes_for_node' do
|
|
|
|
it { expect(order.cursor_attributes_for_node(model)).to eq({ id: '100' }.with_indifferent_access) }
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#apply_cursor_conditions' do
|
|
|
|
context 'when params with string keys are passed' do
|
|
|
|
subject(:sql) { order.apply_cursor_conditions(scope, { 'id' => '100' }).to_sql }
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it { is_expected.to include('"projects"."id" < 100)') }
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when params with symbol keys are passed' do
|
|
|
|
subject(:sql) { order.apply_cursor_conditions(scope, { id: '100' }).to_sql }
|
|
|
|
|
|
|
|
it { is_expected.to include('"projects"."id" < 100)') }
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when string attribute name is given' do
|
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
order_expression: Project.arel_table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it_behaves_like 'cursor attribute examples'
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when symbol attribute name is given' do
|
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: :id,
|
|
|
|
order_expression: Project.arel_table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
|
|
|
|
it_behaves_like 'cursor attribute examples'
|
2021-11-11 11:23:49 +05:30
|
|
|
|
|
|
|
context 'with projections' do
|
|
|
|
context 'when additional_projections is empty' do
|
|
|
|
let(:scope) { Project.select(:id, :namespace_id) }
|
|
|
|
|
|
|
|
subject(:sql) { order.apply_cursor_conditions(scope, { id: '100' }).to_sql }
|
|
|
|
|
|
|
|
it 'has correct projections' do
|
|
|
|
is_expected.to include('SELECT "projects"."id", "projects"."namespace_id" FROM "projects"')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when there are additional_projections' do
|
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
order = Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'created_at_field',
|
|
|
|
column_expression: Project.arel_table[:created_at],
|
|
|
|
order_expression: Project.arel_table[:created_at].desc,
|
|
|
|
order_direction: :desc,
|
|
|
|
distinct: false,
|
|
|
|
add_to_projections: true
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
order_expression: Project.arel_table[:id].desc
|
|
|
|
)
|
|
|
|
])
|
2021-11-11 11:23:49 +05:30
|
|
|
|
|
|
|
order
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:scope) { Project.select(:id, :namespace_id).reorder(order) }
|
|
|
|
|
|
|
|
subject(:sql) { order.apply_cursor_conditions(scope).to_sql }
|
|
|
|
|
|
|
|
it 'has correct projections' do
|
|
|
|
is_expected.to include('SELECT "projects"."id", "projects"."namespace_id", "projects"."created_at" AS created_at_field FROM "projects"')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
describe 'UNION optimization' do
|
|
|
|
let_it_be(:five_months_ago) { 5.months.ago }
|
|
|
|
|
|
|
|
let_it_be(:user_1) { create(:user, created_at: five_months_ago) }
|
|
|
|
let_it_be(:user_2) { create(:user, created_at: five_months_ago) }
|
|
|
|
let_it_be(:user_3) { create(:user, created_at: 1.month.ago) }
|
|
|
|
let_it_be(:user_4) { create(:user, created_at: 2.months.ago) }
|
|
|
|
|
|
|
|
let(:expected_results) { [user_3, user_4, user_2, user_1] }
|
|
|
|
let(:scope) { User.order(created_at: :desc, id: :desc) }
|
|
|
|
let(:keyset_aware_scope) { Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(scope).first }
|
|
|
|
let(:iterator_options) { { scope: keyset_aware_scope } }
|
|
|
|
|
|
|
|
subject(:items) do
|
|
|
|
[].tap do |collector|
|
|
|
|
Gitlab::Pagination::Keyset::Iterator.new(**iterator_options).each_batch(of: 2) do |models|
|
|
|
|
collector.concat(models)
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
2021-06-08 01:23:25 +05:30
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when UNION optimization is off' do
|
|
|
|
it 'returns items in the correct order' do
|
|
|
|
iterator_options[:use_union_optimization] = false
|
|
|
|
|
|
|
|
expect(items).to eq(expected_results)
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
context 'when UNION optimization is on' do
|
|
|
|
before do
|
|
|
|
iterator_options[:use_union_optimization] = true
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
|
|
|
|
2021-06-08 01:23:25 +05:30
|
|
|
it 'returns items in the correct order' do
|
|
|
|
expect(items).to eq(expected_results)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'calls Gitlab::SQL::Union' do
|
|
|
|
expect_next_instances_of(Gitlab::SQL::Union, 2) do |instance|
|
|
|
|
expect(instance.send(:remove_order)).to eq(false) # Do not remove order from the queries
|
|
|
|
expect(instance.send(:remove_duplicates)).to eq(false) # Do not deduplicate the results
|
|
|
|
end
|
|
|
|
|
|
|
|
items
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'builds UNION query' do
|
|
|
|
cursor_attributes = { created_at: five_months_ago, id: user_2.id }
|
|
|
|
order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(keyset_aware_scope)
|
|
|
|
|
|
|
|
query = order.apply_cursor_conditions(scope, cursor_attributes, use_union_optimization: true).to_sql
|
|
|
|
expect(query).to include('UNION ALL')
|
|
|
|
end
|
2021-04-29 21:17:54 +05:30
|
|
|
end
|
|
|
|
end
|
2022-08-13 15:12:31 +05:30
|
|
|
|
|
|
|
describe '#attribute_names' do
|
|
|
|
let(:expected_attribute_names) { %w(id name) }
|
|
|
|
let(:order) do
|
2022-11-25 23:54:43 +05:30
|
|
|
Gitlab::Pagination::Keyset::Order.build(
|
|
|
|
[
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'id',
|
|
|
|
order_expression: Project.arel_table['id'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
),
|
|
|
|
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
|
|
|
|
attribute_name: 'name',
|
|
|
|
order_expression: Project.arel_table['name'].desc,
|
|
|
|
nullable: :not_nullable,
|
|
|
|
distinct: true
|
|
|
|
)
|
|
|
|
])
|
2022-08-13 15:12:31 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
subject { order.attribute_names }
|
|
|
|
|
|
|
|
it { is_expected.to match_array(expected_attribute_names) }
|
|
|
|
end
|
2021-04-17 20:07:23 +05:30
|
|
|
end
|