Skip to content

Commit 7c8c937

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 7c8c937

File tree

5 files changed

+252
-4
lines changed

5 files changed

+252
-4
lines changed

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
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
case col
112+
when Symbol
113+
col if col == :id || col.to_s.end_with?('__id')
114+
when Sequel::SQL::Identifier
115+
col if col.value == :id
116+
when Sequel::SQL::QualifiedIdentifier
117+
col if col.column == :id
118+
when Sequel::SQL::AliasedExpression
119+
# ORDER BY uses the alias, not the original expression
120+
col.alias if col.alias == :id
121+
end
122+
end
123+
end
124+
end
125+
126+
Dataset.register_extension(:default_order_by_id, DefaultOrderById::DatasetMethods)
127+
end
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'sequel/extensions/default_order_by_id'
5+
6+
RSpec.describe 'Sequel::DefaultOrderById' do
7+
let(:model_class) { VCAP::CloudController::Organization }
8+
let(:db) { model_class.db }
9+
10+
def capture_sql(&)
11+
sqls = []
12+
db.loggers << (logger = Class.new do
13+
define_method(:info) { |msg| sqls << msg if msg.include?('SELECT') }
14+
define_method(:debug) { |_| }
15+
define_method(:error) { |_| }
16+
end.new)
17+
yield
18+
db.loggers.delete(logger)
19+
sqls.last
20+
end
21+
22+
describe 'default ordering' do
23+
it 'adds ORDER BY id to model queries' do
24+
sql = capture_sql { model_class.dataset.first }
25+
expect(sql).to match(/ORDER BY .id./)
26+
end
27+
end
28+
29+
describe 'already_ordered?' do
30+
it 'preserves explicit ORDER BY' do
31+
sql = capture_sql { model_class.dataset.order(:name).first }
32+
expect(sql).to match(/ORDER BY .name./)
33+
expect(sql).not_to match(/ORDER BY .id./)
34+
end
35+
end
36+
37+
describe 'incompatible_with_order?' do
38+
it 'skips for GROUP BY' do
39+
ds = model_class.dataset.group(:status)
40+
expect(ds.sql).not_to match(/ORDER BY/)
41+
end
42+
43+
it 'skips for compound queries (UNION)' do
44+
ds1 = model_class.dataset.where(name: 'a')
45+
ds2 = model_class.dataset.where(name: 'b')
46+
sql = capture_sql { ds1.union(ds2, all: true, from_self: false).all }
47+
expect(sql).not_to match(/\) ORDER BY/)
48+
end
49+
50+
it 'skips for DISTINCT ON' do
51+
sql = capture_sql { model_class.dataset.distinct(:guid).all }
52+
expect(sql).not_to match(/ORDER BY/)
53+
end
54+
55+
it 'skips for from_self (subquery)' do
56+
sql = capture_sql { model_class.dataset.where(name: 'a').from_self.all }
57+
expect(sql).to match(/AS .t1.$/)
58+
end
59+
end
60+
61+
describe 'not_a_data_query?' do
62+
it 'skips for schema introspection (columns!)' do
63+
sql = capture_sql { model_class.dataset.columns! }
64+
expect(sql).not_to match(/ORDER BY/)
65+
end
66+
end
67+
68+
describe 'model_has_id_primary_key?' do
69+
it 'skips for models with non-id primary key' do
70+
guid_pk_model = Class.new(Sequel::Model(db[:organizations])) do
71+
set_primary_key :guid
72+
end
73+
sql = capture_sql { guid_pk_model.dataset.first }
74+
expect(sql).not_to match(/ORDER BY/)
75+
end
76+
end
77+
78+
describe 'find_id_column' do
79+
context 'with SELECT *' do
80+
it 'uses unqualified :id' do
81+
sql = capture_sql { model_class.dataset.first }
82+
expect(sql).to match(/ORDER BY .id./)
83+
expect(sql).not_to match(/ORDER BY .organizations.\..id./)
84+
end
85+
86+
it 'uses qualified column for JOIN to avoid ambiguity' do
87+
sql = capture_sql { model_class.dataset.join(:spaces, organization_id: :id).first }
88+
expect(sql).to match(/ORDER BY .organizations.\..id./)
89+
end
90+
end
91+
92+
context 'with SELECT table.*' do
93+
it 'uses unqualified :id' do
94+
sql = capture_sql { model_class.dataset.select(Sequel::SQL::ColumnAll.new(:organizations)).join(:spaces, organization_id: :id).first }
95+
expect(sql).to match(/ORDER BY .id./)
96+
expect(sql).not_to match(/ORDER BY .organizations.\..id./)
97+
end
98+
end
99+
100+
context 'with qualified id in select list' do
101+
it 'uses the qualified column' do
102+
sql = capture_sql { model_class.dataset.select(:organizations__id, :organizations__name).first }
103+
expect(sql).to match(/ORDER BY .organizations.\..id./)
104+
end
105+
end
106+
107+
context 'with aliased id in select list' do
108+
it 'uses the alias' do
109+
sql = capture_sql { model_class.dataset.select(Sequel.as(:organizations__id, :id), :name).first }
110+
expect(sql).to match(/ORDER BY .id./)
111+
end
112+
end
113+
114+
context 'without id in select list' do
115+
it 'skips ordering' do
116+
sql = capture_sql { model_class.dataset.select(:guid, :name).all }
117+
expect(sql).not_to match(/ORDER BY/)
118+
end
119+
end
120+
end
121+
end

0 commit comments

Comments
 (0)