Skip to content

Commit f39b294

Browse files
committed
Add default ORDER BY id to Sequel model queries
Adds a Sequel extension that hooks into fetch methods (all, each, first) to add ORDER BY id just before execution, ensuring deterministic query results for consistent API responses and reliable test behavior. Skips ordering when incompatible (GROUP BY, compounds, DISTINCT ON, from_self) or unnecessary (explicit ORDER BY, no id column).
1 parent b27da11 commit f39b294

7 files changed

Lines changed: 278 additions & 5 deletions

File tree

app/models/runtime/buildpack_lifecycle_data_model.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ class BuildpackLifecycleDataModel < Sequel::Model(:buildpack_lifecycle_data)
2828
one_to_many :buildpack_lifecycle_buildpacks,
2929
class: '::VCAP::CloudController::BuildpackLifecycleBuildpackModel',
3030
key: :buildpack_lifecycle_data_guid,
31-
primary_key: :guid,
32-
order: :id
31+
primary_key: :guid
3332
plugin :nested_attributes
3433
nested_attributes :buildpack_lifecycle_buildpacks, destroy: true
3534
add_association_dependencies buildpack_lifecycle_buildpacks: :destroy

app/models/runtime/cnb_lifecycle_data_model.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ class CNBLifecycleDataModel < Sequel::Model(:cnb_lifecycle_data)
2727
one_to_many :buildpack_lifecycle_buildpacks,
2828
class: '::VCAP::CloudController::BuildpackLifecycleBuildpackModel',
2929
key: :cnb_lifecycle_data_guid,
30-
primary_key: :guid,
31-
order: :id
30+
primary_key: :guid
3231
plugin :nested_attributes
3332
nested_attributes :buildpack_lifecycle_buildpacks, destroy: true
3433
add_association_dependencies buildpack_lifecycle_buildpacks: :destroy

lib/cloud_controller/db.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'cloud_controller/execution_context'
66
require 'sequel/extensions/query_length_logging'
77
require 'sequel/extensions/request_query_metrics'
8+
require 'sequel/extensions/default_order_by_id'
89

910
module VCAP::CloudController
1011
class DB
@@ -45,6 +46,7 @@ def self.connect(opts, logger)
4546
add_connection_expiration_extension(db, opts)
4647
add_connection_validator_extension(db, opts)
4748
db.extension(:requires_unique_column_names_in_subquery)
49+
db.extension(:default_order_by_id)
4850
add_connection_metrics_extension(db)
4951
db
5052
end

lib/cloud_controller/diego/constants.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'diego/lrp_constants'
2+
13
module VCAP::CloudController
24
module Diego
35
STAGING_DOMAIN = 'cf-app-staging'.freeze

lib/cloud_controller/diego/reporters/instances_reporter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
require 'utils/workpool'
2+
require 'cloud_controller/diego/constants'
23
require 'cloud_controller/diego/reporters/reporter_mixins'
3-
require 'diego/lrp_constants'
44

