summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/graphql/pagination/keyset/order_info_spec.rb
blob: eb28e6c8c0a4a7393c754c45348a83a5a3872eeb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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 SIMILARITY' do
      let(:relation) { Project.sorted_by_similarity_desc('test', include_in_select: true) }

      it 'assigns the right attribute name, named function, and direction' do
        expect(order_list.count).to eq 2
        expect(order_list.first.attribute_name).to eq 'similarity'
        expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Addition)
        expect(order_list.first.named_function.to_sql).to include 'SIMILARITY('
        expect(order_list.first.sort_direction).to eq :desc
      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