Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Arel visitors #2002

Merged
merged 13 commits into from
Apr 15, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
# contribution.
# portions Copyright 2005 Graham Jenkins

require "arel/visitors/oracle"
require "arel/visitors/oracle12"
require "active_record/connection_adapters"
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
Expand Down
158 changes: 158 additions & 0 deletions lib/arel/visitors/oracle.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true

module Arel # :nodoc: all
module Visitors
class Oracle < Arel::Visitors::ToSql
private
def visit_Arel_Nodes_SelectStatement(o, collector)
o = order_hacks(o)

# if need to select first records without ORDER BY and GROUP BY and without DISTINCT
# then can use simple ROWNUM in WHERE clause
if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && !o.cores.first.set_quantifier.class.to_s.match?(/Distinct/)
o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr
)
return super
end

if o.limit && o.offset
o = o.dup
limit = o.limit.expr
offset = o.offset
o.offset = nil
collector << "
SELECT * FROM (
SELECT raw_sql_.*, rownum raw_rnum_
FROM ("

collector = super(o, collector)

if offset.expr.is_a? Nodes::BindParam
collector << ") raw_sql_ WHERE rownum <= ("
collector = visit offset.expr, collector
collector << " + "
collector = visit limit, collector
collector << ") ) WHERE raw_rnum_ > "
collector = visit offset.expr, collector
return collector
else
collector << ") raw_sql_
WHERE rownum <= #{offset.expr.to_i + limit}
)
WHERE "
return visit(offset, collector)
end
end

if o.limit
o = o.dup
limit = o.limit.expr
collector << "SELECT * FROM ("
collector = super(o, collector)
collector << ") WHERE ROWNUM <= "
return visit limit, collector
end

if o.offset
o = o.dup
offset = o.offset
o.offset = nil
collector << "SELECT * FROM (
SELECT raw_sql_.*, rownum raw_rnum_
FROM ("
collector = super(o, collector)
collector << ") raw_sql_
)
WHERE "
return visit offset, collector
end

super
end

def visit_Arel_Nodes_Limit(o, collector)
collector
end

def visit_Arel_Nodes_Offset(o, collector)
collector << "raw_rnum_ > "
visit o.expr, collector
end

def visit_Arel_Nodes_Except(o, collector)
collector << "( "
collector = infix_value o, collector, " MINUS "
collector << " )"
end

def visit_Arel_Nodes_UpdateStatement(o, collector)
# Oracle does not allow ORDER BY/LIMIT in UPDATEs.
if o.orders.any? && o.limit.nil?
# However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
# otherwise let the user deal with the error
o = o.dup
o.orders = []
end

super
end

###
# Hacks for the order clauses specific to Oracle
def order_hacks(o)
return o if o.orders.empty?
return o unless o.cores.any? do |core|
core.projections.any? do |projection|
/FIRST_VALUE/ === projection
end
end
# Previous version with join and split broke ORDER BY clause
# if it contained functions with several arguments (separated by ',').
#
# orders = o.orders.map { |x| visit x }.join(', ').split(',')
orders = o.orders.map do |x|
string = visit(x, Arel::Collectors::SQLString.new).value
if string.include?(",")
split_order_string(string)
else
string
end
end.flatten
o.orders = []
orders.each_with_index do |order, i|
o.orders <<
Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i.match?(order)}")
end
o
end

# Split string by commas but count opening and closing brackets
# and ignore commas inside brackets.
def split_order_string(string)
array = []
i = 0
string.split(",").each do |part|
if array[i]
array[i] << "," << part
else
# to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral
array[i] = part.to_s
end
i += 1 if array[i].count("(") == array[i].count(")")
end
array
end

def visit_Arel_Nodes_BindParam(o, collector)
collector.add_bind(o.value) { |i| ":a#{i}" }
end

def is_distinct_from(o, collector)
collector << "DECODE("
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
end
end
end
65 changes: 65 additions & 0 deletions lib/arel/visitors/oracle12.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

