summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/pagination/keyset/column_order_definition_spec.rb
blob: 778244677ef35b9704c87f78de0e47f86461b4aa (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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Pagination::Keyset::ColumnOrderDefinition do
  let_it_be(:project_name_column) do
    described_class.new(
      attribute_name: :name,
      order_expression: Project.arel_table[:name].asc,
      nullable: :not_nullable,
      distinct: true
    )
  end

  let_it_be(:project_name_lower_column) do
    described_class.new(
      attribute_name: :name,
      order_expression: Project.arel_table[:name].lower.desc,
      nullable: :not_nullable,
      distinct: true
    )
  end

  let_it_be(:project_calculated_column_expression) do
    # COALESCE("projects"."description", 'No Description')
    Arel::Nodes::NamedFunction.new('COALESCE', [
      Project.arel_table[:description],
      Arel.sql("'No Description'")
    ])
  end

  let_it_be(:project_calculated_column) do
    described_class.new(
      attribute_name: :name,
      column_expression: project_calculated_column_expression,
      order_expression: project_calculated_column_expression.asc,
      nullable: :not_nullable,
      distinct: true
    )
  end

  describe '#order_direction' do
    context 'inferring order_direction from order_expression' do
      it { expect(project_name_column).to be_ascending_order }
      it { expect(project_name_column).not_to be_descending_order }

      it { expect(project_name_lower_column).to be_descending_order }
      it { expect(project_name_lower_column).not_to be_ascending_order }

      it { expect(project_calculated_column).to be_ascending_order }
      it { expect(project_calculated_column).not_to be_descending_order }

      it 'raises error when order direction cannot be infered' do
        expect do
          described_class.new(
            attribute_name: :name,
            column_expression: Project.arel_table[:name],
            order_expression: 'name asc',
            reversed_order_expression: 'name desc',
            nullable: :not_nullable,
            distinct: true
          )
        end.to raise_error(RuntimeError, /Invalid or missing `order_direction`/)
      end

      it 'does not raise error when order direction is explicitly given' do
        column_order_definition = described_class.new(
          attribute_name: :name,
          column_expression: Project.arel_table[:name],
          order_expression: 'name asc',
          reversed_order_expression: 'name desc',
          order_direction: :asc,
          nullable: :not_nullable,
          distinct: true
        )

        expect(column_order_definition).to be_ascending_order
      end
    end
  end

  describe '#column_expression' do
    context 'inferring column_expression from order_expression' do
      it 'infers the correct column expression' do
        column_order_definition = described_class.new(attribute_name: :name, order_expression: Project.arel_table[:name].asc)

        expect(column_order_definition.column_expression).to eq(Project.arel_table[:name])
      end

      it 'raises error when raw string is given as order expression' do
        expect do
          described_class.new(attribute_name: :name, order_expression: 'name DESC')
        end.to raise_error(RuntimeError, /Couldn't calculate the column expression. Please pass an ARel node/)
      end
    end
  end

  describe '#reversed_order_expression' do
    it 'raises error when order cannot be reversed automatically' do
      expect do
        described_class.new(
          attribute_name: :name,
          column_expression: Project.arel_table[:name],
          order_expression: 'name asc',
          order_direction: :asc,
          nullable: :not_nullable,
          distinct: true
        )
      end.to raise_error(RuntimeError, /Couldn't determine reversed order/)
    end
  end

  describe '#reverse' do
    it { expect(project_name_column.reverse.order_expression).to eq(Project.arel_table[:name].desc) }
    it { expect(project_name_column.reverse).to be_descending_order }

    it { expect(project_calculated_column.reverse.order_expression).to eq(project_calculated_column_expression.desc) }
    it { expect(project_calculated_column.reverse).to be_descending_order }

    context 'when reversed_order_expression is given' do
      it 'uses the given expression' do
        column_order_definition = described_class.new(
          attribute_name: :name,
          column_expression: Project.arel_table[:name],
          order_expression: 'name asc',
          reversed_order_expression: 'name desc',
          order_direction: :asc,
          nullable: :not_nullable,
          distinct: true
        )

        expect(column_order_definition.reverse.order_expression).to eq('name desc')
      end
    end
  end

  describe '#nullable' do
    context 'when the column is nullable' do
      let(:nulls_last_order) do
        described_class.new(
          attribute_name: :name,
          column_expression: Project.arel_table[:name],
          order_expression: MergeRequest::Metrics.arel_table[:merged_at].desc.nulls_last,
          reversed_order_expression: MergeRequest::Metrics.arel_table[:merged_at].asc.nulls_first,
          order_direction: :desc,
          nullable: :nulls_last, # null values are always last
          distinct: false
        )
      end

      it 'requires the position of the null values in the result' do
        expect(nulls_last_order).to be_nulls_last
      end

      it 'reverses nullable correctly' do
        expect(nulls_last_order.reverse).to be_nulls_first
      end

      it 'raises error when invalid nullable value is given' do
        expect do
          described_class.new(
            attribute_name: :name,
            column_expression: Project.arel_table[:name],
            order_expression: MergeRequest::Metrics.arel_table[:merged_at].desc.nulls_last,
            reversed_order_expression: MergeRequest::Metrics.arel_table[:merged_at].asc.nulls_first,
            order_direction: :desc,
            nullable: true,
            distinct: false
          )
        end.to raise_error(RuntimeError, /Invalid `nullable` is given/)
      end

      it 'raises error when the column is nullable and distinct' do
        expect do
          described_class.new(
            attribute_name: :name,
            column_expression: Project.arel_table[:name],
            order_expression: MergeRequest::Metrics.arel_table[:merged_at].desc.nulls_last,
            reversed_order_expression: MergeRequest::Metrics.arel_table[:merged_at].asc.nulls_first,
            order_direction: :desc,
            nullable: :nulls_last,
            distinct: true
          )
        end.to raise_error(RuntimeError, /Invalid column definition/)
      end
    end
  end

  describe "#order_direction_as_sql_string" do
    let(:nulls_last_order) do
      described_class.new(
        attribute_name: :name,
        column_expression: Project.arel_table[:name],
        order_expression: MergeRequest::Metrics.arel_table[:merged_at].desc.nulls_last,
        reversed_order_expression: MergeRequest::Metrics.arel_table[:merged_at].asc.nulls_first,
        order_direction: :desc,
        nullable: :nulls_last, # null values are always last
        distinct: false
      )
    end

    it { expect(project_name_column.order_direction_as_sql_string).to eq('ASC') }
    it { expect(project_name_column.reverse.order_direction_as_sql_string).to eq('DESC') }
    it { expect(project_name_lower_column.order_direction_as_sql_string).to eq('DESC') }
    it { expect(project_name_lower_column.reverse.order_direction_as_sql_string).to eq('ASC') }
    it { expect(nulls_last_order.order_direction_as_sql_string).to eq('DESC NULLS LAST') }
    it { expect(nulls_last_order.reverse.order_direction_as_sql_string).to eq('ASC NULLS FIRST') }
  end
end