This instance was upgraded to Heptapod 0.27.2 on 2021-12-08 at 22:00 UTC+1

Commit b8f7645b authored by Yorick Peterse's avatar Yorick Peterse
Browse files

Added FromUnion to easily select from a UNION

This commit adds the module `FromUnion`, which provides the class method
`from_union`. This simplifies the process of selecting data from the
result of a UNION, and reduces the likelihood of making mistakes. As a
result, instead of this:

    union = Gitlab::SQL::Union.new([foo, bar])

    Foo.from("(#{union.to_sql}) #{Foo.table_name}")

We can now write this instead:

    Foo.from_union([foo, bar])

This commit also includes some changes to make this new setup work
properly. For example, a bug in Rails 4
(https://github.com/rails/rails/issues/24193) would break the use of
`from("sub-query-here").includes(:relation)` in certain cases. There was
also a CI query which appeared to repeat a lot of conditions from an
outer query on an inner query, which isn't necessary.

Finally, we include a RuboCop cop to ensure developers use this new
module, instead of using Gitlab::SQL::Union directly.

Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/51307
parent c5f3cdb4e8f9
......@@ -2,6 +2,8 @@ module Gitlab
module Database
# Model that can be used for querying permissions of a SQL user.
class Grant < ActiveRecord::Base
include FromUnion
self.table_name =
if Database.postgresql?
'information_schema.role_table_grants'
......@@ -42,9 +44,7 @@ def self.create_and_execute_trigger?(table)
.where("GRANTEE = CONCAT('\\'', REPLACE(CURRENT_USER(), '@', '\\'@\\''), '\\'')")
]
union = SQL::Union.new(queries).to_sql
Grant.from("(#{union}) privs").any?
Grant.from_union(queries, alias_as: 'privs').any?
end
end
end
......
......@@ -89,14 +89,14 @@ def all_groups
ancestors_table = ancestors.alias_to(groups_table)
descendants_table = descendants.alias_to(groups_table)
union = SQL::Union.new([model.unscoped.from(ancestors_table),
model.unscoped.from(descendants_table)])
relation = model
.unscoped
.with
.recursive(ancestors.to_arel, descendants.to_arel)
.from("(#{union.to_sql}) #{model.table_name}")
.from_union([
model.unscoped.from(ancestors_table),
model.unscoped.from(descendants_table)
])
read_only(relation)
end
......
......@@ -49,13 +49,11 @@ def calculate
.where('p_ns.share_with_group_lock IS FALSE')
]
union = Gitlab::SQL::Union.new(relations)
ProjectAuthorization
.unscoped
.with
.recursive(cte.to_arel)
.select_from_union(union)
.select_from_union(relations)
end
private
......
......@@ -24,11 +24,9 @@ def calculate
user.groups.joins(:shared_projects).select_for_project_authorization
]
union = Gitlab::SQL::Union.new(relations)
ProjectAuthorization
.unscoped
.select_from_union(union)
.select_from_union(relations)
end
end
end
......
......@@ -18,7 +18,7 @@ def initialize(text)
def users
return User.none unless @text.present?
@users ||= User.from("(#{union.to_sql}) users")
@users ||= User.from_union(union_relations)
end
# rubocop: enable CodeReuse/ActiveRecord
......@@ -43,13 +43,13 @@ def matches
private
def union
def union_relations
relations = []
relations << User.by_any_email(emails) if emails.any?
relations << User.by_username(usernames) if usernames.any?
Gitlab::SQL::Union.new(relations)
relations
end
end
end
# frozen_string_literal: true
require_relative '../../spec_helpers'
module RuboCop
module Cop
module Gitlab
# Cop that disallows the use of `Gitlab::SQL::Union`, in favour of using
# the `FromUnion` module.
class Union < RuboCop::Cop::Cop
include SpecHelpers
MSG = 'Use the `FromUnion` concern, instead of using `Gitlab::SQL::Union` directly'
def_node_matcher :raw_union?, <<~PATTERN
(send (const (const (const nil? :Gitlab) :SQL) :Union) :new ...)
PATTERN
def on_send(node)
return unless raw_union?(node)
return if in_spec?(node)
add_offense(node, location: :expression)
end
end
end
end
end
......@@ -3,6 +3,7 @@
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/finder_with_find_by'
require_relative 'cop/gitlab/union'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/avoid_return_from_blocks'
require_relative 'cop/avoid_break_from_strong_memoize'
......
# frozen_string_literal: true
require 'spec_helper'
describe FromUnion do
describe '.from_union' do
let(:model) do
Class.new(ActiveRecord::Base) do
self.table_name = 'users'
include FromUnion
end
end
it 'selects from the results of the UNION' do
query = model.from_union([model.where(id: 1), model.where(id: 2)])
expect(query.to_sql).to match(/FROM \(SELECT.+UNION.+SELECT.+\) users/m)
end
it 'supports the use of a custom alias for the sub query' do
query = model.from_union(
[model.where(id: 1), model.where(id: 2)],
alias_as: 'kittens'
)
expect(query.to_sql).to match(/FROM \(SELECT.+UNION.+SELECT.+\) kittens/m)
end
it 'supports keeping duplicate rows' do
query = model.from_union(
[model.where(id: 1), model.where(id: 2)],
remove_duplicates: false
)
expect(query.to_sql)
.to match(/FROM \(SELECT.+UNION ALL.+SELECT.+\) users/m)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/union'
describe RuboCop::Cop::Gitlab::Union do
include CopHelper
subject(:cop) { described_class.new }
it 'flags the use of Gitlab::SQL::Union.new' do
expect_offense(<<~SOURCE)
Gitlab::SQL::Union.new([foo])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `FromUnion` concern, instead of using `Gitlab::SQL::Union` directly
SOURCE
end
it 'does not flag the use of Gitlab::SQL::Union in a spec' do
allow(cop).to receive(:in_spec?).and_return(true)
expect_no_offenses('Gitlab::SQL::Union.new([foo])')
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