Add Gitlab::SQL:CTE for easily building CTE statements
This commit is contained in:
parent
75797ac3d2
commit
fe8261fdb2
2 changed files with 92 additions and 0 deletions
50
lib/gitlab/sql/cte.rb
Normal file
50
lib/gitlab/sql/cte.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
module Gitlab
|
||||
module SQL
|
||||
# Class for easily building CTE statements.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# cte = CTE.new(:my_cte_name)
|
||||
# ns = Arel::Table.new(:namespaces)
|
||||
#
|
||||
# cte << Namespace.
|
||||
# where(ns[:parent_id].eq(some_namespace_id))
|
||||
#
|
||||
# Namespace
|
||||
# with(cte.to_arel).
|
||||
# from(cte.alias_to(ns))
|
||||
class CTE
|
||||
attr_reader :table, :query
|
||||
|
||||
# name - The name of the CTE as a String or Symbol.
|
||||
def initialize(name, query)
|
||||
@table = Arel::Table.new(name)
|
||||
@query = query
|
||||
end
|
||||
|
||||
# Returns the Arel relation for this CTE.
|
||||
def to_arel
|
||||
sql = Arel::Nodes::SqlLiteral.new("(#{query.to_sql})")
|
||||
|
||||
Arel::Nodes::As.new(table, sql)
|
||||
end
|
||||
|
||||
# Returns an "AS" statement that aliases the CTE name as the given table
|
||||
# name. This allows one to trick ActiveRecord into thinking it's selecting
|
||||
# from an actual table, when in reality it's selecting from a CTE.
|
||||
#
|
||||
# alias_table - The Arel table to use as the alias.
|
||||
def alias_to(alias_table)
|
||||
Arel::Nodes::As.new(table, alias_table)
|
||||
end
|
||||
|
||||
# Applies the CTE to the given relation, returning a new one that will
|
||||
# query from it.
|
||||
def apply_to(relation)
|
||||
relation.except(:where)
|
||||
.with(to_arel)
|
||||
.from(alias_to(relation.model.arel_table))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
42
spec/lib/gitlab/sql/cte_spec.rb
Normal file
42
spec/lib/gitlab/sql/cte_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::SQL::CTE, :postgresql do
|
||||
describe '#to_arel' do
|
||||
it 'generates an Arel relation for the CTE body' do
|
||||
relation = User.where(id: 1)
|
||||
cte = described_class.new(:cte_name, relation)
|
||||
sql = cte.to_arel.to_sql
|
||||
name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
|
||||
|
||||
sql1 = ActiveRecord::Base.connection.unprepared_statement do
|
||||
relation.except(:order).to_sql
|
||||
end
|
||||
|
||||
expect(sql).to eq("#{name} AS (#{sql1})")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#alias_to' do
|
||||
it 'returns an alias for the CTE' do
|
||||
cte = described_class.new(:cte_name, nil)
|
||||
table = Arel::Table.new(:kittens)
|
||||
|
||||
source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name)
|
||||
alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens)
|
||||
|
||||
expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#apply_to' do
|
||||
it 'applies a CTE to an ActiveRecord::Relation' do
|
||||
user = create(:user)
|
||||
cte = described_class.new(:cte_name, User.where(id: user.id))
|
||||
|
||||
relation = cte.apply_to(User.all)
|
||||
|
||||
expect(relation.to_sql).to match(/WITH .+cte_name/)
|
||||
expect(relation.to_a).to eq(User.where(id: user.id).to_a)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue