Commit ac503c52 authored by Eulyeon Ko's avatar Eulyeon Ko

Support lower named function

Undo refactor
parent 9f4d41c7
...@@ -99,6 +99,8 @@ module Gitlab ...@@ -99,6 +99,8 @@ module Gitlab
field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z') field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z')
elsif field_value.nil? elsif field_value.nil?
nil nil
elsif lower_named_function?(column_definition)
field_value.downcase
else else
field_value.to_s field_value.to_s
end end
...@@ -184,6 +186,10 @@ module Gitlab ...@@ -184,6 +186,10 @@ module Gitlab
private private
def lower_named_function?(column_definition)
column_definition.column_expression.is_a?(Arel::Nodes::NamedFunction) && column_definition.column_expression.name&.downcase == 'lower'
end
def composite_row_comparison_possible? def composite_row_comparison_possible?
!column_definitions.one? && !column_definitions.one? &&
column_definitions.all?(&:not_nullable?) && column_definitions.all?(&:not_nullable?) &&
......
...@@ -24,55 +24,65 @@ module Gitlab ...@@ -24,55 +24,65 @@ module Gitlab
end end
def build def build
return [scope.reorder!(primary_key_order(primary_key_desc)), true] if order_values.empty? order = if order_values.empty?
return [scope.reorder!(keyset_order), true] if Gitlab::Pagination::Keyset::Order.keyset_aware?(scope) primary_key_descending_order
elsif Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
simple_order ? [scope.reorder!(simple_order), true] : [scope, false] # [scope, success] Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
# Ordered by a primary key. Ex. 'ORDER BY id'.
elsif ordered_by_primary_key?
primary_key_order
# Ordered by one non-primary table column. Ex. 'ORDER BY created_at'.
elsif ordered_by_other_column?
column_with_tie_breaker_order
# Ordered by two table columns with the last column as a tie breaker. Ex. 'ORDER BY created, id ASC'.
elsif ordered_by_other_column_with_tie_breaker?
tie_breaker_attribute = order_values.second
tie_breaker_column_order = Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: tie_breaker_attribute
)
column_with_tie_breaker_order(tie_breaker_column_order)
end
order ? [scope.reorder!(order), true] : [scope, false] # [scope, success]
end end
private private
attr_reader :scope, :order_values, :model_class, :arel_table, :primary_key attr_reader :scope, :order_values, :model_class, :arel_table, :primary_key
def keyset_order def table_column?(name)
Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope) model_class.column_names.include?(name.to_s)
end end
def simple_order def primary_key?(attribute)
return unless ordered_with_arel_attributes? arel_table[primary_key].to_s == attribute.to_s
# Ordered by a primary key: 'ORDER BY id'.
if order_values.one? && primary_key?(order_values.first)
primary_key_order(order_values.first)
# Ordered by one non-primary table column: 'ORDER BY created_at'.
elsif order_values.one? && table_column?(order_values.first)
simple_double_column_order
# Ordered by two table columns with the last column as a tie breaker: 'ORDER BY lower(title), id'.
elsif order_values.size == 2 && table_column?(order_values.first) && primary_key?(order_values.second)
tie_breaker_value = order_values.second
simple_double_column_order(tie_breaker_value)
end
end end
def ordered_with_arel_attributes? def lower_named_function?(attribute)
arel_attributes = order_values.map { |o| o.try(:expr) }.compact attribute.is_a?(Arel::Nodes::NamedFunction) && attribute.name&.downcase == 'lower'
arel_attributes.size == order_values.size
end end
def primary_key?(order_value) def supported_column?(attribute)
arel_table[primary_key].to_s == order_value.expr.to_s if lower_named_function?(attribute)
attribute.expressions.one? && attribute.expressions.first.respond_to?(:name) && table_column?(attribute.expressions.first.name)
else
attribute.respond_to?(:name) && table_column?(attribute.name)
end
end end
def table_column?(order_value) def column_name(order_value)
return unless order_value.expr.try(:name) if lower_named_function?(order_value.expr)
order_value.expr.expressions.first.name
model_class.column_names.include?(order_value.expr.name.to_s) else
order_value.expr.name
end
end end
def nullability(order_value) def nullability(order_value)
nullable = model_class.columns.find { |column| column.name == order_value.expr.name }.null nullable = model_class.columns.find { |column| column.name == column_name(order_value) }.null
if nullable && order_value.is_a?(Arel::Nodes::Ascending) if nullable && order_value.is_a?(Arel::Nodes::Ascending)
:nulls_last :nulls_last
...@@ -83,36 +93,82 @@ module Gitlab ...@@ -83,36 +93,82 @@ module Gitlab
end end
end end
def primary_key_desc def primary_key_descending_order
arel_table[primary_key].desc Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: arel_table[primary_key].desc
)
])
end end
def primary_key_order(order_value) def primary_key_order
Gitlab::Pagination::Keyset::Order.build([primary_key_column(order_value)]) Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: order_values.first
)
])
end end
def simple_double_column_order(tie_breaker_value = primary_key_desc) def column_with_tie_breaker_order(tie_breaker_column_order = default_tie_breaker_column_order)
Gitlab::Pagination::Keyset::Order.build([ Gitlab::Pagination::Keyset::Order.build([
regular_column(order_values.first), column(order_values.first),
primary_key_column(tie_breaker_value) tie_breaker_column_order
]) ])
end end
def primary_key_column(order_value) def column(order_value)
return lower_named_function_column(order_value) if lower_named_function?(order_value.expr)
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key, attribute_name: order_value.expr.name,
order_expression: order_value order_expression: order_value,
nullable: nullability(order_value),
distinct: false
) )
end end
def regular_column(order_value) def lower_named_function_column(order_value)
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: order_value.expr.name, attribute_name: column_name(order_value),
order_expression: order_value, order_expression: order_value,
column_expression: Arel::Nodes::NamedFunction.new("LOWER", [model_class.arel_table[column_name(order_value)]]),
nullable: nullability(order_value), nullable: nullability(order_value),
distinct: false distinct: false
) )
end end
def ordered_by_primary_key?
return unless order_values.one?
attribute = order_values.first.try(:expr)
attribute && primary_key?(attribute)
end
def ordered_by_other_column?
return unless order_values.one?
attribute = order_values.first.try(:expr)
attribute && supported_column?(attribute)
end
def ordered_by_other_column_with_tie_breaker?
return unless order_values.size == 2
attribute = order_values.first.try(:expr)
return unless attribute && supported_column?(attribute)
tie_breaker_attribute = order_values.second.try(:expr)
tie_breaker_attribute && primary_key?(tie_breaker_attribute)
end
def default_tie_breaker_column_order
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: model_class.primary_key,
order_expression: arel_table[primary_key].desc
)
end
end end
end end
end end
......
...@@ -239,7 +239,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder ...@@ -239,7 +239,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
end end
it 'raises error when unsupported scope is passed' do it 'raises error when unsupported scope is passed' do
scope = Issue.order(Issue.arel_table[:id].lower.desc) scope = Issue.order(Arel::Nodes::NamedFunction.new('UPPER', [Issue.arel_table[:id]]))
options = { options = {
scope: scope, scope: scope,
......
...@@ -441,6 +441,47 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do ...@@ -441,6 +441,47 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
end end
end end
context 'when ordering by the named function LOWER' do
let(:order) do
Gitlab::Pagination::Keyset::Order.build([
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'title',
column_expression: Arel::Nodes::NamedFunction.new("LOWER", [table['title'].desc]),
order_expression: table['title'].lower.desc,
nullable: :not_nullable,
distinct: false
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
column_expression: table['id'],
order_expression: table['id'].desc,
nullable: :not_nullable,
distinct: true
)
])
end
let(:table_data) do
<<-SQL
VALUES (1, 'A')
SQL
end
let(:query) do
<<-SQL
SELECT id, title
FROM (#{table_data}) my_table (id, title)
ORDER BY #{order};
SQL
end
subject { run_query(query) }
it "uses downcased value for encoding and decoding a cursor" do
expect(order.cursor_attributes_for_node(subject.first)['title']).to eq("a")
end
end
context 'when the passed cursor values do not match with the order definition' do context 'when the passed cursor values do not match with the order definition' do
let(:order) do let(:order) do
Gitlab::Pagination::Keyset::Order.build([ Gitlab::Pagination::Keyset::Order.build([
......
...@@ -67,6 +67,22 @@ RSpec.describe Gitlab::Pagination::Keyset::SimpleOrderBuilder do ...@@ -67,6 +67,22 @@ RSpec.describe Gitlab::Pagination::Keyset::SimpleOrderBuilder do
end end
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
column_definition = order_object.column_definitions.first
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 'return :unable_to_order symbol when order cannot be built' do context 'return :unable_to_order symbol when order cannot be built' do
subject(:success) { described_class.build(scope).last } subject(:success) { described_class.build(scope).last }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment