1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe Gitlab::Pagination::Keyset::Iterator do 6 let_it_be(:project) { create(:project) } 7 let_it_be(:issue_list_with_same_pos) { create_list(:issue, 3, project: project, relative_position: 100, updated_at: 1.day.ago) } 8 let_it_be(:issue_list_with_null_pos) { create_list(:issue, 3, project: project, relative_position: nil, updated_at: 1.day.ago) } 9 let_it_be(:issue_list_with_asc_pos) { create_list(:issue, 3, :with_asc_relative_position, project: project, updated_at: 1.day.ago) } 10 11 let(:klass) { Issue } 12 let(:column) { 'relative_position' } 13 let(:direction) { :asc } 14 let(:reverse_direction) { ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::REVERSED_ORDER_DIRECTIONS[direction] } 15 let(:nulls_position) { :nulls_last } 16 let(:reverse_nulls_position) { ::Gitlab::Pagination::Keyset::ColumnOrderDefinition::REVERSED_NULL_POSITIONS[nulls_position] } 17 let(:custom_reorder) do 18 Gitlab::Pagination::Keyset::Order.build([ 19 Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( 20 attribute_name: column, 21 column_expression: klass.arel_table[column], 22 order_expression: ::Gitlab::Database.nulls_order(column, direction, nulls_position), 23 reversed_order_expression: ::Gitlab::Database.nulls_order(column, reverse_direction, reverse_nulls_position), 24 order_direction: direction, 25 nullable: nulls_position, 26 distinct: false 27 ), 28 Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( 29 attribute_name: 'id', 30 order_expression: klass.arel_table[:id].send(direction) 31 ) 32 ]) 33 end 34 35 let(:iterator_params) { nil } 36 let(:scope) { project.issues.reorder(custom_reorder) } 37 38 subject(:iterator) { described_class.new(**iterator_params) } 39 40 shared_examples 'iterator examples' do 41 describe '.each_batch' do 42 it 'yields an ActiveRecord::Relation when a block is given' do 43 iterator.each_batch(of: 1) do |relation| 44 expect(relation).to be_a_kind_of(ActiveRecord::Relation) 45 end 46 end 47 48 it 'raises error when ordering configuration cannot be automatically determined' do 49 expect do 50 described_class.new(scope: MergeRequestDiffCommit.order(:merge_request_diff_id, :relative_order)) 51 end.to raise_error /The order on the scope does not support keyset pagination/ 52 end 53 54 it 'accepts a custom batch size' do 55 count = 0 56 57 iterator.each_batch(of: 2) { |relation| count += relation.count(:all) } 58 59 expect(count).to eq(9) 60 end 61 62 it 'continues after the cursor' do 63 loaded_records = [] 64 cursor = nil 65 66 # stopping the iterator after the first batch and storing the cursor 67 iterator.each_batch(of: 2) do |relation| # rubocop: disable Lint/UnreachableLoop 68 loaded_records.concat(relation.to_a) 69 record = loaded_records.last 70 71 cursor = custom_reorder.cursor_attributes_for_node(record) 72 break 73 end 74 75 expect(loaded_records).to eq(project.issues.order(custom_reorder).take(2)) 76 77 new_iterator = described_class.new(**iterator_params.merge(cursor: cursor)) 78 new_iterator.each_batch(of: 2) do |relation| 79 loaded_records.concat(relation.to_a) 80 end 81 82 expect(loaded_records).to eq(project.issues.order(custom_reorder)) 83 end 84 85 it 'allows updating of the yielded relations' do 86 time = Time.current 87 88 iterator.each_batch(of: 2) do |relation| 89 Issue.connection.execute("UPDATE issues SET updated_at = '#{time.to_s(:inspect)}' WHERE id IN (#{relation.reselect(:id).to_sql})") 90 end 91 92 expect(Issue.pluck(:updated_at)).to all(be_within(5.seconds).of(time)) 93 end 94 95 context 'with ordering direction' do 96 context 'when ordering asc' do 97 it 'orders ascending by default, including secondary order column' do 98 positions = [] 99 100 iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:relative_position, :id)) } 101 102 expect(positions).to eq(project.issues.reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')).order(id: :asc).pluck(:relative_position, :id)) 103 end 104 end 105 106 context 'when reversing asc order' do 107 let(:scope) { project.issues.order(custom_reorder.reversed_order) } 108 109 it 'orders in reverse of ascending' do 110 positions = [] 111 112 iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:relative_position, :id)) } 113 114 expect(positions).to eq(project.issues.reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')).order(id: :desc).pluck(:relative_position, :id)) 115 end 116 end 117 118 context 'when asc order, with nulls first' do 119 let(:nulls_position) { :nulls_first } 120 121 it 'orders ascending with nulls first' do 122 positions = [] 123 124 iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:relative_position, :id)) } 125 126 expect(positions).to eq(project.issues.reorder(::Gitlab::Database.nulls_first_order('relative_position', 'ASC')).order(id: :asc).pluck(:relative_position, :id)) 127 end 128 end 129 130 context 'when ordering desc' do 131 let(:direction) { :desc } 132 let(:nulls_position) { :nulls_last } 133 134 it 'orders descending' do 135 positions = [] 136 137 iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:relative_position, :id)) } 138 139 expect(positions).to eq(project.issues.reorder(::Gitlab::Database.nulls_last_order('relative_position', 'DESC')).order(id: :desc).pluck(:relative_position, :id)) 140 end 141 end 142 143 context 'when ordering by columns are repeated twice' do 144 let(:direction) { :desc } 145 let(:column) { :id } 146 147 it 'orders descending' do 148 positions = [] 149 150 iterator.each_batch(of: 2) { |rel| positions.concat(rel.pluck(:id)) } 151 152 expect(positions).to eq(project.issues.reorder(id: :desc).pluck(:id)) 153 end 154 end 155 end 156 end 157 end 158 159 context 'when use_union_optimization is used' do 160 let(:iterator_params) { { scope: scope, use_union_optimization: true } } 161 162 include_examples 'iterator examples' 163 end 164 165 context 'when use_union_optimization is not used' do 166 let(:iterator_params) { { scope: scope, use_union_optimization: false } } 167 168 include_examples 'iterator examples' 169 end 170end 171