Skip to content

Commit

Permalink
Support ANSI SQL2003 window functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Staubo committed Feb 22, 2012
1 parent 6e427e5 commit a1a6fbc
Show file tree
Hide file tree
Showing 14 changed files with 390 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/arel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

require 'arel/expressions'
require 'arel/predications'
require 'arel/window_predications'
require 'arel/math'
require 'arel/alias_predication'
require 'arel/order_predications'
Expand Down
4 changes: 4 additions & 0 deletions lib/arel/nodes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
require 'arel/nodes/delete_statement'
require 'arel/nodes/table_alias'
require 'arel/nodes/infix_operation'
require 'arel/nodes/over'

# nary
require 'arel/nodes/and'
Expand All @@ -38,6 +39,9 @@
require 'arel/nodes/values'
require 'arel/nodes/named_function'

# windows
require 'arel/nodes/window'

# joins
require 'arel/nodes/inner_join'
require 'arel/nodes/outer_join'
Expand Down
1 change: 1 addition & 0 deletions lib/arel/nodes/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Nodes
class Function < Arel::Nodes::Node
include Arel::Expression
include Arel::Predications
include Arel::WindowPredications
attr_accessor :expressions, :alias, :distinct

def initialize expr, aliaz = nil
Expand Down
13 changes: 13 additions & 0 deletions lib/arel/nodes/over.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Arel
module Nodes

class Over < Binary
def initialize(left, right = nil)
super(left, right)
end

def operator; 'OVER' end
end

end
end
4 changes: 3 additions & 1 deletion lib/arel/nodes/select_core.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Arel
module Nodes
class SelectCore < Arel::Nodes::Node
attr_accessor :top, :projections, :wheres, :groups
attr_accessor :top, :projections, :wheres, :groups, :windows
attr_accessor :having, :source, :set_quantifier

def initialize
Expand All @@ -14,6 +14,7 @@ def initialize
@wheres = []
@groups = []
@having = nil
@windows = []
end

def from
Expand All @@ -34,6 +35,7 @@ def initialize_copy other
@wheres = @wheres.clone
@groups = @groups.clone
@having = @having.clone if @having
@windows = @windows.clone
end
end
end
Expand Down
78 changes: 78 additions & 0 deletions lib/arel/nodes/window.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
module Arel
module Nodes
class Window < Arel::Nodes::Node
include Arel::Expression
attr_accessor :orders, :framing

def initialize
@orders = []
end

def order *expr
# FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
@orders.concat expr.map { |x|
String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
}
self
end

def frame(expr)
raise ArgumentError, "Window frame cannot be set more than once" if @frame
@framing = expr
end

def rows(expr = nil)
frame(Rows.new(expr))
end

def range(expr = nil)
frame(Range.new(expr))
end

def initialize_copy other
super
@orders = @orders.map { |x| x.clone }
end
end

class NamedWindow < Window
attr_accessor :name

def initialize name
super()
@name = name
end

def initialize_copy other
super
@name = other.name.clone
end
end

class Rows < Unary
def initialize(expr = nil)
super(expr)
end
end

class Range < Unary
def initialize(expr = nil)
super(expr)
end
end

class CurrentRow < Arel::Nodes::Node; end

class Preceding < Unary
def initialize(expr = nil)
super(expr)
end
end

class Following < Unary
def initialize(expr = nil)
super(expr)
end
end
end
end
6 changes: 6 additions & 0 deletions lib/arel/select_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ def having *exprs
self
end

def window name
window = Nodes::NamedWindow.new(name)
@ctx.windows.push window
window
end

def project *projections
# FIXME: converting these to SQLLiterals is probably not good, but
# rails tests require it.
Expand Down
2 changes: 2 additions & 0 deletions lib/arel/visitors/depth_first.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def terminal o
alias :visit_Arel_Nodes_Node :terminal
alias :visit_Arel_Nodes_SqlLiteral :terminal
alias :visit_Arel_Nodes_BindParam :terminal
alias :visit_Arel_Nodes_Window :terminal
alias :visit_Arel_SqlLiteral :terminal
alias :visit_BigDecimal :terminal
alias :visit_Bignum :terminal
Expand All @@ -136,6 +137,7 @@ def visit_Arel_Nodes_SelectCore o
visit o.source
visit o.wheres
visit o.groups
visit o.windows
visit o.having
end

Expand Down
19 changes: 19 additions & 0 deletions lib/arel/visitors/dot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ def unary o
alias :visit_Arel_Nodes_On :unary
alias :visit_Arel_Nodes_Top :unary
alias :visit_Arel_Nodes_UnqualifiedColumn :unary
alias :visit_Arel_Nodes_Preceding :unary
alias :visit_Arel_Nodes_Following :unary
alias :visit_Arel_Nodes_Rows :unary
alias :visit_Arel_Nodes_Range :unary