module Arel # :nodoc: all
module Visitors
class Oracle12 < Arel::Visitors::ToSql
private
def visit_Arel_Nodes_SelectStatement(o, collector)
# Oracle does not allow LIMIT clause with select for update
if o.limit && o.lock
raise ArgumentError, <<~MSG
Combination of limit and lock is not supported. Because generated SQL statements
`SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.
MSG
end
super
end

def visit_Arel_Nodes_SelectOptions(o, collector)
collector = maybe_visit o.offset, collector
collector = maybe_visit o.limit, collector
maybe_visit o.lock, collector
end

def visit_Arel_Nodes_Limit(o, collector)
collector << "FETCH FIRST "
collector = visit o.expr, collector
collector << " ROWS ONLY"
end

def visit_Arel_Nodes_Offset(o, collector)
collector << "OFFSET "
visit o.expr, collector
collector << " ROWS"
end

def visit_Arel_Nodes_Except(o, collector)
collector << "( "
collector = infix_value o, collector, " MINUS "
collector << " )"
end

def visit_Arel_Nodes_UpdateStatement(o, collector)
# Oracle does not allow ORDER BY/LIMIT in UPDATEs.
if o.orders.any? && o.limit.nil?
# However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
# otherwise let the user deal with the error
o = o.dup
o.orders = []
end

super
end

def visit_Arel_Nodes_BindParam(o, collector)
collector.add_bind(o.value) { |i| ":a#{i}" }
end

def is_distinct_from(o, collector)
collector << "DECODE("
collector = visit [o.left, o.right, 0, 1], collector
collector << ")"
end
end
end
end
100 changes: 100 additions & 0 deletions test/cases/arel/visitors/oracle12_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# frozen_string_literal: true

require_relative "../helper"

module Arel
module Visitors
class Oracle12Test < Arel::Spec
before do
@visitor = Oracle12.new Table.engine.connection
@table = Table.new(:users)
end

def compile(node)
@visitor.accept(node, Collectors::SQLString.new).value
end

it "modified except to be minus" do
left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
sql = compile Nodes::Except.new(left, right)
sql.must_be_like %{
( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
}
end

it "generates select options offset then limit" do
stmt = Nodes::SelectStatement.new
stmt.offset = Nodes::Offset.new(1)
stmt.limit = Nodes::Limit.new(10)
sql = compile(stmt)
sql.must_be_like "SELECT OFFSET 1 ROWS FETCH FIRST 10 ROWS ONLY"
end

describe "locking" do
it "generates ArgumentError if limit and lock are used" do
stmt = Nodes::SelectStatement.new
stmt.limit = Nodes::Limit.new(10)
stmt.lock = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
assert_raises ArgumentError do
compile(stmt)
end
end

it "defaults to FOR UPDATE when locking" do
node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
compile(node).must_be_like "FOR UPDATE"
end
end

describe "Nodes::BindParam" do
it "increments each bind param" do
query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
.and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
compile(query).must_be_like %{
"users"."name" = :a1 AND "users"."id" = :a2
}
end
end

describe "Nodes::IsNotDistinctFrom" do
it "should construct a valid generic SQL statement" do
test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson"
compile(test).must_be_like %{
DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0
}
end

it "should handle column names on both sides" do
test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name]
compile(test).must_be_like %{
DECODE("users"."first_name", "users"."last_name", 0, 1) = 0
}
end

it "should handle nil" do
@table = Table.new(:users)
val = Nodes.build_quoted(nil, @table[:active])
sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val)
sql.must_be_like %{ "users"."name" IS NULL }
end
end

describe "Nodes::IsDistinctFrom" do
it "should handle column names on both sides" do
test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name]
compile(test).must_be_like %{
DECODE("users"."first_name", "users"."last_name", 0, 1) = 1
}
end

it "should handle nil" do
@table = Table.new(:users)
val = Nodes.build_quoted(nil, @table[:active])
sql = compile Nodes::IsDistinctFrom.new(@table[:name], val)
sql.must_be_like %{ "users"."name" IS NOT NULL }
end
end
end
end
end