Skip to content

Commit a55e091

Browse files
committed
When ordering by 'created_at', add another order by 'id'
This ensures a chronological order of records with the same 'created_at' timestamp.
1 parent 3a40231 commit a55e091

4 files changed

Lines changed: 67 additions & 30 deletions

File tree

lib/cloud_controller/paging/sequel_paginator.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,28 @@ def get_page(dataset, pagination_options)
66
page = pagination_options.page
77
per_page = pagination_options.per_page
88
order_direction = pagination_options.order_direction
9-
order_by = pagination_options.order_by
9+
order_by = pagination_options.order_by.to_sym
1010
table_name = dataset.model.table_name
11-
has_guid_column = dataset.model.columns.include?(:guid)
1211

13-
order_type = Sequel.send(order_direction, Sequel.qualify(table_name, order_by))
14-
dataset = dataset.order(order_type)
12+
# 1. Order the dataset by the specified column and direction.
13+
dataset = dataset.order(Sequel.send(order_direction, Sequel.qualify(table_name, order_by)))
1514

15+
# 2. When ordering by 'created_at', add another order by 'id' to ensure a chronological order of records with the same 'created_at' timestamp.
16+
dataset = dataset.order_append(Sequel.send(order_direction, Sequel.qualify(table_name, :id))) if order_by == :created_at && has_column?(dataset, :id)
17+
18+
# 3. Add a secondary order in case 'secondary_default_order_by' is specified and no custom order has been provided.
1619
secondary_order_by = pagination_options.secondary_order_by
1720
dataset = dataset.order_append(Sequel.send(order_direction, Sequel.qualify(table_name, secondary_order_by))) if secondary_order_by
18-
dataset = dataset.order_append(Sequel.send(order_direction, Sequel.qualify(table_name, :guid))) if order_by != 'id' && has_guid_column
21+
22+
# 4. If the dataset is not already ordered by a unique column, add another order by 'guid' to ensure a deterministic ordering of records.
23+
if !order_includes_unique_column?(dataset.opts[:order]) && has_column?(dataset, :guid)
24+
dataset = dataset.order_append(Sequel.send(order_direction, Sequel.qualify(table_name, :guid)))
25+
end
1926

2027
distinct_opt = dataset.opts[:distinct]
2128
if !distinct_opt.nil? && !distinct_opt.empty? # DISTINCT ON
2229
order_opt = dataset.opts[:order]
23-
dataset = if order_opt.any? { |o| %i[id guid].include?(o.expression.column.to_sym) }
30+
dataset = if order_includes_unique_column?(order_opt)
2431
# If ORDER BY columns are unique, use them for the DISTINCT ON clause.
2532
dataset.distinct(*order_opt.map(&:expression))
2633
else
@@ -46,6 +53,14 @@ def can_paginate_with_window_function?(dataset)
4653

4754
private
4855

56+
def has_column?(dataset, column_name)
57+
dataset.model.columns.include?(column_name)
58+
end
59+
60+
def order_includes_unique_column?(order_opt)
61+
order_opt.any? { |o| %i[id guid].include?(o.expression.column.to_sym) }
62+
end
63+
4964
def paginate_with_window_function(dataset, per_page, page, table_name)
5065
dataset = dataset.from_self if dataset.opts[:distinct]
5166

spec/request_spec_shared_examples.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ def errors_without_test_mode_info(parsed_response)
363363

364364
context 'order_by created_at' do
365365
let!(:resource_1) { resource_klass.make(guid: '1', created_at: '2020-05-26T18:47:03Z', **additional_resource_params) }
366-
let!(:resource_2) { resource_klass.make(guid: '2', created_at: '2020-05-26T18:47:02Z', **additional_resource_params) }
367-
let!(:resource_3) { resource_klass.make(guid: '3', created_at: '2020-05-26T18:47:01Z', **additional_resource_params) }
366+
let!(:resource_2) { resource_klass.make(guid: '3', created_at: '2020-05-26T18:47:02Z', **additional_resource_params) }
367+
let!(:resource_3) { resource_klass.make(guid: '2', created_at: '2020-05-26T18:47:02Z', **additional_resource_params) }
368368
let!(:resource_4) { resource_klass.make(guid: '4', created_at: '2020-05-26T18:47:04Z', **additional_resource_params) }
369369

370370
it 'sorts ascending' do

spec/support/bootstrap/fake_model_tables.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def drop_tables_for_query_spec
278278
def tables_for_sequel_paginator_spec
279279
db.create_table :table_without_guid do
280280
primary_key :id
281-
DateTime :created_at
281+
String :name
282282
end
283283
end
284284

spec/unit/lib/cloud_controller/paging/sequel_paginator_spec.rb

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -187,28 +187,37 @@ class TableWithoutGuid < Sequel::Model(:table_without_guid); end
187187
expect(paginated_result.records[3].guid).to eq(app_model2.guid)
188188
end
189189

