2019-12-26 22:10:19 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
module Gitlab
|
|
|
|
module Graphql
|
2020-04-22 19:07:51 +05:30
|
|
|
module Pagination
|
2019-12-26 22:10:19 +05:30
|
|
|
module Keyset
|
|
|
|
class OrderInfo
|
2020-03-13 15:44:24 +05:30
|
|
|
attr_reader :attribute_name, :sort_direction, :named_function
|
2019-12-26 22:10:19 +05:30
|
|
|
|
|
|
|
def initialize(order_value)
|
2020-03-13 15:44:24 +05:30
|
|
|
@attribute_name, @sort_direction, @named_function =
|
|
|
|
if order_value.is_a?(String)
|
|
|
|
extract_nulls_last_order(order_value)
|
|
|
|
else
|
|
|
|
extract_attribute_values(order_value)
|
|
|
|
end
|
2019-12-26 22:10:19 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def operator_for(before_or_after)
|
|
|
|
case before_or_after
|
|
|
|
when :before
|
|
|
|
sort_direction == :asc ? '<' : '>'
|
|
|
|
when :after
|
|
|
|
sort_direction == :asc ? '>' : '<'
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Only allow specific node types
|
|
|
|
def self.build_order_list(relation)
|
|
|
|
order_list = relation.order_values.select do |value|
|
|
|
|
supported_order_value?(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
order_list.map { |info| OrderInfo.new(info) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.validate_ordering(relation, order_list)
|
|
|
|
if order_list.empty?
|
|
|
|
raise ArgumentError.new('A minimum of 1 ordering field is required')
|
|
|
|
end
|
|
|
|
|
|
|
|
if order_list.count > 2
|
2020-05-24 23:13:21 +05:30
|
|
|
# Keep in mind an order clause for primary key is added if one is not present
|
|
|
|
# lib/gitlab/graphql/pagination/keyset/connection.rb:97
|
2019-12-26 22:10:19 +05:30
|
|
|
raise ArgumentError.new('A maximum of 2 ordering fields are allowed')
|
|
|
|
end
|
|
|
|
|
|
|
|
# make sure the last ordering field is non-nullable
|
|
|
|
attribute_name = order_list.last&.attribute_name
|
|
|
|
|
|
|
|
if relation.columns_hash[attribute_name].null
|
|
|
|
raise ArgumentError.new("Column `#{attribute_name}` must not allow NULL")
|
|
|
|
end
|
|
|
|
|
|
|
|
if order_list.last.attribute_name != relation.primary_key
|
|
|
|
raise ArgumentError.new("Last ordering field must be the primary key, `#{relation.primary_key}`")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.supported_order_value?(order_value)
|
|
|
|
return true if order_value.is_a?(Arel::Nodes::Ascending) || order_value.is_a?(Arel::Nodes::Descending)
|
|
|
|
return false unless order_value.is_a?(String)
|
|
|
|
|
|
|
|
tokens = order_value.downcase.split
|
|
|
|
|
|
|
|
tokens.last(2) == %w(nulls last) && tokens.count == 4
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def extract_nulls_last_order(order_value)
|
|
|
|
tokens = order_value.downcase.split
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
column_reference = tokens.first
|
|
|
|
sort_direction = tokens[1] == 'asc' ? :asc : :desc
|
|
|
|
|
|
|
|
# Handles the case when the order value is coming from another table.
|
|
|
|
# Example: table_name.column_name
|
|
|
|
# Query the value using the fully qualified column name: pass table_name.column_name as the named_function
|
|
|
|
if fully_qualified_column_reference?(column_reference)
|
|
|
|
[column_reference, sort_direction, Arel.sql(column_reference)]
|
|
|
|
else
|
|
|
|
[column_reference, sort_direction, nil]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Example: table_name.column_name
|
|
|
|
def fully_qualified_column_reference?(attribute)
|
|
|
|
attribute.to_s.count('.') == 1
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
def extract_attribute_values(order_value)
|
2020-11-24 15:15:51 +05:30
|
|
|
if ordering_by_lower?(order_value)
|
|
|
|
[order_value.expr.expressions[0].name.to_s, order_value.direction, order_value.expr]
|
|
|
|
elsif ordering_by_similarity?(order_value)
|
|
|
|
['similarity', order_value.direction, order_value.expr]
|
2021-01-03 14:25:43 +05:30
|
|
|
elsif ordering_by_case?(order_value)
|
|
|
|
['case_order_value', order_value.direction, order_value.expr]
|
|
|
|
elsif ordering_by_array_position?(order_value)
|
|
|
|
['array_position', order_value.direction, order_value.expr]
|
2020-11-24 15:15:51 +05:30
|
|
|
else
|
|
|
|
[order_value.expr.name, order_value.direction, nil]
|
|
|
|
end
|
2020-03-13 15:44:24 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
# determine if ordering using LOWER, eg. "ORDER BY LOWER(boards.name)"
|
|
|
|
def ordering_by_lower?(order_value)
|
|
|
|
order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'lower'
|
2019-12-26 22:10:19 +05:30
|
|
|
end
|
2020-11-24 15:15:51 +05:30
|
|
|
|
2021-01-03 14:25:43 +05:30
|
|
|
# determine if ordering using ARRAY_POSITION, eg. "ORDER BY ARRAY_POSITION(Array[4,3,1,2]::smallint, state)"
|
|
|
|
def ordering_by_array_position?(order_value)
|
|
|
|
order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'array_position'
|
|
|
|
end
|
|
|
|
|
2020-11-24 15:15:51 +05:30
|
|
|
# determine if ordering using SIMILARITY scoring based on Gitlab::Database::SimilarityScore
|
|
|
|
def ordering_by_similarity?(order_value)
|
2021-01-03 14:25:43 +05:30
|
|
|
Gitlab::Database::SimilarityScore.order_by_similarity?(order_value)
|
|
|
|
end
|
|
|
|
|
|
|
|
# determine if ordering using CASE
|
|
|
|
def ordering_by_case?(order_value)
|
|
|
|
order_value.expr.is_a?(Arel::Nodes::Case)
|
2020-11-24 15:15:51 +05:30
|
|
|
end
|
2019-12-26 22:10:19 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2021-02-22 17:27:13 +05:30
|
|
|
|
|
|
|
Gitlab::Graphql::Pagination::Keyset::OrderInfo.prepend_if_ee('EE::Gitlab::Graphql::Pagination::Keyset::OrderInfo')
|