55
module VCAP::CloudController
66
module Diego
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# frozen_string_literal: true
2+
3+
# Sequel extension that adds default ORDER BY id to model queries.
4+
#
5+
# Hooks into fetch methods (all, each, first) to add ORDER BY id just before
6+
# execution. This ensures ordering is only added to the final query, not to
7+
# subqueries or compound query parts.
8+
#
9+
# Skips default ordering when:
10+
# - Query already has explicit ORDER BY
11+
# - Query is incompatible (GROUP BY, compounds, DISTINCT ON, from_self)
12+
# - Query is schema introspection (LIMIT 0)
13+
# - Model doesn't have id as primary key
14+
# - id is not in the select list
15+
#
16+
# For JOIN queries with SELECT *, uses qualified column (table.id) to avoid
17+
# ambiguity.
18+
#
19+
# Ensures deterministic query results for consistent API responses and
20+
# reliable test behavior.
21+
#
22+
# Usage:
23+
# DB.extension(:default_order_by_id)
24+
#
25+
module Sequel
26+
module DefaultOrderById
27+
module DatasetMethods
28+
def all(*, &)
29+
id_col = id_column_for_order
30+
return super unless id_col
31+
32+
order(id_col).all(*, &)
33+
end
34+
35+
def each(*, &)
36+
id_col = id_column_for_order
37+
return super unless id_col
38+
39+
order(id_col).each(*, &)
40+
end
41+
42+
def first(*, &)
43+
id_col = id_column_for_order
44+
return super unless id_col
45+
46+
order(id_col).first(*, &)
47+
end
48+
49+
private
50+
51+
def id_column_for_order
52+
return if already_ordered? || incompatible_with_order? || not_a_data_query? || !model_has_id_primary_key?
53+
54+
find_id_column
55+
end
56+
57+
def already_ordered?
58+
opts[:order]
59+
end
60+
61+
def incompatible_with_order?
62+
opts[:group] || # Aggregated results don't have individual ids
63+
opts[:compounds] || # Compound queries (e.g. UNION) have own ordering
64+
distinct_on? || # DISTINCT ON requires matching ORDER BY
65+
from_self? # Outer query handles ordering
66+
end
67+
68+
def distinct_on?
69+
opts[:distinct].is_a?(Array) && opts[:distinct].any?
70+
end
71+
72+
def from_self?
73+
opts[:from].is_a?(Array) && opts[:from].any? { |f| f.is_a?(Sequel::SQL::AliasedExpression) }
74+
end
75+
76+
def not_a_data_query?
77+
opts[:limit] == 0 # Schema introspection query
78+
end
79+
80+
def model_has_id_primary_key?
81+
return false unless respond_to?(:model) && model
82+
83+
model.primary_key == :id
84+
end
85+
86+
def find_id_column
87+
select_cols = opts[:select]
88+
89+
if select_cols.nil? || select_cols.empty?
90+
# SELECT * includes id
91+
if opts[:join]
92+
# Qualify to avoid ambiguity with joined tables
93+
return Sequel.qualify(model.table_name, :id)
94+
end
95+
96+
return :id
97+
end
98+
99+
select_cols.each do |col|
100+
# SELECT table.* includes id
101+
return :id if col.is_a?(Sequel::SQL::ColumnAll) && col.table == model.table_name
102+
103+
id_col = extract_id_column(col)
104+
return id_col if id_col
105+
end
106+
107+
nil
108+
end
109+
110+
def extract_id_column(col)
111+
return col if id_expression?(col)
112+
113+
return col.alias if col.is_a?(Sequel::SQL::AliasedExpression) && id_expression?(col.expression)
114+
115+
nil
116+
end
117+
118+
def id_expression?(expr)
119+
case expr
120+
when Symbol
121+
expr == :id || expr.to_s.end_with?('__id')
122+
when Sequel::SQL::Identifier
123+
expr.value == :id
124+
when Sequel::SQL::QualifiedIdentifier
125+
expr.column == :id
126+
else
127+
false
128+
end
129+
end
130+
end
131+
end
132+
133+
Dataset.register_extension(:default_order_by_id, DefaultOrderById::DatasetMethods)
134+
end
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
3+
require 'db_spec_helper'
4+
require 'sequel/extensions/default_order_by_id'
5+
6+
RSpec.describe 'Sequel::DefaultOrderById' do
7+
let(:db) { DbConfig.new.connection }
8+
9+
let(:model_class) { Class.new(Sequel::Model(db[:test_default_order_main])) }
10+
11+
before do
12+
db.create_table?(:test_default_order_main) do
13+
primary_key :id
14+
String :name
15+
String :guid
16+
String :status
17+
end
18+
db.create_table?(:test_default_order_join) do
19+
primary_key :id
20+
foreign_key :main_id, :test_default_order_main
21+
end
22+
end
23+
24+
after do
25+
db.drop_table?(:test_default_order_join)
26+
db.drop_table?(:test_default_order_main)
27+
end
28+
29+
def capture_sql(&)
30+
sqls = []
31+
db.loggers << (logger = Class.new do
32+
define_method(:info) { |msg| sqls << msg if msg.include?('SELECT') }
33+
define_method(:debug) { |_| }
34+
define_method(:error) { |_| }
35+
end.new)
36+
yield
37+
db.loggers.delete(logger)
38+
sqls.last
39+
end
40+
41+
describe 'default ordering' do
42+
it 'adds ORDER BY id to model queries' do
43+
sql = capture_sql { model_class.dataset.all }
44+
expect(sql).to match(/ORDER BY .id./)
45+
end
46+
end
47+
48+
describe 'already_ordered?' do
49+
it 'preserves explicit ORDER BY' do
50+
sql = capture_sql { model_class.dataset.order(:name).all }
51+
expect(sql).to match(/ORDER BY .name./)
52+
end
53+
end
54+
55+
describe 'incompatible_with_order?' do
56+
it 'skips for GROUP BY' do
57+
sql = capture_sql { model_class.dataset.select_group(:status).select_append(Sequel.function(:max, :id).as(:id)).all }
58+
expect(sql).not_to match(/ORDER BY/)
59+
end
60+
61+
it 'skips for compound queries (UNION)' do
62+
ds1 = model_class.dataset.where(name: 'a')
63+
ds2 = model_class.dataset.where(name: 'b')
64+
sql = capture_sql { ds1.union(ds2, all: true, from_self: false).all }
65+
expect(sql).not_to match(/ORDER BY/)
66+
end
67+
68+
it 'skips for DISTINCT ON' do
69+
sql = capture_sql { model_class.dataset.distinct(:guid).all }
70+
expect(sql).not_to match(/ORDER BY/)
71+
end
72+
73+
it 'skips for from_self (subquery)' do
74+
sql = capture_sql { model_class.dataset.where(name: 'a').from_self.all }
75+
expect(sql).not_to match(/ORDER BY/)
76+
end
77+
end
78+
79+
describe 'not_a_data_query?' do
80+
it 'skips for schema introspection (columns!)' do
81+
sql = capture_sql { model_class.dataset.columns! }
82+
expect(sql).not_to match(/ORDER BY/)
83+
end
84+
end
85+
86+
describe 'model_has_id_primary_key?' do
87+
it 'skips for models with non-id primary key' do
88+
guid_pk_model = Class.new(Sequel::Model(db[:test_default_order_main])) do
89+
set_primary_key :guid
90+
end
91+
sql = capture_sql { guid_pk_model.dataset.all }
92+
expect(sql).not_to match(/ORDER BY/)
93+
end
94+
end
95+
96+
describe 'find_id_column' do
97+
context 'with SELECT *' do
98+
it 'uses unqualified :id' do
99+
sql = capture_sql { model_class.dataset.all }
100+
expect(sql).to match(/ORDER BY .id./)
101+
end
102+
103+
it 'uses qualified column for JOIN to avoid ambiguity' do
104+
sql = capture_sql { model_class.dataset.join(:test_default_order_join, main_id: :id).all }
105+
expect(sql).to match(/ORDER BY .test_default_order_main.\..id./)
106+
end
107+
end
108+
109+
context 'with SELECT table.*' do
110+
it 'uses unqualified :id' do
111+
sql = capture_sql { model_class.dataset.select(Sequel::SQL::ColumnAll.new(:test_default_order_main)).join(:test_default_order_join, main_id: :id).all }
112+
expect(sql).to match(/ORDER BY .id./)
113+
end
114+
end
115+
116+
context 'with qualified id in select list' do
117+
it 'uses the qualified column' do
118+
sql = capture_sql { model_class.dataset.select(:test_default_order_main__id, :test_default_order_main__name).all }
119+
expect(sql).to match(/ORDER BY .test_default_order_main.\..id./)
120+
end
121+
end
122+
123+
context 'with aliased id in select list' do
124+
it 'uses the alias' do
125+
sql = capture_sql { model_class.dataset.select(Sequel.as(:test_default_order_main__id, :order_id), :name).all }
126+
expect(sql).to match(/ORDER BY .order_id./)
127+
end
128+
end
129+
130+
context 'without id in select list' do
131+
it 'skips ordering' do
132+
sql = capture_sql { model_class.dataset.select(:guid, :name).all }
133+
expect(sql).not_to match(/ORDER BY/)
134+
end
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)