190-
it 'orders by GUID as a secondary field when available' do
191-
options = { page: page, per_page: 2, order_by: 'created_at', order_direction: 'asc' }
192-
app_model1.update(guid: '1', created_at: '2019-12-25T13:00:00Z')
193-
app_model2.update(guid: '2', created_at: '2019-12-25T13:00:00Z')
190+
context 'deterministic ordering' do
191+
before do
192+
app_model1.update(guid: '2', created_at: '2019-12-25T13:00:00Z', updated_at: '2019-12-25T13:00:00Z')
193+
app_model2.update(guid: '1', created_at: '2019-12-25T13:00:00Z', updated_at: '2019-12-25T13:00:00Z')
194+
end
194195

195-
pagination_options = PaginationOptions.new(options)
196-
paginated_result = paginator.get_page(dataset, pagination_options)
197-
expect(paginated_result.records.first.guid).to eq(app_model1.guid)
198-
expect(paginated_result.records.second.guid).to eq(app_model2.guid)
199-
end
196+
context "when ordered by 'created_at'" do
197+
it "orders by 'id' as a secondary field" do
198+
options = { page: page, per_page: 2, order_by: 'created_at', order_direction: 'asc' }
200199

201-
it 'does not order by GUID when the table has no GUID' do
202-
options = { page: page, per_page: 2, order_by: 'created_at', order_direction: 'asc' }
200+
pagination_options = PaginationOptions.new(options)
201+
expect do
202+
paginated_result = paginator.get_page(dataset, pagination_options)
203+
expect(paginated_result.records.first.guid).to eq(app_model1.guid)
204+
expect(paginated_result.records.second.guid).to eq(app_model2.guid)
205+
end.to have_queried_db_times(/ORDER BY .\w*.\..created_at. ASC, .\w*.\..id. ASC LIMIT/i, 1)
206+
end
207+
end
203208

204-
pagination_options = PaginationOptions.new(options)
205-
ds = TableWithoutGuid.dataset
206-
expect do
207-
paginator.get_page(ds, pagination_options)
208-
end.to have_queried_db_times(/ORDER BY .\w*.\..created_at. ASC LIMIT/i, 1)
209-
expect do
210-
paginator.get_page(ds, pagination_options)
211-
end.to have_queried_db_times(/ORDER BY .\w*.\..created_at. ASC, .\w*.\..guid. ASC LIMIT/i, 0)
209+
context "when ordered by a non-unique column other than 'created_at'" do
210+
it "orders by 'guid' as a secondary field" do
211+
options = { page: page, per_page: 2, order_by: 'updated_at', order_direction: 'asc' }
212+
213+
pagination_options = PaginationOptions.new(options)
214+
expect do
215+
paginated_result = paginator.get_page(dataset, pagination_options)
216+
expect(paginated_result.records.first.guid).to eq(app_model2.guid)
217+
expect(paginated_result.records.second.guid).to eq(app_model1.guid)
218+
end.to have_queried_db_times(/ORDER BY .\w*.\..updated_at. ASC, .\w*.\..guid. ASC LIMIT/i, 1)
219+
end
220+
end
212221
end
213222

214223
it 'does not order by GUID when the primary order is by ID' do
@@ -223,6 +232,19 @@ class TableWithoutGuid < Sequel::Model(:table_without_guid); end
223232
end.to have_queried_db_times(/ORDER BY .\w*.\..id. ASC, .\w*.\..guid. ASC LIMIT/i, 0)
224233
end
225234

235+
it 'does not order by GUID when the table has no GUID' do
236+
options = { page: page, per_page: 2, order_by: 'name', order_direction: 'asc' }
237+
238+
pagination_options = PaginationOptions.new(options)
239+
ds = TableWithoutGuid.dataset
240+
expect do
241+
paginator.get_page(ds, pagination_options)
242+
end.to have_queried_db_times(/ORDER BY .\w*.\..name. ASC LIMIT/i, 1)
243+
expect do
244+
paginator.get_page(ds, pagination_options)
245+
end.to have_queried_db_times(/ORDER BY .\w*.\..name. ASC, .\w*.\..guid. ASC LIMIT/i, 0)
246+
end
247+
226248
context 'when a DISTINCT ON clause is used' do # MySQL uses GROUP BY instead
227249
let(:distinct_dataset) { dataset.distinct(:id) }
228250

@@ -237,12 +259,12 @@ class TableWithoutGuid < Sequel::Model(:table_without_guid); end
237259
end
238260

239261
context 'when ordered by other column' do
240-
let(:pagination_options) { PaginationOptions.new({ order_by: 'created_at' }) }
262+
let(:pagination_options) { PaginationOptions.new({ order_by: 'name' }) }
241263

242264
it 'uses other column and GUID for DISTINCT ON clause' do
243265
expect do
244266
paginator.get_page(distinct_dataset, pagination_options)
245-
end.to have_queried_db_times(/(select distinct on \(.*created_at.*,.*guid.*\) .* from)|(group by)/i, paginator.can_paginate_with_window_function?(dataset) ? 1 : 2)
267+
end.to have_queried_db_times(/(select distinct on \(.*name.*,.*guid.*\) .* from)|(group by)/i, paginator.can_paginate_with_window_function?(dataset) ? 1 : 2)
246268
end
247269

248270
context 'when table has no GUID column' do

0 commit comments

Comments
 (0)