def window o
visit_edge o, "orders"
visit_edge o, "framing"
end
alias :visit_Arel_Nodes_Window :window

def named_window o
visit_edge o, "orders"
visit_edge o, "framing"
visit_edge o, "name"
end
alias :visit_Arel_Nodes_NamedWindow :named_window

def function o
visit_edge o, "expressions"
Expand Down Expand Up @@ -103,6 +120,7 @@ def visit_Arel_Nodes_SelectCore o
visit_edge o, "source"
visit_edge o, "projections"
visit_edge o, "wheres"
visit_edge o, "windows"
end

def visit_Arel_Nodes_SelectStatement o
Expand Down Expand Up @@ -159,6 +177,7 @@ def binary o
alias :visit_Arel_Nodes_NotEqual :binary
alias :visit_Arel_Nodes_NotIn :binary
alias :visit_Arel_Nodes_Or :binary
alias :visit_Arel_Nodes_Over :binary

def visit_String o
@node_stack.last.fields << o
Expand Down
54 changes: 54 additions & 0 deletions lib/arel/visitors/to_sql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def visit_Arel_Nodes_SelectCore o
("WHERE #{o.wheres.map { |x| visit x }.join ' AND ' }" unless o.wheres.empty?),
("GROUP BY #{o.groups.map { |x| visit x }.join ', ' }" unless o.groups.empty?),
(visit(o.having) if o.having),
("WINDOW #{o.windows.map { |x| visit x }.join ', ' }" unless o.windows.empty?)
].compact.join ' '
end

Expand Down Expand Up @@ -175,6 +176,59 @@ def visit_Arel_Nodes_Except o
"( #{visit o.left} EXCEPT #{visit o.right} )"
end

def visit_Arel_Nodes_NamedWindow o
"#{quote_column_name o.name} AS #{visit_Arel_Nodes_Window o}"
end

def visit_Arel_Nodes_Window o
s = [
("ORDER BY #{o.orders.map { |x| visit(x) }.join(', ')}" unless o.orders.empty?),
(visit o.framing if o.framing)
].compact.join ' '
"(#{s})"
end

def visit_Arel_Nodes_Rows o
if o.expr
"ROWS #{visit o.expr}"
else
"ROWS"
end
end

def visit_Arel_Nodes_Range o
if o.expr
"RANGE #{visit o.expr}"
else
"RANGE"
end
end

def visit_Arel_Nodes_Preceding o
"#{o.expr ? visit(o.expr) : 'UNBOUNDED'} PRECEDING"
end

def visit_Arel_Nodes_Following o
"#{o.expr ? visit(o.expr) : 'UNBOUNDED'} FOLLOWING"
end

def visit_Arel_Nodes_CurrentRow o
"CURRENT ROW"
end

def visit_Arel_Nodes_Over o
case o.right
when nil
"#{visit o.left} OVER ()"
when Arel::Nodes::SqlLiteral
"#{visit o.left} OVER #{visit o.right}"
when String, Symbol
"#{visit o.left} OVER #{quote_column_name o.right.to_s}"
else
"#{visit o.left} OVER #{visit o.right}"
end
end

def visit_Arel_Nodes_Having o
"HAVING #{visit o.expr}"
end
Expand Down
9 changes: 9 additions & 0 deletions lib/arel/window_predications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Arel
module WindowPredications

def over(expr = nil)
Nodes::Over.new(self, expr)
end

end
end
40 changes: 40 additions & 0 deletions test/nodes/test_over.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'helper'

describe Arel::Nodes::Over do
describe 'with literal' do
it 'should reference the window definition by name' do
table = Arel::Table.new :users
table[:id].count.over('foo').to_sql.must_be_like %{
COUNT("users"."id") OVER "foo"
}
end
end

describe 'with SQL literal' do
it 'should reference the window definition by name' do
table = Arel::Table.new :users
table[:id].count.over(Arel.sql('foo')).to_sql.must_be_like %{
COUNT("users"."id") OVER foo
}
end
end

describe 'with no expression' do
it 'should use empty definition' do
table = Arel::Table.new :users
table[:id].count.over.to_sql.must_be_like %{
COUNT("users"."id") OVER ()
}
end
end

describe 'with expression' do
it 'should use definition in sub-expression' do
table = Arel::Table.new :users
window = Arel::Nodes::Window.new.order(table['foo'])
table[:id].count.over(window).to_sql.must_be_like %{
COUNT("users"."id") OVER (ORDER BY \"users\".\"foo\")
}
end
end
end
Loading

0 comments on commit a1a6fbc

Please sign in to comment.