Skip to content

Commit

Permalink
Impl aggregation function (not very effiecient yet). There is now a D…
Browse files Browse the repository at this point in the history
…SL for creating things like a sum of the revenue grouped by quarter [#72]

Tidied up some RSpecs
  • Loading branch information
andreas committed Oct 26, 2009
1 parent 4de7d5f commit 21f4009
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 121 deletions.
2 changes: 1 addition & 1 deletion lib/neo4j/extensions/aggregate/aggregator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def create_group_for_key(parent, node, key)
# check if it is the leaf node or not
if (@child_dsl)
# this is not the leaf aggregate dsl, let the child node add the node instead
@child_dsl.create_groups(group_node, node) # TODO
@child_dsl.create_groups(group_node, node)
else
# this IS a leaf aggregate dsl, add node to the group
rel_type = node.kind_of?(AggregateGroupNode)? key : :aggregate
Expand Down
24 changes: 8 additions & 16 deletions lib/neo4j/extensions/aggregate/aggregator_each.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ def initialize(root_node, nodes_or_class)
Neo4j.event_handler.add(self)
@filter = nodes_or_class
end
puts "init aggregate_each root #{root_node.neo_node_id}"
@root_node = root_node
@nodes = nodes_or_class
end
Expand Down Expand Up @@ -34,8 +33,6 @@ def on_property_changed(node, prop_key, old_value, new_value) # :nodoc:
return if node.class != @filter
return unless @group_by.include?(prop_key.to_sym)

puts "on_property_changed node: #{node.neo_node_id} prop: #{prop_key} old: #{old_value} new: #{new_value}"

# for each aggregate the node belongs to delete it
# we have to first create it and then deleted, otherwise cascade delete will kick in
group = node.aggregate_groups(@root_node.aggregate_id)
Expand All @@ -45,15 +42,6 @@ def on_property_changed(node, prop_key, old_value, new_value) # :nodoc:

# delete this aggregate group if it exists
group.delete if group

# puts "EXIT NODE CHANGED ===="
# puts "OUTGOING NODE"
# node.print 3, :outgoing
# puts "INCOMING NODE"
# node.print 3, :incoming
#
# puts "ROOT NODE, OUTGOING"
@root_node.print 3, :outgoing
end

# Specifies which properties we should group on.
Expand All @@ -65,20 +53,24 @@ def group_by(*keys)
self
end

def with(prop_key, &proc)
@with_proc = proc
@prop_key = prop_key
end

def execute(nodes = @nodes)
return unless nodes
nodes.each do |node|
group_node = GroupEachNode.new
group_node.group_by = @group_by.join(',')
group_node.aggregate = node
puts " create rel between #{node.neo_node_id} and group_node #{group_node.neo_node_id}"
rel = group_node.relationships.outgoing(:aggregate)[node]
puts " found rel #{rel.neo_relationship_id} agg_id: #{@root_node.aggregate_id.to_s} start: #{rel.start_node.neo_node_id}, end: #{rel.end_node.neo_node_id}"
rel[:aggregate_group] = @root_node.aggregate_id.to_s
@root_node.groups << group_node
puts " created rel between root #{@root_node.neo_node_id} and #{group_node.neo_node_id}"
# group_node.print 3, :both
if @with_proc
val = group_node.inject(0) {|sum, val| next sum if val.nil?; @with_proc.call(sum, val, 0)}
group_node[@prop_key.to_s] = val
end
end
@nodes = nil # prevent it to run twice
end
Expand Down
174 changes: 91 additions & 83 deletions test/extensions/aggregate_each_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@

AggregateEachNode = Neo4j::Aggregate::AggregateEachNode

describe "Aggregate Each" do
describe "Aggregate each node" do
before(:all) do
class Company
include Neo4j::NodeMixin
property :month, :revenue
end
end


after(:all) do
undefine_class :Company
end

before(:each) do
start
Neo4j::Transaction.new
Expand All @@ -27,45 +31,65 @@ class Company
@registrations.each {|reg| reg.unregister}
end

it "should create a new group for each node" do
#pending "work in progress"
nodes = []
4.times {nodes << Neo4j::Node.new}
nodes[0][:colour] = 'red'; nodes[0][:name] = "a"; nodes[0][:age] = 0
nodes[1][:colour] = 'red'; nodes[1][:name] = "b"; nodes[1][:age] = 1
nodes[2][:colour] = 'red'; nodes[2][:name] = "c"; nodes[2][:age] = 2
nodes[3][:colour] = 'blue'; nodes[3][:name] = "d"; nodes[3][:age] = 3
describe "Access aggregated node properties" do
before(:each) do
# Neo4j::Transaction.new
@nodes = []
4.times {@nodes << Neo4j::Node.new}
@nodes[0][:colour] = 'red'; @nodes[0][:name] = "a"; @nodes[0][:age] = 0
@nodes[1][:colour] = 'red'; @nodes[1][:name] = "b"; @nodes[1][:age] = 1
@nodes[2][:colour] = 'red'; @nodes[2][:name] = "c"; @nodes[2][:age] = 2
@nodes[3][:colour] = 'blue'; @nodes[3][:name] = "d"; @nodes[3][:age] = 3

# when
@aggregate_node = AggregateEachNode.new
@aggregate_node.aggregate_each(@nodes).group_by(:colour, :name).execute
end

it "can be retrieved as a group Neo4j::NodeMixin#aggregate_groups" do
Neo4j::Transaction.new
# then we should have one group
@nodes[0].aggregate_groups.to_a.size.should == 1
group = @nodes[0].aggregate_groups.to_a[0]

agg1 = AggregateEachNode.new
# and that group should contain the node property values
group.should include('red', 'a')
group.to_a.size.should == 2
group[:age].should == 0 # group for @nodes[0]
end

# when
agg1.aggregate_each(nodes).group_by(:colour, :name).execute
it "can all be retrieved from the aggregate node" do
# there are total 8 property values
@aggregate_node.to_a.sort.should == ["blue", "d", "red", "c", "red", "b", "red", "a"].sort
end

# then
nodes[0].aggregate_groups.to_a.size.should == 1
g1 = nodes[0].aggregate_groups.to_a[0]
g1.should include('red', 'a')
g1.to_a.size.should == 2
g1[:age].should == 0 # group for @nodes[0]
it "can be retrieved from the aggregate node as an group" do
# there are total 4 groups
@aggregate_node.aggregate_size.should == 4

agg1.to_a.sort.should == ["blue", "d", "red", "c", "red", "b", "red", "a"].sort
agg1.to_a.size.should == 8
agg1.aggregate_size.should == 4
# so that we know which group is which group we put it in a age_groups map
age_groups = {}
@aggregate_node.groups.each {|g| age_groups[g[:age]] = g}

age_groups[3].should include('blue','d')
age_groups[0].should include('red','a')
end
end

it "should delete group if the node is deleted" do
nodes = []
4.times {nodes << Neo4j::Node.new}
nodes[0][:colour] = 'red'; nodes[0][:name] = "a"; nodes[0][:age] = 0
nodes[1][:colour] = 'red'; nodes[1][:name] = "b"; nodes[1][:age] = 1
nodes[2][:colour] = 'red'; nodes[2][:name] = "c"; nodes[2][:age] = 2
nodes[0][:colour] = 'red'; nodes[0][:name] = "a"; nodes[0][:age] = 0
nodes[1][:colour] = 'red'; nodes[1][:name] = "b"; nodes[1][:age] = 1
nodes[2][:colour] = 'red'; nodes[2][:name] = "c"; nodes[2][:age] = 2
nodes[3][:colour] = 'blue'; nodes[3][:name] = "d"; nodes[3][:age] = 3

agg1 = AggregateEachNode.new
agg1.aggregate_each(nodes).group_by(:colour, :name)
agg1.to_a.size.should == 8
agg1.aggregate_size.should == 4
agg1.groups.size.should == 4


# when
n = nodes[2].aggregate_groups.to_a[0]
Expand All @@ -83,7 +107,7 @@ class Company
it "should create new groups for each node, group by quarter" do
revenue1 = Neo4j::Node.new
revenue2 = Neo4j::Node.new
revenue1[:jan] = 1; revenue1[:feb] = 2; revenue1[:mars] = 3; revenue1[:april] = 4; revenue1[:may] = 5; revenue1[:june] = 6
revenue1[:jan] = 1; revenue1[:feb] = 2; revenue1[:mars] = 3; revenue1[:april] = 4; revenue1[:may] = 5; revenue1[:june] = 6
revenue2[:jan] = 11; revenue2[:feb] = 12; revenue2[:mars] = 13; revenue2[:april] = 14; revenue2[:may] = 15; revenue2[:june] = 16

# when
Expand All @@ -96,8 +120,8 @@ class Company
# then there should be two groups, one for each revenue node
q1.aggregate_size.should == 2
# with each 3 values, total 6 (2*3)
q1.to_a.sort.should == [1,2,3,11,12,13]
q2.to_a.sort.should == [4,5,6,14,15,16]
q1.to_a.sort.should == [1, 2, 3, 11, 12, 13]
q2.to_a.sort.should == [4, 5, 6, 14, 15, 16]
end


Expand All @@ -107,29 +131,29 @@ class Company

# when
c1 = Company.new
c1[:jan] = 100
c1[:feb] = 200
c1[:jan] = 100
c1[:feb] = 200
c1[:mars] = 300
c1[:april] = 400
c1[:may] = 500
c1[:april] = 400
c1[:may] = 500
c1[:june] = 600

c2 = Company.new
c2[:jan] = 1100
c2[:feb] = 1200
c2[:jan] = 1100
c2[:feb] = 1200
c2[:mars] = 1300
c2[:april] = 1400
c2[:may] = 1500
c2[:april] = 1400
c2[:may] = 1500
c2[:june] = 1600

q1.aggregate_each([c1,c2]).group_by(:jan, :feb, :mars).execute
q2.aggregate_each([c1,c2]).group_by(:april, :may, :june).execute
q1.aggregate_each([c1, c2]).group_by(:jan, :feb, :mars).execute
q2.aggregate_each([c1, c2]).group_by(:april, :may, :june).execute

# then
q1.groups.should include(c1.aggregate_groups(:q1))
q1.groups.should include(c2.aggregate_groups(:q1))
q2.groups.should include(c1.aggregate_groups(:q2))
q2.groups.should include(c2.aggregate_groups(:q2))
q2.groups.should include(c2.aggregate_groups(:q2))
end

it "should allow to register nodes classes to be part of aggregates" do
Expand All @@ -141,24 +165,24 @@ class Company

# when
c1 = Company.new
c1[:jan] = 100
c1[:feb] = 200
c1[:jan] = 100
c1[:feb] = 200
c1[:mars] = 300
c1[:april] = 400
c1[:may] = 500
c1[:april] = 400
c1[:may] = 500
c1[:june] = 600

c2 = Company.new
c2[:jan] = 1100
c2[:feb] = 1200
c2[:jan] = 1100
c2[:feb] = 1200
c2[:mars] = 1300
c2[:april] = 1400
c2[:may] = 1500
c2[:april] = 1400
c2[:may] = 1500
c2[:june] = 1600

# then
q1.should include(100,200,300,1100,1200,1300)
q2.should include(400,500,600,1400,1500,1600)
q1.should include(100, 200, 300, 1100, 1200, 1300)
q2.should include(400, 500, 600, 1400, 1500, 1600)
end

it "should update the aggregate when a node changes" do
Expand All @@ -167,9 +191,9 @@ class Company

# given
c1 = Company.new
c1[:jan] = 100
c1[:feb] = 200
q1.should include(100,200)
c1[:jan] = 100
c1[:feb] = 200
q1.should include(100, 200)

# when
c1[:feb] = 42
Expand All @@ -179,65 +203,49 @@ class Company
q1.should include(42)
end

it "should delete the aggregate when the node is deleted" do
it "should delete the group when the node is deleted" do
pending
q1 = AggregateEachNode.new(:q1)
@registrations << q1.aggregate_each(Company).group_by(:jan, :feb, :mars)

# given
c1 = Company.new
c1[:jan] = 100
c1[:feb] = 200
q1.should include(100,200)
c1[:jan] = 100
c1[:feb] = 200
q1.should include(100, 200)
q1.groups.size.should == 1
Neo4j.load(q1.neo_node_id).should_not be_nil

# then
# puts "C1, incoming"
# c1.print 3, :incoming
#
# puts "C1, outgoing"
# c1.print 3, :outgoing
#
# puts "Q1, incoming"
# q1.print 3, :incoming
#
# puts "Q1, outgoing"
# q1.print 3, :outgoing
puts "------------------------"
puts "D E L E T E"
puts "------------------------"

# when
c1.delete

# then
puts "Q1------------------"
q1.print 2,:both
q1.groups.size.should == 0

q1.should_not include(100)
q1.should_not include(200)
end

it "should allow to register nodes classes to be part of aggregates" do
pending
# pending
# given
q1 = AggregateEachNode.new(:q1)
q1.aggregate_each(Company).group_by(:jan, :feb, :mars).with(:sum){|sum,val,prev_val| sum + val - prev_val}
q1.aggregate_each(Company).group_by(:jan, :feb, :mars).with(:sum){|sum, val, prev_val| sum + val - prev_val}
q2 = AggregateEachNode.new(:q2)
q2.aggregate_each(Company).group_by(:april, :may, :june)
q2.aggregate_each(Company).group_by(:april, :may, :june).with(:sum){|sum, val, prev_val| sum + val - prev_val}

# when
c1 = Company.new
c1[:jan] = 100
c1[:feb] = 200
c1[:jan] = 100
c1[:feb] = 200
c1[:mars] = 300
c1[:april] = 400
c1[:may] = 500
c1[:april] = 400
c1[:may] = 500
c1[:june] = 600

# then
puts "C1 GROUP Q1: #{c1.aggregate_groups(:q1).neo_node_id}"
puts "C1 GROUP Q2: #{c1.aggregate_groups(:q2).neo_node_id}"

c1.aggregate_groups(:q1)[:sum].should == 100+200+300
c1.aggregate_groups(:q2)[:sum].should == 400+500+600
end
Expand Down

0 comments on commit 21f4009

Please sign in to comment.