summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/pagination/keyset/simple_order_builder_spec.rb
blob: 4f1d380ab0a155f452e9f42562c5f555d6fa8967 (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
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Pagination::Keyset::SimpleOrderBuilder do
  let(:ordered_scope) { described_class.build(scope).first }
  let(:order_object) { Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(ordered_scope) }
  let(:column_definition) { order_object.column_definitions.first }

  subject(:sql_with_order) { ordered_scope.to_sql }

  context 'when no order present' do
    let(:scope) { Project.where(id: [1, 2, 3]) }

    it 'orders by primary key' do
      expect(sql_with_order).to end_with('ORDER BY "projects"."id" DESC')
    end

    it 'sets the column definition distinct and not nullable' do
      expect(column_definition).to be_not_nullable
      expect(column_definition).to be_distinct
    end

    context "when the order scope's model uses default_scope" do
      let(:scope) do
        model = Class.new(ApplicationRecord) do
          self.table_name = 'events'

          default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
        end

        model.reorder(nil)
      end

      it 'orders by primary key' do
        expect(sql_with_order).to end_with('ORDER BY "events"."id" DESC')
      end
    end
  end

  context 'when primary key order present' do
    let(:scope) { Project.where(id: [1, 2, 3]).order(id: :asc) }

    it 'orders by primary key without altering the direction' do
      expect(sql_with_order).to end_with('ORDER BY "projects"."id" ASC')
    end
  end

  context 'when ordered by other column' do
    let(:scope) { Project.where(id: [1, 2, 3]).order(created_at: :asc) }

    it 'adds extra primary key order as tie-breaker' do
      expect(sql_with_order).to end_with('ORDER BY "projects"."created_at" ASC, "projects"."id" DESC')
    end

    it 'sets the column definition for created_at non-distinct and nullable' do
      expect(column_definition.attribute_name).to eq('created_at')
      expect(column_definition.nullable?).to eq(true) # be_nullable calls non_null? method for some reason
      expect(column_definition).not_to be_distinct
    end
  end

  context 'when ordered by two columns where the last one is the tie breaker' do
    let(:scope) { Project.where(id: [1, 2, 3]).order(created_at: :asc, id: :asc) }

    it 'preserves the order' do
      expect(sql_with_order).to end_with('ORDER BY "projects"."created_at" ASC, "projects"."id" ASC')
    end
  end

  context 'when non-nullable column is given' do
    let(:scope) { Project.where(id: [1, 2, 3]).order(namespace_id: :asc, id: :asc) }

    it 'sets the column definition for namespace_id non-distinct and non-nullable' do
      expect(column_definition.attribute_name).to eq('namespace_id')
      expect(column_definition).to be_not_nullable
      expect(column_definition).not_to be_distinct
    end
  end

  context 'when ordering by a column with the lower named function' do
    let(:scope) { Project.where(id: [1, 2, 3]).order(Project.arel_table[:name].lower.desc) }

    it 'sets the column definition for name' do
      expect(column_definition.attribute_name).to eq('name')
      expect(column_definition.column_expression.expressions.first.name).to eq('name')
      expect(column_definition.column_expression.name).to eq('LOWER')
    end

    it 'adds extra primary key order as tie-breaker' do
      expect(sql_with_order).to end_with('ORDER BY LOWER("projects"."name") DESC, "projects"."id" DESC')
    end
  end

  context "NULLS order given as as an Arel literal" do
    context 'when NULLS LAST order is given without a tie-breaker' do
      let(:scope) { Project.order(Project.arel_table[:created_at].asc.nulls_last) }

      it 'sets the column definition for created_at appropriately' do
        expect(column_definition.attribute_name).to eq('created_at')
      end

      it 'orders by primary key' do
        expect(sql_with_order)
          .to end_with('ORDER BY "projects"."created_at" ASC NULLS LAST, "projects"."id" DESC')
      end
    end

    context 'when NULLS FIRST order is given with a tie-breaker' do
      let(:scope) { Issue.order(Issue.arel_table[:relative_position].desc.nulls_first).order(id: :asc) }

      it 'sets the column definition for created_at appropriately' do
        expect(column_definition.attribute_name).to eq('relative_position')
      end

      it 'orders by the given primary key' do
        expect(sql_with_order)
          .to end_with('ORDER BY "issues"."relative_position" DESC NULLS FIRST, "issues"."id" ASC')
      end
    end
  end

  context "NULLS order given as as an Arel node" do
    context 'when NULLS LAST order is given without a tie-breaker' do
      let(:scope) { Project.order(Project.arel_table[:created_at].asc.nulls_last) }

      it 'sets the column definition for created_at appropriately' do
        expect(column_definition.attribute_name).to eq('created_at')
      end

      it 'orders by primary key' do
        expect(sql_with_order).to end_with('ORDER BY "projects"."created_at" ASC NULLS LAST, "projects"."id" DESC')
      end
    end

    context 'when NULLS FIRST order is given with a tie-breaker' do
      let(:scope) { Issue.order(Issue.arel_table[:relative_position].desc.nulls_first).order(id: :asc) }

      it 'sets the column definition for created_at appropriately' do
        expect(column_definition.attribute_name).to eq('relative_position')
      end

      it 'orders by the given primary key' do
        expect(sql_with_order).to end_with('ORDER BY "issues"."relative_position" DESC NULLS FIRST, "issues"."id" ASC')
      end
    end
  end

  context 'return :unable_to_order symbol when order cannot be built' do
    subject(:success) { described_class.build(scope).last }

    context 'when raw SQL order is given' do
      let(:scope) { Project.order('id DESC') }

      it { is_expected.to eq(false) }
    end

    context 'when an invalid NULLS order is given' do
      using RSpec::Parameterized::TableSyntax

      where(:scope) do
        [
          lazy { Project.order(Arel.sql('projects.updated_at created_at Asc Nulls Last')) },
          lazy { Project.order(Arel.sql('projects.created_at ZZZ NULLS FIRST')) },
          lazy { Project.order(Arel.sql('projects.relative_position ASC NULLS LAST')) }
        ]
      end

      with_them do
        it { is_expected.to eq(false) }
      end
    end

    context 'when more than 2 columns are given for the order' do
      let(:scope) { Project.order(created_at: :asc, updated_at: :desc, id: :asc) }

      it { is_expected.to eq(false) }
    end
  end
end