Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-29 15:11:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-29 15:11:29 +0300
commit7fd99ae2a4424cf996adcc1a3c3f2a753c0ec5aa (patch)
tree9f2d1feb617798aa6e03aa042ec9ce30e240d47a /lib/gitlab/graphql
parenta36e5d33c2ca0e724053066efcec9471b861485c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/gitlab/graphql')
-rw-r--r--lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb62
-rw-r--r--lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb55
-rw-r--r--lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb39
-rw-r--r--lib/gitlab/graphql/pagination/keyset/connection.rb97
-rw-r--r--lib/gitlab/graphql/pagination/keyset/generic_keyset_pagination.rb98
-rw-r--r--lib/gitlab/graphql/pagination/keyset/order_info.rb124
-rw-r--r--lib/gitlab/graphql/pagination/keyset/query_builder.rb73
7 files changed, 26 insertions, 522 deletions
diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb
deleted file mode 100644
index 6645dac36fa..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/conditions/base_condition.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- module Conditions
- class BaseCondition
- # @param [Arel::Table] arel_table for the relation being ordered
- # @param [Array<OrderInfo>] order_list of extracted orderings
- # @param [Array] values from the decoded cursor
- # @param [Array<String>] operators determining sort comparison
- # @param [Symbol] before_or_after indicates whether we want
- # items :before the cursor or :after the cursor
- def initialize(arel_table, order_list, values, operators, before_or_after)
- @arel_table = arel_table
- @order_list = order_list
- @values = values
- @operators = operators
- @before_or_after = before_or_after
-
- @before_or_after = :after unless [:after, :before].include?(@before_or_after)
- end
-
- def build
- raise NotImplementedError
- end
-
- private
-
- attr_reader :arel_table, :order_list, :values, :operators, :before_or_after
-
- def table_condition(order_info, value, operator)
- if order_info.named_function
- target = order_info.named_function
-
- if target.try(:name)&.casecmp('lower') == 0
- value = value&.downcase
- end
- else
- target = arel_table[order_info.attribute_name]
- end
-
- case operator
- when '>'
- target.gt(value)
- when '<'
- target.lt(value)
- when '='
- target.eq(value)
- when 'is_null'
- target.eq(nil)
- when 'is_not_null'
- target.not_eq(nil)
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb
deleted file mode 100644
index ec70f5c5a24..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/conditions/not_null_condition.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- module Conditions
- class NotNullCondition < BaseCondition
- def build
- conditions = [first_attribute_condition]
-
- # If there is only one order field, we can assume it
- # does not contain NULLs, and don't need additional
- # conditions
- unless order_list.count == 1
- conditions << [second_attribute_condition, final_condition]
- end
-
- conditions.join
- end
-
- private
-
- # ex: "(relative_position > 23)"
- def first_attribute_condition
- <<~SQL
- (#{table_condition(order_list.first, values.first, operators.first).to_sql})
- SQL
- end
-
- # ex: " OR (relative_position = 23 AND id > 500)"
- def second_attribute_condition
- <<~SQL
- OR (
- #{table_condition(order_list.first, values.first, '=').to_sql}
- AND
- #{table_condition(order_list[1], values[1], operators[1]).to_sql}
- )
- SQL
- end
-
- # ex: " OR (relative_position IS NULL)"
- def final_condition
- if before_or_after == :after
- <<~SQL
- OR (#{table_condition(order_list.first, nil, 'is_null').to_sql})
- SQL
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb b/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb
deleted file mode 100644
index 1aae1020e79..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/conditions/null_condition.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- module Conditions
- class NullCondition < BaseCondition
- def build
- [first_attribute_condition, final_condition].join
- end
-
- private
-
- # ex: "(relative_position IS NULL AND id > 500)"
- def first_attribute_condition
- <<~SQL
- (
- #{table_condition(order_list.first, nil, 'is_null').to_sql}
- AND
- #{table_condition(order_list[1], values[1], operators[1]).to_sql}
- )
- SQL
- end
-
- # ex: " OR (relative_position IS NOT NULL)"
- def final_condition
- if before_or_after == :before
- <<~SQL
- OR (#{table_condition(order_list.first, nil, 'is_not_null').to_sql})
- SQL
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb
index 3e119a39e6d..b074c273996 100644
--- a/lib/gitlab/graphql/pagination/keyset/connection.rb
+++ b/lib/gitlab/graphql/pagination/keyset/connection.rb
@@ -29,7 +29,6 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
include ::Gitlab::Graphql::ConnectionCollectionMethods
prepend ::Gitlab::Graphql::ConnectionRedaction
- prepend GenericKeysetPagination
# rubocop: disable Naming/PredicateName
# https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields
@@ -58,19 +57,13 @@ module Gitlab
def has_next_page
strong_memoize(:has_next_page) do
if before
- # If `before` is specified, that points to a specific record,
- # even if it's the last one. Since we're asking for `before`,
- # then the specific record we're pointing to is in the
- # next page
true
elsif first
case sliced_nodes
when Array
sliced_nodes.size > limit_value
else
- # If we count the number of requested items plus one (`limit_value + 1`),
- # then if we get `limit_value + 1` then we know there is a next page
- relation_count(set_limit(sliced_nodes, limit_value + 1)) == limit_value + 1
+ sliced_nodes.limit(1).offset(limit_value).exists? # rubocop: disable CodeReuse/ActiveRecord
end
else
false
@@ -80,20 +73,15 @@ module Gitlab
# rubocop: enable Naming/PredicateName
def cursor_for(node)
- encoded_json_from_ordering(node)
+ order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(items)
+ encode(order.cursor_attributes_for_node(node).to_json)
end
def sliced_nodes
- @sliced_nodes ||=
- begin
- OrderInfo.validate_ordering(ordered_items, order_list) unless loaded?(ordered_items)
-
- sliced = ordered_items
- sliced = slice_nodes(sliced, before, :before) if before.present?
- sliced = slice_nodes(sliced, after, :after) if after.present?
-
- sliced
- end
+ sliced = ordered_items
+ sliced = slice_nodes(sliced, before, :before) if before.present?
+ sliced = slice_nodes(sliced, after, :after) if after.present?
+ sliced
end
def nodes
@@ -104,6 +92,20 @@ module Gitlab
@nodes ||= limited_nodes.to_a
end
+ def items
+ original_items = super
+ return original_items if Gitlab::Pagination::Keyset::Order.keyset_aware?(original_items)
+
+ strong_memoize(:keyset_pagination_items) do
+ rebuilt_items_with_keyset_order, success =
+ Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(original_items)
+
+ raise(Gitlab::Pagination::Keyset::UnsupportedScopeOrder) unless success
+
+ rebuilt_items_with_keyset_order
+ end
+ end
+
private
# Apply `first` and `last` to `sliced_nodes`
@@ -129,11 +131,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def slice_nodes(sliced, encoded_cursor, before_or_after)
- decoded_cursor = ordering_from_encoded_json(encoded_cursor)
- builder = QueryBuilder.new(arel_table, order_list, decoded_cursor, before_or_after)
- ordering = builder.conditions
+ order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(sliced)
+ order = order.reversed_order if before_or_after == :before
- sliced.where(*ordering).where.not(id: decoded_cursor['id'])
+ decoded_cursor = ordering_from_encoded_json(encoded_cursor)
+ order.apply_cursor_conditions(sliced, decoded_cursor)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -157,57 +159,10 @@ module Gitlab
raise ArgumentError, 'Relation must have a primary key'
end
- list = OrderInfo.build_order_list(items)
-
- if loaded?(items) && !before.present? && !after.present?
- @order_list = list.presence || [OrderInfo.new(items.primary_key)]
-
- # already sorted, or trivially sorted
- next items if list.present? || items.size <= 1
-
- pkey = items.primary_key.to_sym
- next items.sort_by { |item| item[pkey] }.reverse
- end
-
- # ensure there is a primary key ordering
- if list&.last&.attribute_name != items.primary_key
- items.order(arel_table[items.primary_key].desc) # rubocop: disable CodeReuse/ActiveRecord
- else
- items
- end
- end
- end
-
- def order_list
- strong_memoize(:order_list) do
- OrderInfo.build_order_list(ordered_items)
+ items
end
end
- def arel_table
- items.arel_table
- end
-
- # Storing the current order values in the cursor allows us to
- # make an intelligent decision on handling NULL values.
- # Otherwise we would either need to fetch the record first,
- # or fetch it in the SQL, significantly complicating it.
- def encoded_json_from_ordering(node)
- ordering = { 'id' => node[:id].to_s }
-
- order_list.each do |field|
- field_name = field.try(:attribute_name) || field
- field_value = node[field_name]
- ordering[field_name] = if field_value.is_a?(Time)
- field_value.to_s(:inspect)
- else
- field_value.to_s
- end
- end
-
- encode(ordering.to_json)
- end
-
def ordering_from_encoded_json(cursor)
Gitlab::Json.parse(decode(cursor))
rescue JSON::ParserError
diff --git a/lib/gitlab/graphql/pagination/keyset/generic_keyset_pagination.rb b/lib/gitlab/graphql/pagination/keyset/generic_keyset_pagination.rb
deleted file mode 100644
index 9beb40ddd7e..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/generic_keyset_pagination.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- # https://gitlab.com/gitlab-org/gitlab/-/issues/334973
- # Use the generic keyset implementation if the given ActiveRecord scope supports it.
- # Note: this module is temporary, at some point it will be merged with Keyset::Connection
- module GenericKeysetPagination
- extend ActiveSupport::Concern
-
- # rubocop: disable Naming/PredicateName
- # rubocop: disable CodeReuse/ActiveRecord
- def has_next_page
- return super unless Gitlab::Pagination::Keyset::Order.keyset_aware?(items)
-
- strong_memoize(:generic_keyset_pagination_has_next_page) do
- if before
- true
- elsif first
- case sliced_nodes
- when Array
- sliced_nodes.size > limit_value
- else
- sliced_nodes.limit(1).offset(limit_value).exists?
- end
- else
- false
- end
- end
- end
-
- # rubocop: enable CodeReuse/ActiveRecord
- def ordered_items
- raise ArgumentError, 'Relation must have a primary key' unless items.primary_key.present?
-
- return super unless Gitlab::Pagination::Keyset::Order.keyset_aware?(items)
-
- items
- end
-
- def cursor_for(node)
- return super unless Gitlab::Pagination::Keyset::Order.keyset_aware?(items)
-
- order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(items)
- encode(order.cursor_attributes_for_node(node).to_json)
- end
-
- def slice_nodes(sliced, encoded_cursor, before_or_after)
- return super unless Gitlab::Pagination::Keyset::Order.keyset_aware?(sliced)
-
- order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(sliced)
- order = order.reversed_order if before_or_after == :before
-
- decoded_cursor = ordering_from_encoded_json(encoded_cursor)
- order.apply_cursor_conditions(sliced, decoded_cursor)
- end
-
- def sliced_nodes
- return super unless Gitlab::Pagination::Keyset::Order.keyset_aware?(items)
-
- sliced = ordered_items
- sliced = slice_nodes(sliced, before, :before) if before.present?
- sliced = slice_nodes(sliced, after, :after) if after.present?
- sliced
- end
-
- def items
- original_items = super
- return original_items if Feature.disabled?(:new_graphql_keyset_pagination) || Gitlab::Pagination::Keyset::Order.keyset_aware?(original_items)
-
- strong_memoize(:generic_keyset_pagination_items) do
- rebuilt_items_with_keyset_order, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(original_items)
-
- if success
- rebuilt_items_with_keyset_order
- else
- if original_items.is_a?(ActiveRecord::Relation)
- old_keyset_pagination_usage.increment({ model: original_items.model.to_s })
- end
-
- original_items
- end
- end
- end
-
- def old_keyset_pagination_usage
- @old_keyset_pagination_usage ||= Gitlab::Metrics.counter(
- :old_keyset_pagination_usage,
- 'The number of times the old keyset pagination code was used'
- )
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb
deleted file mode 100644
index 57e85ebe7f6..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/order_info.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- class OrderInfo
- attr_reader :attribute_name, :sort_direction, :named_function
-
- def initialize(order_value)
- @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
- 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, 'A minimum of 1 ordering field is required'
- end
-
- if order_list.count > 2
- # Keep in mind an order clause for primary key is added if one is not present
- # lib/gitlab/graphql/pagination/keyset/connection.rb:97
- raise ArgumentError, '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, "Column `#{attribute_name}` must not allow NULL"
- end
-
- if order_list.last.attribute_name != relation.primary_key
- raise ArgumentError, "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
-
- 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
- end
-
- def extract_attribute_values(order_value)
- if ordering_by_lower?(order_value)
- [order_value.expr.expressions[0].name.to_s, order_value.direction, order_value.expr]
- 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]
- else
- [order_value.expr.name, order_value.direction, nil]
- end
- 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'
- end
-
- # 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
-
- # determine if ordering using CASE
- def ordering_by_case?(order_value)
- order_value.expr.is_a?(Arel::Nodes::Case)
- end
- end
- end
- end
- end
-end
-
-Gitlab::Graphql::Pagination::Keyset::OrderInfo.prepend_mod_with('Gitlab::Graphql::Pagination::Keyset::OrderInfo')
diff --git a/lib/gitlab/graphql/pagination/keyset/query_builder.rb b/lib/gitlab/graphql/pagination/keyset/query_builder.rb
deleted file mode 100644
index a2f53ae83dd..00000000000
--- a/lib/gitlab/graphql/pagination/keyset/query_builder.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Pagination
- module Keyset
- class QueryBuilder
- def initialize(arel_table, order_list, decoded_cursor, before_or_after)
- @arel_table = arel_table
- @order_list = order_list
- @decoded_cursor = decoded_cursor
- @before_or_after = before_or_after
-
- if order_list.empty?
- raise ArgumentError, 'No ordering scopes have been supplied'
- end
- end
-
- # Based on whether the main field we're ordering on is NULL in the
- # cursor, we can more easily target our query condition.
- # We assume that the last ordering field is unique, meaning
- # it will not contain NULLs.
- # We currently only support two ordering fields.
- #
- # Example of the conditions for
- # relation: Issue.order(relative_position: :asc).order(id: :asc)
- # after cursor: relative_position: 1500, id: 500
- #
- # when cursor[relative_position] is not NULL
- #
- # ("issues"."relative_position" > 1500)
- # OR (
- # "issues"."relative_position" = 1500
- # AND
- # "issues"."id" > 500
- # )
- # OR ("issues"."relative_position" IS NULL)
- #
- # when cursor[relative_position] is NULL
- #
- # "issues"."relative_position" IS NULL
- # AND
- # "issues"."id" > 500
- #
- def conditions
- attr_values = order_list.map do |field|
- name = field.try(:attribute_name) || field
- decoded_cursor[name]
- end
-
- if order_list.count == 1 && attr_values.first.nil?
- raise Gitlab::Graphql::Errors::ArgumentError, 'Before/after cursor invalid: `nil` was provided as only sortable value'
- end
-
- if order_list.count == 1 || attr_values.first.present?
- Keyset::Conditions::NotNullCondition.new(arel_table, order_list, attr_values, operators, before_or_after).build
- else
- Keyset::Conditions::NullCondition.new(arel_table, order_list, attr_values, operators, before_or_after).build
- end
- end
-
- private
-
- attr_reader :arel_table, :order_list, :decoded_cursor, :before_or_after
-
- def operators
- order_list.map { |field| field.operator_for(before_or_after) }
- end
- end
- end
- end
- end
-end