Commit 1c508188 authored by Brett Walker's avatar Brett Walker

Ensure that there is always a primary key

change several user errors into programmer errors,
and other small refactoring/fixes
parent c1740e9a
...@@ -146,6 +146,10 @@ query($project_path: ID!) { ...@@ -146,6 +146,10 @@ query($project_path: ID!) {
} }
``` ```
To ensure that we get consistent ordering, we will append an ordering on the primary
key, in descending order. This is usually `id`, so basically we will add `order(id: :desc)`
to the end of the relation. A primary key _must_ be available on the underlying table.
### Exposing permissions for a type ### Exposing permissions for a type
To expose permissions the current user has on a resource, you can call To expose permissions the current user has on a resource, you can call
......
...@@ -18,10 +18,6 @@ module Gitlab ...@@ -18,10 +18,6 @@ module Gitlab
attr_reader :arel_table, :names, :values, :operator, :before_or_after attr_reader :arel_table, :names, :values, :operator, :before_or_after
def assemble_conditions(conditions)
conditions.join
end
def table_condition(attribute, value, operator) def table_condition(attribute, value, operator)
case operator case operator
when '>' when '>'
......
...@@ -7,18 +7,16 @@ module Gitlab ...@@ -7,18 +7,16 @@ module Gitlab
module Conditions module Conditions
class NotNullCondition < BaseCondition class NotNullCondition < BaseCondition
def build def build
conditions = [] conditions = [first_attribute_condition]
conditions << first_attribute_condition
# If there is only one order field, we can assume it # If there is only one order field, we can assume it
# does not contain NULLs, and don't need additional # does not contain NULLs, and don't need additional
# conditions # conditions
unless names.count == 1 unless names.count == 1
conditions << second_attribute_condition conditions << [second_attribute_condition, final_condition]
conditions << final_condition
end end
assemble_conditions(conditions) conditions.join
end end
private private
......
...@@ -7,11 +7,7 @@ module Gitlab ...@@ -7,11 +7,7 @@ module Gitlab
module Conditions module Conditions
class NullCondition < BaseCondition class NullCondition < BaseCondition
def build def build
conditions = [] [first_attribute_condition, final_condition].join
conditions << first_attribute_condition
conditions << final_condition
assemble_conditions(conditions)
end end
private private
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# Keyset::Connection provides cursor based pagination, to avoid using OFFSET. # Keyset::Connection provides cursor based pagination, to avoid using OFFSET.
# It basically sorts / filters using WHERE sorting_value > cursor. # It basically sorts / filters using WHERE sorting_value > cursor.
# We do this for performance reasons (https://gitlab.com/gitlab-org/gitlab-ce/issues/45756), # We do this for performance reasons (https://gitlab.com/gitlab-org/gitlab-foss/issues/45756),
# as well as for having stable pagination # as well as for having stable pagination
# https://graphql-ruby.org/pro/cursors.html#whats-the-difference # https://graphql-ruby.org/pro/cursors.html#whats-the-difference
# https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong # https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong
...@@ -82,6 +82,10 @@ module Gitlab ...@@ -82,6 +82,10 @@ module Gitlab
def ordered_nodes def ordered_nodes
strong_memoize(:order_nodes) do strong_memoize(:order_nodes) do
unless nodes.primary_key.present?
raise ArgumentError.new('Relation must have a primary key')
end
list = OrderInfo.build_order_list(nodes) list = OrderInfo.build_order_list(nodes)
# ensure there is a primary key ordering # ensure there is a primary key ordering
......
...@@ -33,22 +33,22 @@ module Gitlab ...@@ -33,22 +33,22 @@ module Gitlab
def self.validate_ordering(relation, order_list) def self.validate_ordering(relation, order_list)
if order_list.empty? if order_list.empty?
raise ::ArgumentError.new('A minimum of 1 ordering field is required') raise ArgumentError.new('A minimum of 1 ordering field is required')
end end
if order_list.count > 2 if order_list.count > 2
raise Gitlab::Graphql::Errors::ArgumentError.new('A maximum of 2 ordering fields are allowed') raise ArgumentError.new('A maximum of 2 ordering fields are allowed')
end end
# make sure the last ordering field is non-nullable # make sure the last ordering field is non-nullable
attribute_name = order_list.last&.attribute_name attribute_name = order_list.last&.attribute_name
if relation.columns_hash[attribute_name].null if relation.columns_hash[attribute_name].null
raise Gitlab::Graphql::Errors::ArgumentError.new("Column `#{attribute_name}` must not allow NULL") raise ArgumentError.new("Column `#{attribute_name}` must not allow NULL")
end end
if order_list.last.attribute_name != relation.primary_key if order_list.last.attribute_name != relation.primary_key
raise Gitlab::Graphql::Errors::ArgumentError.new("Last ordering field must be the primary key, `#{relation.primary_key}`") raise ArgumentError.new("Last ordering field must be the primary key, `#{relation.primary_key}`")
end end
end end
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
@arel_table, @order_list, @decoded_cursor, @before_or_after = arel_table, order_list, decoded_cursor, before_or_after @arel_table, @order_list, @decoded_cursor, @before_or_after = arel_table, order_list, decoded_cursor, before_or_after
if order_list.empty? if order_list.empty?
raise Gitlab::Graphql::Errors::ArgumentError.new('No ordering scopes have been supplied') raise ArgumentError.new('No ordering scopes have been supplied')
end end
end end
...@@ -44,12 +44,10 @@ module Gitlab ...@@ -44,12 +44,10 @@ module Gitlab
attr_values = attr_names.map { |name| decoded_cursor[name] } attr_values = attr_names.map { |name| decoded_cursor[name] }
if attr_names.count == 1 && attr_values.first.nil? if attr_names.count == 1 && attr_values.first.nil?
raise Gitlab::Graphql::Errors::ArgumentError.new('Only one sortable scope and nil was supplied') raise Gitlab::Graphql::Errors::ArgumentError.new('Before/after cursor invalid: `nil` was provided as only sortable value')
end end
operators = comparison_operators if attr_names.count == 1 || attr_values.first.present?
if attr_names.count == 1 || attr_values.first.presence
Keyset::Conditions::NotNullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build Keyset::Conditions::NotNullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
else else
Keyset::Conditions::NullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build Keyset::Conditions::NullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
...@@ -60,7 +58,7 @@ module Gitlab ...@@ -60,7 +58,7 @@ module Gitlab
attr_reader :arel_table, :order_list, :decoded_cursor, :before_or_after attr_reader :arel_table, :order_list, :decoded_cursor, :before_or_after
def comparison_operators def operators
order_list.map { |field| field.operator_for(before_or_after) } order_list.map { |field| field.operator_for(before_or_after) }
end end
end end
......
...@@ -14,7 +14,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do ...@@ -14,7 +14,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do
("issues"."id" > 500) ("issues"."id" > 500)
SQL SQL
expect(condition.build).to eq expected_sql expect(condition.build.squish).to eq expected_sql.squish
end end
end end
...@@ -32,7 +32,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do ...@@ -32,7 +32,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do
OR ("issues"."relative_position" IS NULL) OR ("issues"."relative_position" IS NULL)
SQL SQL
expect(condition.build).to eq expected_sql expect(condition.build.squish).to eq expected_sql.squish
end end
end end
...@@ -49,7 +49,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do ...@@ -49,7 +49,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do
) )
SQL SQL
expect(condition.build).to eq expected_sql expect(condition.build.squish).to eq expected_sql.squish
end end
end end
end end
......
...@@ -18,7 +18,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NullCondition do ...@@ -18,7 +18,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NullCondition do
) )
SQL SQL
expect(condition.build).to eq expected_sql expect(condition.build.squish).to eq expected_sql.squish
end end
end end
...@@ -35,7 +35,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NullCondition do ...@@ -35,7 +35,7 @@ describe Gitlab::Graphql::Connections::Keyset::Conditions::NullCondition do
OR ("issues"."relative_position" IS NOT NULL) OR ("issues"."relative_position" IS NOT NULL)
SQL SQL
expect(condition.build).to eq expected_sql expect(condition.build.squish).to eq expected_sql.squish
end end
end end
end end
......
...@@ -22,34 +22,34 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do ...@@ -22,34 +22,34 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
let(:cursor) { connection.cursor_from_node(project) } let(:cursor) { connection.cursor_from_node(project) }
it 'returns an encoded ID' do it 'returns an encoded ID' do
expect(decoded_cursor(cursor)).to eq({ 'id' => project.id.to_s }) expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s)
end end
context 'when an order is specified' do context 'when an order is specified' do
let(:nodes) { Project.order(:updated_at) } let(:nodes) { Project.order(:updated_at) }
it 'returns the encoded value of the order' do it 'returns the encoded value of the order' do
expect(decoded_cursor(cursor)).to include({ 'updated_at' => project.updated_at.to_s }) expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
end end
it 'includes the :id even when not specified in the order' do it 'includes the :id even when not specified in the order' do
expect(decoded_cursor(cursor)).to include({ 'id' => project.id.to_s }) expect(decoded_cursor(cursor)).to include('id' => project.id.to_s)
end end
end end
context 'when multiple orders is specified' do context 'when multiple orders are specified' do
let(:nodes) { Project.order(:updated_at).order(:created_at) } let(:nodes) { Project.order(:updated_at).order(:created_at) }
it 'returns the encoded value of the order' do it 'returns the encoded value of the order' do
expect(decoded_cursor(cursor)).to include({ 'updated_at' => project.updated_at.to_s }) expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
end end
end end
context 'when multiple orders with SQL is specified' do context 'when multiple orders with SQL are specified' do
let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) }
it 'returns the encoded value of the order' do it 'returns the encoded value of the order' do
expect(decoded_cursor(cursor)).to include({ 'updated_at' => project.updated_at.to_s }) expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
end end
end end
end end
...@@ -285,5 +285,19 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do ...@@ -285,5 +285,19 @@ describe Gitlab::Graphql::Connections::Keyset::Connection do
expect(last_order_name).to eq sliced.primary_key expect(last_order_name).to eq sliced.primary_key
end end
end end
context 'when there is no primary key' do
let(:nodes) { NoPrimaryKey.all }
it 'raises an error' do
expect(NoPrimaryKey.primary_key).to be_nil
expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key')
end
end
end
class NoPrimaryKey < ActiveRecord::Base
self.table_name = 'no_primary_key'
self.primary_key = nil
end end
end end
...@@ -27,7 +27,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do ...@@ -27,7 +27,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do
it 'raises an error' do it 'raises an error' do
expect { described_class.validate_ordering(relation, order_list) } expect { described_class.validate_ordering(relation, order_list) }
.to raise_error(::ArgumentError, 'A minimum of 1 ordering field is required') .to raise_error(ArgumentError, 'A minimum of 1 ordering field is required')
end end
end end
...@@ -36,7 +36,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do ...@@ -36,7 +36,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do
it 'raises an error' do it 'raises an error' do
expect { described_class.validate_ordering(relation, order_list) } expect { described_class.validate_ordering(relation, order_list) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'A maximum of 2 ordering fields are allowed') .to raise_error(ArgumentError, 'A maximum of 2 ordering fields are allowed')
end end
end end
...@@ -45,7 +45,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do ...@@ -45,7 +45,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do
it 'raises an error' do it 'raises an error' do
expect { described_class.validate_ordering(relation, order_list) } expect { described_class.validate_ordering(relation, order_list) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "Column `updated_at` must not allow NULL") .to raise_error(ArgumentError, "Column `updated_at` must not allow NULL")
end end
end end
...@@ -54,7 +54,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do ...@@ -54,7 +54,7 @@ describe Gitlab::Graphql::Connections::Keyset::OrderInfo do
it 'raises error if primary key is not last field' do it 'raises error if primary key is not last field' do
expect { described_class.validate_ordering(relation, order_list) } expect { described_class.validate_ordering(relation, order_list) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "Last ordering field must be the primary key, `#{relation.primary_key}`") .to raise_error(ArgumentError, "Last ordering field must be the primary key, `#{relation.primary_key}`")
end end
end end
end end
......
...@@ -6,7 +6,7 @@ describe Gitlab::Graphql::Connections::Keyset::QueryBuilder do ...@@ -6,7 +6,7 @@ describe Gitlab::Graphql::Connections::Keyset::QueryBuilder do
context 'when number of ordering fields is 0' do context 'when number of ordering fields is 0' do
it 'raises an error' do it 'raises an error' do
expect { described_class.new(Issue.arel_table, [], {}, :after) } expect { described_class.new(Issue.arel_table, [], {}, :after) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'No ordering scopes have been supplied') .to raise_error(ArgumentError, 'No ordering scopes have been supplied')
end end
end end
...@@ -24,7 +24,7 @@ describe Gitlab::Graphql::Connections::Keyset::QueryBuilder do ...@@ -24,7 +24,7 @@ describe Gitlab::Graphql::Connections::Keyset::QueryBuilder do
it 'raises an error' do it 'raises an error' do
expect { builder.conditions } expect { builder.conditions }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Only one sortable scope and nil was supplied') .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Before/after cursor invalid: `nil` was provided as only sortable value')
end end
end end
......
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