diff options
Diffstat (limited to 'lib/gitlab/pagination/keyset')
8 files changed, 127 insertions, 14 deletions
diff --git a/lib/gitlab/pagination/keyset/column_order_definition.rb b/lib/gitlab/pagination/keyset/column_order_definition.rb index 2b968c4253f..302e7b406b1 100644 --- a/lib/gitlab/pagination/keyset/column_order_definition.rb +++ b/lib/gitlab/pagination/keyset/column_order_definition.rb @@ -114,6 +114,20 @@ module Gitlab # - When the order is a calculated expression or the column is in another table (JOIN-ed) # # If the add_to_projections is true, the query builder will automatically add the column to the SELECT values + # + # **sql_type** + # + # The SQL type of the column or SQL expression. This is an optional field which is only required when using the + # column with the InOperatorOptimization class. + # + # Example: When the order expression is a calculated SQL expression. + # + # { + # attribute_name: 'id_times_count', + # order_expression: Arel.sql('(id * count)').asc, + # sql_type: 'integer' # the SQL type here must match with the type of the produced data by the order_expression. Putting 'text' here would be incorrect. + # } + # class ColumnOrderDefinition REVERSED_ORDER_DIRECTIONS = { asc: :desc, desc: :asc }.freeze REVERSED_NULL_POSITIONS = { nulls_first: :nulls_last, nulls_last: :nulls_first }.freeze @@ -122,7 +136,8 @@ module Gitlab attr_reader :attribute_name, :column_expression, :order_expression, :add_to_projections, :order_direction - def initialize(attribute_name:, order_expression:, column_expression: nil, reversed_order_expression: nil, nullable: :not_nullable, distinct: true, order_direction: nil, add_to_projections: false) + # rubocop: disable Metrics/ParameterLists + def initialize(attribute_name:, order_expression:, column_expression: nil, reversed_order_expression: nil, nullable: :not_nullable, distinct: true, order_direction: nil, sql_type: nil, add_to_projections: false) @attribute_name = attribute_name @order_expression = order_expression @column_expression = column_expression || calculate_column_expression(order_expression) @@ -130,8 +145,10 @@ module Gitlab @reversed_order_expression = reversed_order_expression || calculate_reversed_order(order_expression) @nullable = parse_nullable(nullable, distinct) @order_direction = parse_order_direction(order_expression, order_direction) + @sql_type = sql_type @add_to_projections = add_to_projections end + # rubocop: enable Metrics/ParameterLists def reverse self.class.new( @@ -185,6 +202,12 @@ module Gitlab sql_string end + def sql_type + raise Gitlab::Pagination::Keyset::SqlTypeMissingError.for_column(self) if @sql_type.nil? + + @sql_type + end + private attr_reader :reversed_order_expression, :nullable, :distinct diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb index 3f620f74eca..93b28661bb0 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/column_data.rb @@ -4,23 +4,35 @@ module Gitlab module Pagination module Keyset module InOperatorOptimization + # This class is used for wrapping an Arel column with + # convenient helper methods in order to make the query + # building for the InOperatorOptimization a bit cleaner. class ColumnData attr_reader :original_column_name, :as, :arel_table - def initialize(original_column_name, as, arel_table) - @original_column_name = original_column_name.to_s + # column - name of the DB column + # as - custom alias for the column + # arel_table - relation where the column is located + def initialize(column, as, arel_table) + @original_column_name = column @as = as.to_s @arel_table = arel_table end + # Generates: `issues.name AS my_alias` def projection arel_column.as(as) end + # Generates: issues.name` def arel_column arel_table[original_column_name] end + # overridden in OrderByColumnData class + alias_method :column_expression, :arel_column + + # Generates: `issues.my_alias` def arel_column_as arel_table[as] end @@ -29,8 +41,9 @@ module Gitlab "#{arel_table.name}_#{original_column_name}_array" end + # Generates: SELECT ARRAY_AGG(...) AS issues_name_array def array_aggregated_column - Arel::Nodes::NamedFunction.new('ARRAY_AGG', [arel_column]).as(array_aggregated_column_name) + Arel::Nodes::NamedFunction.new('ARRAY_AGG', [column_expression]).as(array_aggregated_column_name) end end end diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb new file mode 100644 index 00000000000..9cb1ba1542d --- /dev/null +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_column_data.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module Pagination + module Keyset + module InOperatorOptimization + class OrderByColumnData < ColumnData + extend ::Gitlab::Utils::Override + + attr_reader :column + + # column - a ColumnOrderDefinition object + # as - custom alias for the column + # arel_table - relation where the column is located + def initialize(column, as, arel_table) + super(column.attribute_name.to_s, as, arel_table) + @column = column + end + + override :arel_column + def arel_column + column.column_expression + end + + override :column_expression + def column_expression + arel_table[original_column_name] + end + + def column_for_projection + column.column_expression.as(original_column_name) + end + end + end + end + end +end diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb index d8c69a74e6b..d6513114d08 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/order_by_columns.rb @@ -9,16 +9,16 @@ module Gitlab # This class exposes collection methods for the order by columns # - # Example: by modelling the `issues.created_at ASC, issues.id ASC` ORDER BY + # Example: by modeling the `issues.created_at ASC, issues.id ASC` ORDER BY # SQL clause, this class will receive two ColumnOrderDefinition objects def initialize(columns, arel_table) @columns = columns.map do |column| - ColumnData.new(column.attribute_name, "order_by_columns_#{column.attribute_name}", arel_table) + OrderByColumnData.new(column, "order_by_columns_#{column.attribute_name}", arel_table) end end def arel_columns - columns.map(&:arel_column) + columns.map(&:column_for_projection) end def array_aggregated_columns diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb index 53faf8469f2..065a3a0cf20 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder.rb @@ -120,7 +120,7 @@ module Gitlab .from(array_cte) .join(Arel.sql("LEFT JOIN LATERAL (#{initial_keyset_query.to_sql}) #{table_name} ON TRUE")) - order_by_columns.each { |column| q.where(column.arel_column.not_eq(nil)) } + order_by_columns.each { |column| q.where(column.column_expression.not_eq(nil)) } q.as('array_scope_lateral_query') end @@ -231,7 +231,7 @@ module Gitlab order .apply_cursor_conditions(keyset_scope, cursor_values, use_union_optimization: true) - .reselect(*order_by_columns.arel_columns) + .reselect(*order_by_columns.map(&:column_for_projection)) .limit(1) end diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb index fc2b56048f6..932aa0c2d28 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy.rb @@ -12,11 +12,7 @@ module Gitlab end def initializer_columns - order_by_columns.map do |column| - column_name = column.original_column_name.to_s - type = model.columns_hash[column_name].sql_type - "NULL::#{type} AS #{column_name}" - end + order_by_columns.map { |column_data| null_with_type_cast(column_data) } end def columns @@ -30,6 +26,15 @@ module Gitlab private attr_reader :model, :order_by_columns + + def null_with_type_cast(column_data) + column_name = column_data.original_column_name.to_s + active_record_column = model.columns_hash[column_name] + + type = active_record_column ? active_record_column.sql_type : column_data.column.sql_type + + "NULL::#{type} AS #{column_name}" + end end end end diff --git a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb index b12c33d6e51..51f38c1da58 100644 --- a/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb +++ b/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/record_loader_strategy.rb @@ -9,6 +9,8 @@ module Gitlab RECORDS_COLUMN = 'records' def initialize(finder_query, model, order_by_columns) + verify_order_by_attributes_on_model!(model, order_by_columns) + @finder_query = finder_query @order_by_columns = order_by_columns @table_name = model.table_name @@ -34,6 +36,20 @@ module Gitlab private attr_reader :finder_query, :order_by_columns, :table_name + + def verify_order_by_attributes_on_model!(model, order_by_columns) + order_by_columns.map(&:column).each do |column| + unless model.columns_hash[column.attribute_name.to_s] + text = <<~TEXT + The "RecordLoaderStrategy" does not support the following ORDER BY column because + it's not available on the \"#{model.table_name}\" table: #{column.attribute_name} + + Omit the "finder_query" parameter to use the "OrderValuesLoaderStrategy". + TEXT + raise text + end + end + end end end end diff --git a/lib/gitlab/pagination/keyset/sql_type_missing_error.rb b/lib/gitlab/pagination/keyset/sql_type_missing_error.rb new file mode 100644 index 00000000000..0525ae13e9c --- /dev/null +++ b/lib/gitlab/pagination/keyset/sql_type_missing_error.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module Gitlab + module Pagination + module Keyset + class SqlTypeMissingError < StandardError + def self.for_column(column) + message = <<~TEXT + The "sql_type" attribute is not set for the following column definition: + #{column.attribute_name} + + See the ColumnOrderDefinition class for more context. + TEXT + + new(message) + end + end + end + end +end |